easyui的tree获取父节点_力扣 1519——子数中标签相同的节点数
本題主要在于對(duì)樹(shù)這種數(shù)據(jù)結(jié)構(gòu)的考察,以及深度優(yōu)先遍歷的使用,優(yōu)化時(shí)可以采取空間換時(shí)間的策略。
原題
給你一棵樹(shù)(即,一個(gè)連通的無(wú)環(huán)無(wú)向圖),這棵樹(shù)由編號(hào)從 0 到 n - 1 的 n 個(gè)節(jié)點(diǎn)組成,且恰好有 n - 1 條 edges 。樹(shù)的根節(jié)點(diǎn)為節(jié)點(diǎn) 0 ,樹(shù)上的每一個(gè)節(jié)點(diǎn)都有一個(gè)標(biāo)簽,也就是字符串 labels 中的一個(gè)小寫(xiě)字符(編號(hào)為 i 的 節(jié)點(diǎn)的標(biāo)簽就是 labels[i] )
邊數(shù)組 edges 以 edges[i] = [ai, bi] 的形式給出,該格式表示節(jié)點(diǎn) ai 和 bi 之間存在一條邊。
返回一個(gè)大小為 n 的數(shù)組,其中 ans[i] 表示第 i 個(gè)節(jié)點(diǎn)的子樹(shù)中與節(jié)點(diǎn) i 標(biāo)簽相同的節(jié)點(diǎn)數(shù)。
樹(shù) T 中的子樹(shù)是由 T 中的某個(gè)節(jié)點(diǎn)及其所有后代節(jié)點(diǎn)組成的樹(shù)。
示例 1:
輸入:n = 7, edges = [[0,1],[0,2],[1,4],[1,5],[2,3],[2,6]], labels = "abaedcd"輸出:[2,1,1,1,1,1,1]解釋:節(jié)點(diǎn) 0 的標(biāo)簽為 'a' ,以 'a' 為根節(jié)點(diǎn)的子樹(shù)中,節(jié)點(diǎn) 2 的標(biāo)簽也是 'a' ,因此答案為 2 。注意樹(shù)中的每個(gè)節(jié)點(diǎn)都是這棵子樹(shù)的一部分。節(jié)點(diǎn) 1 的標(biāo)簽為 'b' ,節(jié)點(diǎn) 1 的子樹(shù)包含節(jié)點(diǎn) 1、4 和 5,但是節(jié)點(diǎn) 4、5 的標(biāo)簽與節(jié)點(diǎn) 1 不同,故而答案為 1(即,該節(jié)點(diǎn)本身)。示例 2:
輸入:n = 4, edges = [[0,1],[1,2],[0,3]], labels = "bbbb"輸出:[4,2,1,1]解釋:節(jié)點(diǎn) 2 的子樹(shù)中只有節(jié)點(diǎn) 2 ,所以答案為 1 。節(jié)點(diǎn) 3 的子樹(shù)中只有節(jié)點(diǎn) 3 ,所以答案為 1 。節(jié)點(diǎn) 1 的子樹(shù)中包含節(jié)點(diǎn) 1 和 2 ,標(biāo)簽都是 'b' ,因此答案為 2 。節(jié)點(diǎn) 0 的子樹(shù)中包含節(jié)點(diǎn) 0、1、2 和 3,標(biāo)簽都是 'b',因此答案為 4 。示例 3 :
輸入:n = 5, edges = [[0,1],[0,2],[1,3],[0,4]], labels = "aabab"輸出:[3,2,1,1,1]示例 4:
輸入:n = 6, edges = [[0,1],[0,2],[1,3],[3,4],[4,5]], labels = "cbabaa"輸出:[1,2,1,1,2,1]示例 5:
輸入:n = 7, edges = [[0,1],[1,2],[2,3],[3,4],[4,5],[5,6]], labels = "aaabaaa"輸出:[6,5,4,1,3,2,1]提示:
- 1 <= n <= 10^5
- edges.length == n - 1
- edges[i].length == 2
- 0 <= ai, bi < n
- ai != bi
- labels.length == n
- labels 僅由小寫(xiě)英文字母組成
原題 url:https://leetcode-cn.com/problems/number-of-nodes-in-the-sub-tree-with-the-same-label
解題
首次嘗試
這道題是要讓我們計(jì)算:在子樹(shù)中,和當(dāng)前節(jié)點(diǎn)字符相同的節(jié)點(diǎn)個(gè)數(shù)。
那么我們就必然需要構(gòu)建樹(shù)中各個(gè)節(jié)點(diǎn)的關(guān)系,那么就需要記錄父子節(jié)點(diǎn)的關(guān)系,因?yàn)槭瞧胀ǖ臉?shù),一個(gè)節(jié)點(diǎn)的子節(jié)點(diǎn)可能有多個(gè),因此我用LinkedList[] tree這樣一個(gè)數(shù)組進(jìn)行存儲(chǔ),其中tree[i]代表節(jié)點(diǎn) i 的所有子節(jié)點(diǎn)。
至于求相同節(jié)點(diǎn)的個(gè)數(shù),我想著可以從根節(jié)點(diǎn) 0 開(kāi)始逐個(gè)遍歷,先獲取其第一層子節(jié)點(diǎn),再根據(jù)第一層子節(jié)點(diǎn)逐個(gè)獲取,可以采用廣度優(yōu)先遍歷的形式。
讓我們看看代碼:
class Solution { public int[] countSubTrees(int n, int[][] edges, String labels) { // 構(gòu)造樹(shù) LinkedList[] tree = new LinkedList[n]; for (int[] edge : edges) { // edge[0]的子節(jié)點(diǎn) LinkedList child = tree[edge[0]]; if (child == null) { child = new LinkedList<>(); tree[edge[0]] = child; } // 增加子節(jié)點(diǎn) child.add(edge[1]); } // 結(jié)果 int[] result = new int[n]; // 遍歷并計(jì)算 for (int i = 0; i < n; i++) { // 需要遍歷的字符 char cur = labels.charAt(i); // 該節(jié)點(diǎn)的子樹(shù)中與該字符相同的節(jié)點(diǎn)數(shù) int curCount = 0; // 廣度優(yōu)先遍歷 LinkedList searchList = new LinkedList<>(); searchList.add(i); while(!searchList.isEmpty()) { int index = searchList.removeFirst(); if (cur == labels.charAt(index)) { curCount++; } // 找出該節(jié)點(diǎn)的子樹(shù) if (tree[index] == null) { continue; } searchList.addAll(tree[index]); } result[i] = curCount; } return result; }}提交之后,發(fā)現(xiàn)有錯(cuò)誤。錯(cuò)誤的情況是:
輸入:4[[0,2],[0,3],[1,2]]"aeed"輸出:[1,2,1,1]預(yù)期:[1,1,2,1]根據(jù)這樣輸入,我構(gòu)造出的樹(shù)是:
1 0 / 2 3但根據(jù)預(yù)期結(jié)果反推出來(lái)的樹(shù)是:
0 / 2 3 /1那么輸入中最后給出的[1,2]就不是從父節(jié)點(diǎn)指向子節(jié)點(diǎn),也就是輸入中給出的邊關(guān)聯(lián)的節(jié)點(diǎn)順序,是任意的。
那我們的樹(shù)究竟該如何構(gòu)造呢?
雙向記錄構(gòu)造樹(shù)
既然我們?cè)跇?gòu)造樹(shù)的時(shí)候,無(wú)法直接得出父子關(guān)系,那么就將對(duì)應(yīng)兩個(gè)節(jié)點(diǎn)同時(shí)記錄另一個(gè)節(jié)點(diǎn)。
根據(jù)題目中給出的條件:樹(shù)的根節(jié)點(diǎn)為節(jié)點(diǎn) 0。這樣我們?cè)诒闅v的時(shí)候,就從 0 開(kāi)始,只要 0 關(guān)聯(lián)的節(jié)點(diǎn),一定是 0 的子節(jié)點(diǎn)。將這些節(jié)點(diǎn)進(jìn)行標(biāo)記,這樣再遞歸訪問(wèn)接下來(lái)的節(jié)點(diǎn)時(shí),如果是標(biāo)記過(guò)的,則說(shuō)明是父節(jié)點(diǎn),這樣就可以明確父子節(jié)點(diǎn)關(guān)系了。
至于遍歷的時(shí)候,因?yàn)檫@次我們是不知道父子節(jié)點(diǎn)關(guān)系的,所以無(wú)法直接采用廣度優(yōu)先遍歷,換成深度優(yōu)先遍歷。
讓我們看看代碼:
class Solution { // 總節(jié)點(diǎn)數(shù) int n; // 樹(shù) Map> tree; // 字符串 String labels; // 最終結(jié)果 int[] result; public int[] countSubTrees(int n, int[][] edges, String labels) { this.n = n; this.labels = labels; result = new int[n]; LinkedList list; // 雙向構(gòu)造樹(shù)的關(guān)系 tree = new HashMap<>(n / 4 * 3 + 1); for (int[] edge : edges) { // 添加映射關(guān)系 list = tree.computeIfAbsent(edge[0], k -> new LinkedList<>()); list.add(edge[1]); list = tree.computeIfAbsent(edge[1], k -> new LinkedList<>()); list.add(edge[0]); } // 深度優(yōu)先搜索 dfs(0); return result; } public int[] dfs(int index) { // 當(dāng)前子樹(shù)中,所有字符的個(gè)數(shù) int[] charArray = new int[26]; // 開(kāi)始計(jì)算,標(biāo)志該節(jié)點(diǎn)已經(jīng)計(jì)算過(guò) result[index] = 1; // 獲得其關(guān)聯(lián)的節(jié)點(diǎn) List nodes = tree.get(index); // 遍歷 for (int node : nodes) { // 如果該節(jié)點(diǎn)已經(jīng)訪問(wèn)過(guò) if (result[node] > 0) { continue; } // 遞歸遍歷子節(jié)點(diǎn) int[] array = dfs(node); for (int i = 0; i < 26; i++) { charArray[i] += array[i]; } } // 將當(dāng)前節(jié)點(diǎn)的值計(jì)算一下 charArray[labels.charAt(index) - 'a'] += 1; result[index] = charArray[labels.charAt(index) - 'a']; return charArray; }}提交OK,執(zhí)行用時(shí)136ms,超過(guò)36.71%,內(nèi)存消耗104.5MB,超過(guò)91.38%。
時(shí)間復(fù)雜度上,應(yīng)該是要研究dfs方法中的兩個(gè)for循環(huán),外層肯定是每個(gè)節(jié)點(diǎn)都遍歷一遍,內(nèi)層還需要遍歷26個(gè)英文字母,也就是O(n)。
空間復(fù)雜度上,最大的應(yīng)該就是存儲(chǔ)節(jié)點(diǎn)映射關(guān)系的tree了,里面實(shí)際上就是 2n 個(gè)節(jié)點(diǎn)(因?yàn)槊織l邊對(duì)應(yīng)的兩個(gè)節(jié)點(diǎn)都會(huì)互相存一次對(duì)方),因此也就是O(n)。
雖然過(guò)了,但執(zhí)行速度很慢,可以進(jìn)一步優(yōu)化。
用空間換時(shí)間
針對(duì)我上面的解法,其中tree我是用的Map,雖然其get方法理論上是O(n),但畢竟涉及 hash,可以優(yōu)化成數(shù)組。
至于每次取節(jié)點(diǎn)對(duì)應(yīng)的字符所用的charAt方法,具體其實(shí)是:
public char charAt(int index) { if ((index < 0) || (index >= value.length)) { throw new StringIndexOutOfBoundsException(index); } return value[index]; }每次都會(huì)檢查一次 index,其實(shí)這完全是可以省略的,因此可以提前構(gòu)造好每個(gè)位置對(duì)應(yīng)的值,也用一個(gè)數(shù)組存儲(chǔ)。
讓我們看看新的代碼:
class Solution { // 總節(jié)點(diǎn)數(shù) int n; // 樹(shù) LinkedList[] tree; // 每個(gè)節(jié)點(diǎn)的值(用數(shù)字表示) int[] nodeValueArray; // 最終結(jié)果 int[] result; public int[] countSubTrees(int n, int[][] edges, String labels) { this.n = n; nodeValueArray = new int[n]; result = new int[n]; // 雙向構(gòu)造樹(shù)的關(guān)系 tree = new LinkedList[n]; for (int i = 0; i < n; i++) { tree[i] = new LinkedList<>(); } for (int[] edge : edges) { // 添加映射關(guān)系 tree[edge[0]].add(edge[1]); tree[edge[1]].add(edge[0]); } // 生成節(jié)點(diǎn)的值 for (int i = 0; i < n; i++) { nodeValueArray[i] = labels.charAt(i) - 'a'; } // 深度優(yōu)先搜索 dfs(0); return result; } public int[] dfs(int index) { // 當(dāng)前子樹(shù)中,所有字符的個(gè)數(shù) int[] charArray = new int[26]; // 開(kāi)始計(jì)算,標(biāo)志該節(jié)點(diǎn)已經(jīng)計(jì)算過(guò) result[index] = 1; // 獲得其關(guān)聯(lián)的節(jié)點(diǎn) List nodes = tree[index]; // 遍歷 for (int node : nodes) { // 如果該節(jié)點(diǎn)已經(jīng)訪問(wèn)過(guò) if (result[node] > 0) { continue; } // 遞歸遍歷子節(jié)點(diǎn) int[] array = dfs(node); for (int i = 0; i < 26; i++) { charArray[i] += array[i]; } } // 將當(dāng)前節(jié)點(diǎn)的值計(jì)算一下 charArray[nodeValueArray[index]] += 1; result[index] = charArray[nodeValueArray[index]]; return charArray; }}提交之后,執(zhí)行用時(shí)是96ms,內(nèi)存消耗是402.2MB。看來(lái)優(yōu)化的效果并不明顯。
研究一下目前最優(yōu)解法
這個(gè)解法真的是巧妙,執(zhí)行用時(shí)20ms,超過(guò)了100%,內(nèi)存消耗76.3MB,超過(guò)了100%。
我在代碼中增加了注釋,方便大家理解。但這樣的寫(xiě)法,研究一下是能夠看懂,但讓我想估計(jì)是永遠(yuǎn)不可能想出來(lái),可以讓大家也一起學(xué)習(xí)和借鑒:
public class Solution { static class Next { Next next; Node node; Next(Next next, Node node) { this.next = next; this.node = node; } } static class Node { /** * 當(dāng)前節(jié)點(diǎn)的index */ final int index; /** * 當(dāng)前節(jié)點(diǎn)對(duì)應(yīng)的字符值(減去'a') */ final int ci; /** * 所有關(guān)聯(lián)的節(jié)點(diǎn) */ Next children; /** * 該節(jié)點(diǎn)的父節(jié)點(diǎn) */ Node parent; /** * 子樹(shù)中和該節(jié)點(diǎn)含有相同字符的節(jié)點(diǎn)總個(gè)數(shù) */ int result; /** * 是否還在隊(duì)列中,可以理解為是否已訪問(wèn)過(guò) */ boolean inQueue; public Node(int index, int ci) { this.index = index; this.ci = ci; this.result = 1; } /** * 從后往前,找到當(dāng)前節(jié)點(diǎn)沒(méi)有訪問(wèn)過(guò)的第一個(gè)子節(jié)點(diǎn) */ Node popChild() { for (; ; ) { // 當(dāng)前節(jié)點(diǎn)的所有關(guān)聯(lián)節(jié)點(diǎn) Next n = this.children; // 如果沒(méi)有,說(shuō)明子節(jié)點(diǎn)都遍歷完了 if (n == null) { return null; } // 從后往前移除關(guān)聯(lián)節(jié)點(diǎn) this.children = n.next; // 返回第一個(gè)沒(méi)有訪問(wèn)過(guò)的節(jié)點(diǎn) if (!n.node.inQueue) { return n.node; } } } /** * 訪問(wèn)了該節(jié)點(diǎn) */ Node enqueue(Node[] cnodes) { // 該節(jié)點(diǎn)標(biāo)記為訪問(wèn)過(guò) this.inQueue = true; // 記錄該節(jié)點(diǎn)的父節(jié)點(diǎn) this.parent = cnodes[ci]; // 那么現(xiàn)在該字符值對(duì)應(yīng)的最高節(jié)點(diǎn),就是當(dāng)前節(jié)點(diǎn)。 // 這樣如果之后也遇到相同字符的子節(jié)點(diǎn),就可以為子節(jié)點(diǎn)賦值其父節(jié)點(diǎn),也就是上面一行是有效的 cnodes[ci] = this; return this; } /** * 退出該節(jié)點(diǎn) */ void dequeue(Node[] cnodes, int[] res) { // 之后會(huì)訪問(wèn)該節(jié)點(diǎn)的兄弟節(jié)點(diǎn),因此父節(jié)點(diǎn)需要重新設(shè)置 cnodes[ci] = this.parent; // 設(shè)置當(dāng)前節(jié)點(diǎn)的值 res[index] = this.result; // 父節(jié)點(diǎn)也可以進(jìn)行累加 if (this.parent != null) { this.parent.result += this.result; } } void link(Node x) { // this節(jié)點(diǎn)和x節(jié)點(diǎn),互相綁定 this.children = new Next(this.children, x); x.children = new Next(x.children, this); } } public int[] countSubTrees(int n, int[][] edges, String labels) { // 構(gòu)造樹(shù) Node[] nodes = new Node[n]; // 每個(gè)節(jié)點(diǎn)對(duì)應(yīng)的字符 for (int i = 0; i < n; i++) { nodes[i] = new Node(i, labels.charAt(i) - 'a'); } // 通過(guò)邊的關(guān)系,將節(jié)點(diǎn)互相綁定 for (int[] es : edges) { nodes[es[0]].link(nodes[es[1]]); } // 最終的結(jié)果 int[] res = new int[n]; // 當(dāng)前訪問(wèn)的節(jié)點(diǎn)下標(biāo) int sz = 0; // 26個(gè)小寫(xiě)英文字母對(duì)應(yīng)的節(jié)點(diǎn)數(shù)組 Node[] cnodes = new Node[26]; // 下面三行可以合并成這一行: // Node node = nodes[sz++] = nodes[0].enqueue(cnodes); nodes[sz] = nodes[0].enqueue(cnodes); // 當(dāng)前訪問(wèn)的節(jié)點(diǎn) Node node = nodes[sz]; // 因?yàn)楫?dāng)前節(jié)點(diǎn)已經(jīng)訪問(wèn)過(guò),自然下標(biāo)需要+1 sz++; for (; ; ) { // 從后往前,找到當(dāng)前節(jié)點(diǎn)沒(méi)有訪問(wèn)過(guò)的第一個(gè)子節(jié)點(diǎn) Node child = node.popChild(); // 如果已經(jīng)全部訪問(wèn)過(guò)了 if (child == null) { // 開(kāi)始計(jì)算 node.dequeue(cnodes, res); if (--sz == 0) { break; } // 回溯到父節(jié)點(diǎn) node = nodes[sz - 1]; } else { // 保證了相鄰節(jié)點(diǎn)一定是父子節(jié)點(diǎn) node = nodes[sz++] = child.enqueue(cnodes); } } return res; }}總結(jié)
以上就是這道題目我的解答過(guò)程了,不知道大家是否理解了。本題主要在于對(duì)樹(shù)這種數(shù)據(jù)結(jié)構(gòu)的考察,以及深度優(yōu)先遍歷的使用,優(yōu)化時(shí)可以采取空間換時(shí)間的策略。
有興趣的話可以訪問(wèn)我的博客或者關(guān)注我的公眾號(hào)、頭條號(hào),說(shuō)不定會(huì)有意外的驚喜。
https://death00.github.io/
公眾號(hào):健程之道
總結(jié)
以上是生活随笔為你收集整理的easyui的tree获取父节点_力扣 1519——子数中标签相同的节点数的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: python中的位置怎么看_如何知道项目
- 下一篇: python网络编程库_python网络