mirror of
https://github.com/CyC2018/Interview-Notebook.git
synced 2026-10-31 02:53:42 +00:00
Update Leetcode 题解 - 树.md
补充非递归后序遍历代码,补充 LCA、路径和与前缀树等题目的解题思路及复杂度说明;新增非递归遍历和 LCA 示意配图,更新 Trie 配图并补充参考资料。
This commit is contained in:
+81
-12
@@ -37,6 +37,7 @@
|
||||
* [Trie](#trie)
|
||||
* [1. 实现一个 Trie](#1-实现一个-trie)
|
||||
* [2. 实现一个 Trie,用来求前缀和](#2-实现一个-trie用来求前缀和)
|
||||
* [参考资料](#参考资料)
|
||||
<!-- GFM-TOC -->
|
||||
|
||||
|
||||
@@ -71,7 +72,7 @@ public int maxDepth(TreeNode root) {
|
||||
15 7
|
||||
```
|
||||
|
||||
平衡树左右子树高度差都小于等于 1
|
||||
平衡树指的是每个节点的左右子树高度差都小于等于 1。
|
||||
|
||||
```java
|
||||
private boolean result = true;
|
||||
@@ -108,6 +109,8 @@ Input:
|
||||
Return 3, which is the length of the path [4,2,1,3] or [5,2,1,3].
|
||||
```
|
||||
|
||||
以某个节点为最高点的最长路径等于它左右子树的高度之和。最长路径不一定经过根节点,因此在求高度的递归中对每个节点都算一次,取最大值。
|
||||
|
||||
```java
|
||||
private int max = 0;
|
||||
|
||||
@@ -178,7 +181,7 @@ public TreeNode mergeTrees(TreeNode t1, TreeNode t2) {
|
||||
|
||||
### 6. 判断路径和是否等于一个数
|
||||
|
||||
Leetcode : 112. Path Sum (Easy)
|
||||
112\. Path Sum (Easy)
|
||||
|
||||
[Leetcode](https://leetcode.com/problems/path-sum/description/) / [力扣](https://leetcode-cn.com/problems/path-sum/description/)
|
||||
|
||||
@@ -232,6 +235,10 @@ Return 3. The paths that sum to 8 are:
|
||||
|
||||
路径不一定以 root 开头,也不一定以 leaf 结尾,但是必须连续。
|
||||
|
||||
对每个节点各统计一次以它开头的路径,每个节点会被它的每个祖先各访问一次,平衡树中时间复杂度为 O(NlogN),退化成链时为 O(N²)。节点值可达 10^9,sum 一路相减可能溢出 int,因此用 long。
|
||||
|
||||
用[和为 K 的子数组](Leetcode%20题解%20-%20哈希表.md#5-和为-k-的子数组)的前缀和方法可以做到 O(N):哈希表记录根到当前节点路径上各前缀和的出现次数,回溯时减掉。
|
||||
|
||||
```java
|
||||
public int pathSum(TreeNode root, int sum) {
|
||||
if (root == null) return 0;
|
||||
@@ -335,7 +342,9 @@ private boolean isSymmetric(TreeNode t1, TreeNode t2) {
|
||||
|
||||
[Leetcode](https://leetcode.com/problems/minimum-depth-of-binary-tree/description/) / [力扣](https://leetcode-cn.com/problems/minimum-depth-of-binary-tree/description/)
|
||||
|
||||
树的根节点到叶子节点的最小路径长度
|
||||
树的根节点到叶子节点的最小路径长度。
|
||||
|
||||
需要注意的是,只有一侧子树为空时不能取 min,因为这个节点不是叶子,路径只能走另一侧。
|
||||
|
||||
```java
|
||||
public int minDepth(TreeNode root) {
|
||||
@@ -392,6 +401,8 @@ private boolean isLeaf(TreeNode node){
|
||||
Output : 2
|
||||
```
|
||||
|
||||
做法同[两节点的最长路径](#3-两节点的最长路径),只是 dfs 返回的是节点值都与 root 相同的单侧最长路径,孩子的值与 root 不同时这一侧记为 0。
|
||||
|
||||
```java
|
||||
private int path = 0;
|
||||
|
||||
@@ -426,6 +437,8 @@ private int dfs(TreeNode root){
|
||||
Maximum amount of money the thief can rob = 3 + 3 + 1 = 7.
|
||||
```
|
||||
|
||||
相连的两个节点不能同时选:选 root 时加上四个孙子子树的结果(val1),不选时为两个孩子子树的结果之和(val2)。同一子树会被祖父和父节点各算一次,因此用 cache 记录。
|
||||
|
||||
```java
|
||||
Map<TreeNode, Integer> cache = new HashMap<>();
|
||||
|
||||
@@ -461,6 +474,8 @@ Output: 5
|
||||
|
||||
一个节点要么具有 0 个或 2 个子节点,如果有子节点,那么根节点是最小的节点。
|
||||
|
||||
孩子的值大于根时,它就是这棵子树中比根大的最小值;等于根时要递归往下找,-1 表示这一侧没有比根大的值。
|
||||
|
||||
```java
|
||||
public int findSecondMinimumValue(TreeNode root) {
|
||||
if (root == null) return -1;
|
||||
@@ -477,7 +492,7 @@ public int findSecondMinimumValue(TreeNode root) {
|
||||
|
||||
## 层次遍历
|
||||
|
||||
使用 BFS 进行层次遍历。不需要使用两个队列来分别存储当前层的节点和下一层的节点,因为在开始遍历一层的节点时,当前队列中的节点数就是当前层的节点数,只要控制遍历这么多节点数,就能保证这次遍历的都是当前层的节点。
|
||||
使用 BFS 进行层次遍历。不需要使用两个队列来分别存储当前层的节点和下一层的节点,因为在开始遍历一层的节点时,当前队列中的节点数就是当前层的节点数,只要控制遍历这么多节点数,就能保证这次遍历的都是当前层的节点。按层输出(102 题)和之字形输出的写法见[剑指 Offer 32.2](32.2%20把二叉树打印成多行.md)、[32.3](32.3%20按之字形顺序打印二叉树.md)。
|
||||
|
||||
### 1. 一棵树每层节点的平均数
|
||||
|
||||
@@ -527,6 +542,8 @@ Output:
|
||||
7
|
||||
```
|
||||
|
||||
按层遍历时每层先放右孩子、再放左孩子,最后一个出队的节点就是最底层最左边的节点。
|
||||
|
||||
```java
|
||||
public int findBottomLeftValue(TreeNode root) {
|
||||
Queue<TreeNode> queue = new LinkedList<>();
|
||||
@@ -559,7 +576,7 @@ public int findBottomLeftValue(TreeNode root) {
|
||||
|
||||
前序、中序、后序遍历只是在对节点访问的顺序有一点不同,其它都相同。
|
||||
|
||||
① 前序
|
||||
前序遍历:
|
||||
|
||||
```java
|
||||
void dfs(TreeNode root) {
|
||||
@@ -570,7 +587,7 @@ void dfs(TreeNode root) {
|
||||
}
|
||||
```
|
||||
|
||||
② 中序
|
||||
中序遍历:
|
||||
|
||||
```java
|
||||
void dfs(TreeNode root) {
|
||||
@@ -581,7 +598,7 @@ void dfs(TreeNode root) {
|
||||
}
|
||||
```
|
||||
|
||||
③ 后序
|
||||
后序遍历:
|
||||
|
||||
```java
|
||||
void dfs(TreeNode root) {
|
||||
@@ -639,12 +656,43 @@ public List<Integer> postorderTraversal(TreeNode root) {
|
||||
}
|
||||
```
|
||||
|
||||
上面的写法只是输出顺序正确,节点并不是按后序访问的。要真正按后序访问,可以像[中序遍历](#3-非递归实现二叉树的中序遍历)那样一路向左压栈,栈顶节点的右子树为空或刚访问完(右孩子等于上一个访问的节点 prev)时才弹出访问,否则先转向右子树。没有 prev 时,下图第 6 步会再次转向 5,陷入死循环:
|
||||
|
||||
<div align="center"> <img src="pics/lc-tree-postorder-stack.png" width="720px"/> </div><br>
|
||||
|
||||
```java
|
||||
public List<Integer> postorderTraversal(TreeNode root) {
|
||||
List<Integer> ret = new ArrayList<>();
|
||||
Stack<TreeNode> stack = new Stack<>();
|
||||
TreeNode cur = root, prev = null; // prev 为上一个访问的节点
|
||||
while (cur != null || !stack.isEmpty()) {
|
||||
while (cur != null) {
|
||||
stack.push(cur);
|
||||
cur = cur.left;
|
||||
}
|
||||
TreeNode node = stack.peek();
|
||||
if (node.right != null && node.right != prev) {
|
||||
cur = node.right; // 右子树还没访问,先转向右子树
|
||||
} else {
|
||||
stack.pop();
|
||||
ret.add(node.val);
|
||||
prev = node;
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 非递归实现二叉树的中序遍历
|
||||
|
||||
94\. Binary Tree Inorder Traversal (Easy)
|
||||
|
||||
[Leetcode](https://leetcode.com/problems/binary-tree-inorder-traversal/description/) / [力扣](https://leetcode-cn.com/problems/binary-tree-inorder-traversal/description/)
|
||||
|
||||
从 cur 一路向左压栈直到为空,弹出栈顶访问后转向它的右子树。以上面的树为例:
|
||||
|
||||
<div align="center"> <img src="pics/lc-tree-inorder-stack.png" width="640px"/> </div><br>
|
||||
|
||||
```java
|
||||
public List<Integer> inorderTraversal(TreeNode root) {
|
||||
List<Integer> ret = new ArrayList<>();
|
||||
@@ -699,7 +747,9 @@ Output:
|
||||
1
|
||||
```
|
||||
|
||||
题目描述:只保留值在 L \~ R 之间的节点
|
||||
题目描述:只保留值在 L \~ R 之间的节点。
|
||||
|
||||
root 的值大于 R 时,它的右子树也都大于 R,结果就是修剪后的左子树;小于 L 时同理。
|
||||
|
||||
```java
|
||||
public TreeNode trimBST(TreeNode root, int L, int R) {
|
||||
@@ -742,7 +792,7 @@ private void inOrder(TreeNode node, int k) {
|
||||
}
|
||||
```
|
||||
|
||||
递归解法:
|
||||
递归解法:每层都要重新统计左子树的大小,最坏时间复杂度为 O(N²);节点中保存子树大小时只需沿一条路径往下走。
|
||||
|
||||
```java
|
||||
public int kthSmallest(TreeNode root, int k) {
|
||||
@@ -778,7 +828,7 @@ Output: The root of a Greater Tree like this:
|
||||
20 13
|
||||
```
|
||||
|
||||
先遍历右子树。
|
||||
按右、根、左的顺序遍历就是从大到小访问,sum 累加访问过的值并写回当前节点。
|
||||
|
||||
```java
|
||||
private int sum = 0;
|
||||
@@ -815,6 +865,8 @@ private void traver(TreeNode node) {
|
||||
For example, the lowest common ancestor (LCA) of nodes 2 and 8 is 6. Another example is LCA of nodes 2 and 4 is 2, since a node can be a descendant of itself according to the LCA definition.
|
||||
```
|
||||
|
||||
p、q 分在 root 两侧或其中一个就是 root 时,root 就是最近公共祖先。
|
||||
|
||||
```java
|
||||
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
|
||||
if (root.val > p.val && root.val > q.val) return lowestCommonAncestor(root.left, p, q);
|
||||
@@ -841,6 +893,10 @@ public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
|
||||
For example, the lowest common ancestor (LCA) of nodes 5 and 1 is 3. Another example is LCA of nodes 5 and 4 is 5, since a node can be a descendant of itself according to the LCA definition.
|
||||
```
|
||||
|
||||
lowestCommonAncestor 在 root 子树中查找:同时有 p、q 时返回最近公共祖先,只有其中一个时返回那个节点,都没有时返回 null。左右子树都找到时 p、q 分在 root 两侧,root 就是答案,否则返回找到的那一侧。查找 5 和 4 时遇到 5 就直接返回,因为 4 在 5 的子树中,5 就是答案。
|
||||
|
||||
<div align="center"> <img src="pics/lc-tree-lca-return.png" width="600px"/> </div><br>
|
||||
|
||||
```java
|
||||
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
|
||||
if (root == null || root == p || root == q) return root;
|
||||
@@ -856,6 +912,8 @@ public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
|
||||
|
||||
[Leetcode](https://leetcode.com/problems/convert-sorted-array-to-binary-search-tree/description/) / [力扣](https://leetcode-cn.com/problems/convert-sorted-array-to-binary-search-tree/description/)
|
||||
|
||||
取中间的元素作为根,两侧的元素个数最多相差 1,因此树是平衡的。
|
||||
|
||||
```java
|
||||
public TreeNode sortedArrayToBST(int[] nums) {
|
||||
return toBST(nums, 0, nums.length - 1);
|
||||
@@ -889,6 +947,8 @@ One possible answer is: [0,-3,9,-10,null,5], which represents the following heig
|
||||
-10 5
|
||||
```
|
||||
|
||||
做法同上一题,只是链表要用快慢指针找中点,并在它前面断开。每层找中点共 O(N),共 O(logN) 层,时间复杂度为 O(NlogN)。
|
||||
|
||||
```java
|
||||
public TreeNode sortedListToBST(ListNode head) {
|
||||
if (head == null) return null;
|
||||
@@ -936,7 +996,7 @@ Output: True
|
||||
|
||||
使用中序遍历得到有序数组之后,再利用双指针对数组进行查找。
|
||||
|
||||
应该注意到,这一题不能用分别在左右子树两部分来处理这种思想,因为两个待求的节点可能分别在左右子树中。
|
||||
需要注意的是,这一题不能分别在左右子树中递归求解,因为两个待求的节点可能分别在左右子树中。
|
||||
|
||||
```java
|
||||
public boolean findTarget(TreeNode root, int k) {
|
||||
@@ -1018,6 +1078,8 @@ return [2].
|
||||
|
||||
答案可能不止一个,也就是有多个值出现的次数一样多。
|
||||
|
||||
中序遍历时相同的值连续出现,和前一个节点比较就能算出当前值的次数 curCnt;curCnt 超过 maxCnt 时结果只留当前值,等于时追加。例如依次访问 1、2、2,结果从 [1] 变为 [1, 2],再变为 [2]。
|
||||
|
||||
```java
|
||||
private int curCnt = 1;
|
||||
private int maxCnt = 1;
|
||||
@@ -1070,7 +1132,7 @@ class Trie {
|
||||
|
||||
private class Node {
|
||||
Node[] childs = new Node[26];
|
||||
boolean isLeaf;
|
||||
boolean isLeaf; // 是否有单词在这个节点结束,不一定是叶子
|
||||
}
|
||||
|
||||
private Node root = new Node();
|
||||
@@ -1123,6 +1185,8 @@ class Trie {
|
||||
}
|
||||
```
|
||||
|
||||
需要注意的是,substring 每次都复制剩下的字符串,长度为 L 的单词共复制约 L<sup>2</sup> / 2 个字符;改为传入下标或用循环往下走,每个操作是 O(L)。
|
||||
|
||||
### 2. 实现一个 Trie,用来求前缀和
|
||||
|
||||
677\. Map Sum Pairs (Medium)
|
||||
@@ -1189,3 +1253,8 @@ class MapSum {
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 参考资料
|
||||
|
||||
- [LeetCode 题库](https://leetcode.com/problemset/)
|
||||
- Sedgewick R, Wayne K. 算法(第 4 版)[M]. 人民邮电出版社, 2012.
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 155 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 115 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 233 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 147 KiB After Width: | Height: | Size: 148 KiB |
Reference in New Issue
Block a user