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