回顾:Java数据结构------二叉树(Binary Tree)详解
前面一篇文章已经整理过二叉树的基本概念、存储方式和几种遍历方式。
但是二叉树真正想学明白,不能只停留在概念上,还是要回到题目里。
因为很多二叉树题看起来不一样:
- 有的是判断两棵树是否相同
- 有的是判断一棵树是不是另一棵树的子树
- 有的是翻转二叉树
- 有的是构建二叉树
- 有的是最近公共祖先
但真正写代码的时候,本质上都绕不开一个问题:
当前我站在这个节点上,我应该做什么?
只要能把当前节点的事情想清楚,再想明白左右子树要返回什么,很多递归题其实就顺了。
为了方便演示,下面统一使用这个节点结构:
java
class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode(int val) {
this.val = val;
}
}
一、检查两棵树是否相同(LeetCode 100)
题目链接:100. 相同的树
核心:同步比较两棵树的当前位置,结构和值都要对上。
1. 这题怎么想
判断两棵树是否相同,不是只看根节点,也不是只看节点个数。
真正要判断的是:
text
两棵树在每一个对应位置上,结构一样,节点值也一样。
所以递归时不能只拿一个节点,而是要同时拿到两个节点:p 和 q。
当前就站在 p 和 q 这两个位置上,有下面几种情况:
-
p == null && q == null- 两棵树在当前位置都没有节点。
- 这个位置是对上的。
- 返回
true。
-
p == null || q == null- 只有一边有节点。
- 结构已经不一样。
- 返回
false。
-
p.val != q.val- 结构暂时对上了,但节点值不同。
- 返回
false。
-
当前节点值相同
- 当前这一层通过。
- 继续比较左子树和右子树。
最后的递归关系就是:
text
p.left 要和 q.left 相同
p.right 要和 q.right 相同
这两个条件必须同时成立,所以最后使用 &&。
2. 代码实现
java
public boolean isSameTree(TreeNode p, TreeNode q) {
if (p == null && q == null) {
return true;
}
if (p == null || q == null) {
return false;
}
if (p.val != q.val) {
return false;
}
return isSameTree(p.left, q.left) && isSameTree(p.right, q.right);
}
3. 容易错点
这里函数返回的不是"当前两个节点是否相等",而是:
text
以 p 和 q 为根的两棵子树是否完全相同。
所以最后必须是左右子树都相同,不能写成 ||。
二、另一棵树的子树(LeetCode 572)
题目链接:572. 另一棵树的子树
核心:在主树里找一个起点,再判断从这个起点开始是否和
subRoot完全相同。
1. 这题怎么想
这道题可以直接复用上一题的思想。
判断 subRoot 是否是 root 的子树,本质上不是找某几个相同的节点值,而是在 root 这棵大树里找一个节点:
text
以这个节点为根的整棵子树 == subRoot
所以这里有两层事情:
- 在主树
root中寻找可能的起点。 - 每到一个起点,就判断这棵子树是否和
subRoot完全相同。
当前站在主树的某个节点 root 上:
-
如果
root == null- 说明这条路径已经走到底。
- 还没有找到匹配的子树。
- 返回
false。
-
如果
root != null- 先把当前节点当成候选起点。
- 调用
isSameTree(root, subRoot)判断。
如果当前节点可以作为起点,整道题直接返回 true。
如果当前节点不行,再去左子树和右子树继续找:
text
左边找到可以,右边找到也可以。
所以这里用 ||。
2. 代码实现
java
public boolean isSubtree(TreeNode root, TreeNode subRoot) {
if (root == null) {
return false;
}
if (isSameTree(root, subRoot)) {
return true;
}
return isSubtree(root.left, subRoot) || isSubtree(root.right, subRoot);
}
private boolean isSameTree(TreeNode p, TreeNode q) {
if (p == null && q == null) {
return true;
}
if (p == null || q == null) {
return false;
}
if (p.val != q.val) {
return false;
}
return isSameTree(p.left, q.left) && isSameTree(p.right, q.right);
}
3. 容易错点
当前节点值和 subRoot 根节点值相同,只能说明"有可能匹配"。
真正要判断的是从当前节点往下的整棵子树是否完全一致,所以必须调用"相同的树"那套判断逻辑。
三、翻转二叉树(LeetCode 226)
题目链接:226. 翻转二叉树
核心:每到一个节点,就交换它的左右子树。
1. 这题怎么想
翻转二叉树不是交换节点值,而是交换左右子树的引用。
当前站在节点 root 上,它要做的事情非常明确:
text
root.left 和 root.right 交换
交换完当前节点以后,当前这一层就处理完了。
但是子树内部还没有翻转。
所以还要继续让 root.left 和 root.right 做同样的事情。
递归过程可以理解成:
-
当前节点为空
- 没有东西可以交换。
- 返回
null。
-
当前节点不为空
- 交换左右子树。
- 继续翻转左子树。
- 继续翻转右子树。
- 返回当前
root。
最后返回 root 的意思是:
text
以当前节点为根的这棵子树已经翻转完成。
2. 代码实现
java
public TreeNode invertTree(TreeNode root) {
if (root == null) {
return null;
}
TreeNode tmp = root.left;
root.left = root.right;
root.right = tmp;
invertTree(root.left);
invertTree(root.right);
return root;
}
3. 容易错点
交换的是左右子树,不是左右孩子的值。
还有一点,交换完成后继续递归 root.left 和 root.right 是没有问题的,因为它们只是位置变了,子树内部仍然需要继续翻转。
四、判断一棵二叉树是否是平衡二叉树(LeetCode 110)
题目链接:110. 平衡二叉树
核心:让每棵子树向上返回高度,如果已经不平衡,就返回
-1。
1. 这题怎么想
平衡二叉树要求每个节点的左右子树高度差都不超过 1。
注意,是每个节点,不是只判断根节点。
如果从上往下判断,每到一个节点就重新计算左右高度,会重复计算很多次。
更自然的做法是从下往上判断。
当前站在节点 root 上,它需要先知道:
text
左子树高度是多少?
右子树高度是多少?
左右高度差是否超过 1?
但是这里还要考虑一种情况:
如果左子树或者右子树本身已经不平衡,那么当前节点也没必要继续算高度了。
所以可以让 getHeight 返回两类信息:
| 返回值 | 含义 |
|---|---|
0、1、2... |
当前子树的正常高度 |
-1 |
当前子树已经不平衡 |
当前处理 root 时:
-
root == null- 空树高度为 0。
- 返回
0。
-
先看左子树
- 如果左子树返回
-1,说明左边已经不平衡。 - 当前节点直接返回
-1。
- 如果左子树返回
-
再看右子树
- 如果右子树返回
-1,说明右边已经不平衡。 - 当前节点也直接返回
-1。
- 如果右子树返回
-
左右子树都正常
- 比较左右高度差。
- 如果超过 1,返回
-1。 - 否则返回当前子树高度。
主函数只需要判断最终结果是不是 -1。
2. 代码实现
java
public boolean isBalanced(TreeNode root) {
return getHeight(root) != -1;
}
private int getHeight(TreeNode root) {
if (root == null) {
return 0;
}
int leftHeight = getHeight(root.left);
if (leftHeight == -1) {
return -1;
}
int rightHeight = getHeight(root.right);
if (rightHeight == -1) {
return -1;
}
if (Math.abs(leftHeight - rightHeight) > 1) {
return -1;
}
return Math.max(leftHeight, rightHeight) + 1;
}
3. 容易错点
getHeight 不是单纯求高度。
它一边求高度,一边判断平衡。
-1 是一个状态标记,表示"下面已经不平衡了",一旦出现就要直接往上传。
五、判断一棵树是否为对称二叉树(LeetCode 101)
题目链接:101. 对称二叉树
核心:对称不是"左子树等于右子树",而是左右子树互为镜像。
1. 这题怎么想
对称二叉树看的不是左右两棵树是否完全相同,而是它们是否互为镜像。
镜像关系是:
text
左子树的左边 对应 右子树的右边
左子树的右边 对应 右子树的左边
所以这道题需要一个函数同时接收两个节点,比如 left 和 right。
当前站在这两个节点上:
-
两个都为空
- 两边同时走到底。
- 结构仍然对称。
- 返回
true。
-
只有一个为空
- 一边有节点,一边没有节点。
- 结构不对称。
- 返回
false。
-
两个都不为空,但值不同
- 内容不对称。
- 返回
false。
-
两个值相同
- 当前层通过。
- 继续比较外侧和内侧。
外侧和内侧分别是:
text
left.left 和 right.right
left.right 和 right.left
这两组都对称,当前这两棵子树才算镜像。
2. 代码实现
java
public boolean isSymmetric(TreeNode root) {
if (root == null) {
return true;
}
return isMirror(root.left, root.right);
}
private boolean isMirror(TreeNode left, TreeNode right) {
if (left == null && right == null) {
return true;
}
if (left == null || right == null) {
return false;
}
if (left.val != right.val) {
return false;
}
return isMirror(left.left, right.right) && isMirror(left.right, right.left);
}
3. 容易错点
最容易写错的是递归方向。
如果写成:
java
isMirror(left.left, right.left)
那就变成比较两棵树是否相同了。
真正要比较的是外侧和外侧、内侧和内侧。
六、判断一棵树是否是完全二叉树
核心:层序遍历时,第一个空位置后面不能再出现非空节点。
1. 这题怎么想
完全二叉树要求节点从上到下、从左到右连续排列。
换句话说:
text
在层序遍历顺序中,一旦出现空位置,后面就不应该再出现真实节点。
所以这道题适合用队列。
但它和普通层序遍历有一个区别:
普通层序遍历一般不会让 null 入队。
这道题必须让 null 入队。
因为 null 代表树结构里的空洞。
当前按层序顺序处理节点:
-
根节点为空
- 空树可以认为是完全二叉树。
- 返回
true。
-
根节点不为空
- 根节点入队。
- 不断从队列中取节点。
-
取出的节点不是
null- 把它的左孩子入队。
- 把它的右孩子入队。
- 注意:左右孩子即使是
null也要入队。
-
第一次取出
null- 说明层序结构中已经出现空位置。
- 后面如果再出现非空节点,就不是完全二叉树。
2. 代码实现
java
import java.util.LinkedList;
import java.util.Queue;
public boolean isCompleteBinaryTree(TreeNode root) {
if (root == null) {
return true;
}
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
while (!queue.isEmpty()) {
TreeNode cur = queue.poll();
if (cur == null) {
break;
}
queue.offer(cur.left);
queue.offer(cur.right);
}
while (!queue.isEmpty()) {
TreeNode cur = queue.poll();
if (cur != null) {
return false;
}
}
return true;
}
3. 容易错点
这道题最关键的就是 null 也要入队。
如果不保存空位置,就无法判断"空洞后面是否还有节点"。
另外如果使用 ArrayDeque,它不允许存放 null,这里可以使用 LinkedList。
七、二叉树构建及遍历
题目链接:TSINGK110 二叉树遍历
核心:输入如果是前序形式,构建时也要按"根、左、右"的顺序消耗字符。
1. 这题怎么想
这类题一般会给一串字符,用特殊字符表示空节点。
常见输入方式是前序:
text
根 左子树 右子树
其中 # 表示当前位置为空。
所以构建树的时候,也应该按照前序顺序读取字符串。
当前正在构建一棵子树:
-
读取当前字符。
-
如果字符是
#- 当前这个位置没有节点。
- 返回
null。 - 这个
null会挂回上一层的left或right。
-
如果字符不是
#- 创建当前根节点。
- 继续构建左子树。
- 再继续构建右子树。
- 返回当前根节点。
这里需要一个 index 记录字符串读到哪里。
因为左子树构建时会消耗一部分字符,左子树结束以后,右子树必须从新的位置继续读。
2. 代码实现
java
public class Main {
private static int index = 0;
static class TreeNode {
char val;
TreeNode left;
TreeNode right;
TreeNode(char val) {
this.val = val;
}
}
public static TreeNode buildTree(String str) {
if (index >= str.length()) {
return null;
}
char ch = str.charAt(index++);
if (ch == '#') {
return null;
}
TreeNode root = new TreeNode(ch);
root.left = buildTree(str);
root.right = buildTree(str);
return root;
}
public static void inorder(TreeNode root) {
if (root == null) {
return;
}
inorder(root.left);
System.out.print(root.val + " ");
inorder(root.right);
}
}
3. 容易错点
# 不能直接忽略。
它虽然不是节点,但它表示一个空位置。
如果不处理 #,左右子树结构会乱掉。
八、最近公共祖先(LeetCode 236)
题目链接:236. 二叉树的最近公共祖先
核心:让左右子树向上汇报有没有找到
p或q。
1. 这题怎么想
最近公共祖先不一定要把两条路径都存下来。
更自然的方式是让递归从下往上返回信息。
当前站在节点 root 上,它只关心三件事:
text
左子树有没有找到目标?
右子树有没有找到目标?
当前节点自己是不是目标?
返回值的含义非常重要:
| 返回值 | 含义 |
|---|---|
null |
当前子树没有找到 p 或 q |
p 或 q |
当前子树找到了其中一个目标 |
| 某个祖先节点 | 最近公共祖先已经在下面确定 |
当前递归过程可以这样看:
-
root == null- 当前路径没有目标。
- 返回
null。
-
root == p || root == q- 当前节点就是目标之一。
- 返回
root。
-
当前节点不是目标
- 去左子树找,结果记为
leftTree。 - 去右子树找,结果记为
rightTree。
- 去左子树找,结果记为
-
左右两边都不为空
- 说明
p和q分别在当前节点两侧。 - 当前节点就是最近公共祖先。
- 返回
root。
- 说明
-
只有一边不为空
- 说明目标或者答案在那一边。
- 继续把那一边的结果向上传。
2. 代码实现
java
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if (root == null) {
return null;
}
if (root == p || root == q) {
return root;
}
TreeNode leftTree = lowestCommonAncestor(root.left, p, q);
TreeNode rightTree = lowestCommonAncestor(root.right, p, q);
if (leftTree != null && rightTree != null) {
return root;
}
if (leftTree != null) {
return leftTree;
}
if (rightTree != null) {
return rightTree;
}
return null;
}
3. 容易错点
当左右两边都不为空时,当前节点必须返回自己。
因为这说明两个目标节点已经在当前节点这里汇合。
如果只有一边不为空,不要强行返回当前节点,而是把那一边的结果继续向上传。
九、前序和中序构建二叉树(LeetCode 105)
题目链接:105. 从前序与中序遍历序列构造二叉树
核心:前序确定根,中序划分左右子树范围。
1. 这题怎么想
先看两个遍历顺序:
text
前序:根 左 右
中序:左 根 右
前序数组中当前第一个还没使用的元素,一定是当前子树的根节点。
中序数组中,根节点左边是左子树,根节点右边是右子树。
所以这道题每一层都做同一件事:
text
用前序拿根节点,用中序切左右范围。
假设当前正在构建中序范围 [inBegin, inEnd] 对应的子树:
-
inBegin > inEnd- 当前范围没有节点。
- 返回
null。
-
从前序数组中取根节点
- 使用
preIndex从前往后取。 - 创建当前根节点。
- 使用
-
在中序数组中找到根节点位置
rootIndexinBegin ~ rootIndex - 1是左子树。rootIndex + 1 ~ inEnd是右子树。
-
先构建左子树,再构建右子树
- 因为前序数组中根节点后面先出现左子树节点。
2. 代码实现
java
public class Solution {
private int preIndex = 0;
public TreeNode buildTree(int[] preorder, int[] inorder) {
return build(preorder, inorder, 0, inorder.length - 1);
}
private TreeNode build(int[] preorder, int[] inorder, int inBegin, int inEnd) {
if (inBegin > inEnd) {
return null;
}
int rootVal = preorder[preIndex++];
TreeNode root = new TreeNode(rootVal);
int rootIndex = findVal(inorder, inBegin, inEnd, rootVal);
root.left = build(preorder, inorder, inBegin, rootIndex - 1);
root.right = build(preorder, inorder, rootIndex + 1, inEnd);
return root;
}
private int findVal(int[] inorder, int begin, int end, int val) {
for (int i = begin; i <= end; i++) {
if (inorder[i] == val) {
return i;
}
}
return -1;
}
}
3. 容易错点
这道题一定要维护中序范围。
每一层递归构建的是某一段中序范围对应的子树,不是一直对整棵树操作。
另外,前序加中序构建时,要先构建左子树,再构建右子树。
十、中序和后序构建二叉树(LeetCode 106)
题目链接:106. 从中序与后序遍历序列构造二叉树
核心:后序从后往前取根,中序继续划分范围。
1. 这题怎么想
先看遍历顺序:
text
中序:左 根 右
后序:左 右 根
后序数组的最后一个元素,一定是当前子树的根节点。
拿到根节点以后,仍然可以去中序数组中找到它的位置,然后划分左右子树范围。
这道题和前序加中序很像,真正不同的是取根节点的方向。
前序是从前往后取根。
后序是从后往前取根。
当前构建中序范围 [inBegin, inEnd]:
- 范围为空,返回
null。 - 从后序数组中取当前根节点。
- 在中序数组中找到根节点位置。
- 根据中序位置划分左右子树。
这里最关键的是:
text
后序从后往前读,顺序是:根 右 左
所以构建时必须先构建右子树,再构建左子树。
2. 代码实现
java
public class Solution {
private int postIndex;
public TreeNode buildTree(int[] inorder, int[] postorder) {
postIndex = postorder.length - 1;
return build(inorder, postorder, 0, inorder.length - 1);
}
private TreeNode build(int[] inorder, int[] postorder, int inBegin, int inEnd) {
if (inBegin > inEnd) {
return null;
}
int rootVal = postorder[postIndex--];
TreeNode root = new TreeNode(rootVal);
int rootIndex = findVal(inorder, inBegin, inEnd, rootVal);
root.right = build(inorder, postorder, rootIndex + 1, inEnd);
root.left = build(inorder, postorder, inBegin, rootIndex - 1);
return root;
}
private int findVal(int[] inorder, int begin, int end, int val) {
for (int i = begin; i <= end; i++) {
if (inorder[i] == val) {
return i;
}
}
return -1;
}
}
3. 容易错点
中序加后序构建时,最容易错的是先构建左子树。
因为从后序数组尾部往前读,根节点后面先遇到的是右子树节点。
顺序写反,代码可能不报错,但构建出来的树是错的。
十一、二叉树前序遍历实现(LeetCode 144)
题目链接:144. 二叉树的前序遍历
核心:前序遍历的访问顺序是根、左、右。
1. 递归写法
前序遍历是:
text
根 -> 左 -> 右
所以当前节点只要不为空,就先把当前节点加入结果,再去处理左子树和右子树。
java
import java.util.LinkedList;
import java.util.List;
public List<Integer> preorderTraversal(TreeNode root) {
List<Integer> ret = new LinkedList<>();
if (root == null) {
return ret;
}
ret.add(root.val);
ret.addAll(preorderTraversal(root.left));
ret.addAll(preorderTraversal(root.right));
return ret;
}
2. 非递归写法
递归里,系统调用栈会帮我们保存回退路线。
非递归就需要自己使用栈。
前序要求根、左、右,但栈是后进先出。
所以想让左孩子先被处理,就要先压右孩子,再压左孩子。
java
import java.util.LinkedList;
import java.util.List;
import java.util.Stack;
public List<Integer> preorderTraversalNor(TreeNode root) {
List<Integer> ret = new LinkedList<>();
if (root == null) {
return ret;
}
Stack<TreeNode> stack = new Stack<>();
stack.push(root);
while (!stack.isEmpty()) {
TreeNode cur = stack.pop();
ret.add(cur.val);
if (cur.right != null) {
stack.push(cur.right);
}
if (cur.left != null) {
stack.push(cur.left);
}
}
return ret;
}
3. 容易错点
非递归前序里,压栈顺序和访问顺序要反着想。
想要左先访问,就要让左后入栈。
十二、二叉树中序遍历实现(LeetCode 94)
题目链接:94. 二叉树的中序遍历
核心:中序遍历的访问顺序是左、根、右。
1. 递归写法
中序遍历是:
text
左 -> 根 -> 右
所以当前节点不能一开始就访问。
必须先处理左子树,左子树结束以后再访问当前节点,最后处理右子树。
java
import java.util.LinkedList;
import java.util.List;
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> ret = new LinkedList<>();
if (root == null) {
return ret;
}
ret.addAll(inorderTraversal(root.left));
ret.add(root.val);
ret.addAll(inorderTraversal(root.right));
return ret;
}
2. 非递归写法
中序非递归比前序绕一点。
因为遇到节点时不能马上访问,它的左子树还没处理。
正确过程是:
- 一路向左走。
- 沿途节点全部压栈。
- 左边走到底以后,从栈中弹出节点。
- 弹出的节点此时可以访问。
- 访问完后转向右子树。
java
import java.util.LinkedList;
import java.util.List;
import java.util.Stack;
public List<Integer> inorderTraversalNor(TreeNode root) {
List<Integer> ret = new LinkedList<>();
Stack<TreeNode> stack = new Stack<>();
TreeNode cur = root;
while (cur != null || !stack.isEmpty()) {
while (cur != null) {
stack.push(cur);
cur = cur.left;
}
TreeNode top = stack.pop();
ret.add(top.val);
cur = top.right;
}
return ret;
}
3. 容易错点
节点入栈不等于访问节点。
中序遍历中,只有节点从栈里弹出时,才说明它的左子树已经处理完,可以加入结果。
十三、二叉树后序遍历实现(LeetCode 145)
题目链接:145. 二叉树的后序遍历
核心:后序遍历的访问顺序是左、右、根。
1. 递归写法
后序遍历是:
text
左 -> 右 -> 根
所以当前节点必须最后访问。
递归写法中,系统调用栈会自然帮我们等左子树和右子树都处理完,再回到当前节点。
java
import java.util.LinkedList;
import java.util.List;
public List<Integer> postorderTraversal(TreeNode root) {
List<Integer> ret = new LinkedList<>();
if (root == null) {
return ret;
}
ret.addAll(postorderTraversal(root.left));
ret.addAll(postorderTraversal(root.right));
ret.add(root.val);
return ret;
}
2. 非递归写法
后序非递归是三种非递归遍历里最容易写错的。
原因是当前节点不能在第一次遇到时访问。
它必须等左子树和右子树都处理完以后,才能加入结果。
递归中,系统调用栈会自动帮我们知道右子树是否已经处理完成。
非递归中,需要自己记录这个状态。
这里用 prev 表示上一个已经访问过的节点。
当前过程是:
-
先一路向左走,并把沿途节点压栈。
-
左边走到底后,查看栈顶节点
top。- 注意是
peek,不是直接pop。
- 注意是
-
判断
top能不能访问:top.right == null:没有右子树,可以访问。top.right == prev:右子树刚刚访问过,也可以访问。
-
如果可以访问:
- 把
top.val加入结果。 - 更新
prev = top。 - 弹出栈顶节点。
- 把
-
如果不能访问:
- 说明右子树还没处理。
- 让
cur = top.right,进入右子树。
java
import java.util.LinkedList;
import java.util.List;
import java.util.Stack;
public List<Integer> postorderTraversalNor(TreeNode root) {
List<Integer> ret = new LinkedList<>();
Stack<TreeNode> stack = new Stack<>();
TreeNode cur = root;
TreeNode prev = null;
while (cur != null || !stack.isEmpty()) {
while (cur != null) {
stack.push(cur);
cur = cur.left;
}
TreeNode top = stack.peek();
if (top.right == null || top.right == prev) {
ret.add(top.val);
prev = top;
stack.pop();
} else {
cur = top.right;
}
}
return ret;
}
3. 容易错点
最关键的判断是:
java
top.right == null || top.right == prev
它表示当前节点的右子树已经不需要再处理。
另外不能一开始就 pop,因为右子树没处理完时,当前节点还需要留在栈里等回退。
十四、二叉树的层序遍历(LeetCode 102)
题目链接:102. 二叉树的层序遍历
核心:队列负责从上到下访问,
count负责隔开每一层。
1. 这题怎么想
层序遍历要求从上到下、从左到右访问节点。
队列天然适合这个顺序。
但是这道题要求按层返回结果,所以还要知道当前层有多少个节点。
每一层开始之前,先记录:
java
int count = queue.size();
此刻队列中的节点,刚好就是当前层的所有节点。
接下来只处理 count 个节点。
每取出一个节点,就把它加入当前层列表,同时把它的左右孩子放入队列。
这些新加入的孩子属于下一层。
因为当前层只处理固定数量的节点,所以不会和下一层混在一起。
2. 代码实现
java
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
public List<List<Integer>> levelOrder(TreeNode root) {
List<List<Integer>> ret = new LinkedList<>();
if (root == null) {
return ret;
}
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
while (!queue.isEmpty()) {
int count = queue.size();
List<Integer> list = new LinkedList<>();
while (count != 0) {
TreeNode cur = queue.poll();
count--;
list.add(cur.val);
if (cur.left != null) {
queue.offer(cur.left);
}
if (cur.right != null) {
queue.offer(cur.right);
}
}
ret.add(list);
}
return ret;
}
3. 容易错点
queue.size() 必须在每一层开始之前记录。
如果处理过程中一直用队列当前大小,下一层节点会不断入队,当前层和下一层就容易混在一起。
十五、根据二叉树创建字符串(LeetCode 606)
题目链接:606. 根据二叉树创建字符串
核心:前序遍历写节点值,同时用括号保留必要的左右结构。
1. 这题怎么想
这道题本质上是前序遍历。
因为字符串要先写当前节点,再写左子树,最后写右子树。
麻烦的地方不在遍历,而在括号什么时候可以省略。
当前站在节点 root 上:
-
root == null- 当前没有节点可以写。
- 直接返回。
-
root != null- 先写
root.val。
- 先写
-
左子树存在
- 用一对括号包住左子树结果。
-
左子树不存在,右子树也不存在
- 当前是叶子节点。
- 不需要继续写括号。
-
左子树不存在,但右子树存在
- 必须写
()。 - 这个空括号表示左子树为空。
- 必须写
-
右子树存在
- 用一对括号包住右子树结果。
为什么左子树为空但右子树存在时必须补 ()?
看这个结构:
text
1
\
2
如果写成:
text
1(2)
那就无法区分 2 是左孩子还是右孩子。
所以必须写成:
text
1()(2)
2. 代码实现
java
public String tree2str(TreeNode root) {
StringBuilder sb = new StringBuilder();
build(root, sb);
return sb.toString();
}
private void build(TreeNode root, StringBuilder sb) {
if (root == null) {
return;
}
sb.append(root.val);
if (root.left != null) {
sb.append("(");
build(root.left, sb);
sb.append(")");
} else {
if (root.right == null) {
return;
} else {
sb.append("()");
}
}
if (root.right != null) {
sb.append("(");
build(root.right, sb);
sb.append(")");
}
}
3. 容易错点
唯一特别需要注意的就是:
text
没有左孩子,但有右孩子时,必须补空括号。
其他空括号能省略就省略,但这个位置不能省。
十六、二叉搜索树与双向链表(NowCoder BM30)
题目链接:BM30 二叉搜索树与双向链表
核心:利用二叉搜索树中序遍历有序的性质,把访问顺序变成链表顺序。
1. 这题怎么想
二叉搜索树有一个非常重要的性质:
text
中序遍历会按照从小到大的顺序访问每个节点。
所以如果在中序遍历过程中,把当前节点和上一个访问过的节点连接起来,就可以得到一条有序双向链表。
这里最关键的变量是 prev。
prev 表示上一个刚刚访问过的节点。
当前中序遍历来到节点 root:
-
先处理左子树
- 左子树中的节点都比当前节点小。
- 它们应该排在链表前面。
-
左子树处理完后,连接当前节点
- 此时
prev就是当前节点前面的那个节点。
- 此时
-
建立双向关系
javaroot.left = prev;如果
prev != null,再执行:javaprev.right = root; -
更新
prevjavaprev = root;这样下一个访问到的节点,就会接在当前节点后面。
-
最后处理右子树
- 右子树中的节点比当前节点大。
- 应该继续接到链表后面。
整棵树连接完后,原来的根节点不一定是链表头。
链表头应该是最小的节点,所以最后还要从原根节点一路向左,找到头节点返回。
2. 代码实现
java
public class Solution {
private TreeNode prev;
public TreeNode Convert(TreeNode pRootOfTree) {
if (pRootOfTree == null) {
return null;
}
inorderConnect(pRootOfTree);
TreeNode head = pRootOfTree;
while (head.left != null) {
head = head.left;
}
return head;
}
private void inorderConnect(TreeNode root) {
if (root == null) {
return;
}
inorderConnect(root.left);
root.left = prev;
if (prev != null) {
prev.right = root;
}
prev = root;
inorderConnect(root.right);
}
}
3. 容易错点
prev 不能定义在递归函数内部。
因为每一层递归都要共享"上一个访问过的节点"这个状态。
另外,这段代码转换出来的是普通双向链表,不是循环双向链表。
如果题目要求循环双向链表,还需要额外把头节点和尾节点连起来。
end*