LeetCode 05 - 二叉树
二叉树迭代遍历
题目链接:
144. 二叉树的前序遍历
递归和迭代两种方法实质上是一样的,只是递归隐式地维护着一个栈,而迭代则显式地维护一个栈。写递归的时候注意,只需要关注每次需要完成的具体任务,不要纠结递归的细节,把对孩子结点的递归调用当做一个黑箱即可,确信它会完成需要的任务。
写递归函数的三要素:
- 确定函数参数和返回值:哪些参数是递归过程中需要处理的,就将其作为递归函数的参数;明确函数的返回值。
- 确定终止条件:如果终止条件不对可能造成 Stack Overflow。
- 确定当前层递归的任务:将进一步的递归任务当做已知信息,只关注当前层应该做什么。
前序遍历
迭代写法:创建一个栈,首先将根结点入栈,每访问一个结点,就依次将它的右孩子、左孩子加入栈。
public List<Integer> preorderTraverse(TreeNode root) {
List<Integer> result = new ArrayList<>();
if (root == null) return result;
Deque<TreeNode> stack = new LinkedList<>();
stack.push(root);
while (!stack.isEmpty()) {
TreeNode cur = stack.pop();
result.add(cur.val);
if (cur.right != null) stack.push(cur.right);
if (cur.left != null) stack.push(cur.left);
}
return result;
}
中序遍历
// 迭代实现
List<Integer> inorder(TreeNode root) {
List<Integer> result = new ArrayList<>();
// 明确维护一个栈
Deque<TreeNode> stack = new LinkedList<>();
while(root != null || !stack.isEmpty()) {
// 走到最左边的结点,同时将途径的结点都入栈
while(root != null) {
stack.push(root);
root = root.left;
}
// 此时栈顶为最左的结点
root = stack.pop();
result.add(root);
// 转到右孩子(如果右孩子为空则会继续弹出栈顶结点)
root = root.right;
}
return result;
}
后续遍历
List<Integer> postorderTraverse(TreeNode root) {
List<Integer> result = new ArrayList<>();
if (root == null) return result;
Deque<TreeNode> stack = new LinkedList<>();
TreeNode prev = null; // 记录上一次访问的结点,用来判断右子树是否已经访问过
while (root != null || !stack.isEmpty()) {
// 走到最左子结点
while (p != null) {
stack.push(p);
p = p.left;
}
root = stack.pop();
if (root.right != null && root.right != prev) { // 右子树还没有访问
stack.push(root); // 重新入栈
root = root.right;
} else { // 右子树已经访问过,则直接访问当前根结点
result.add(root.val);
prev = root;
root = null; // 防止重复往左子树走
}
}
return result;
}
226. 翻转二叉树
二叉树问题的关键点就是 如何将题目要求转化成每个结点应该做的事,这里每个结点要做的就是交换自己的左右孩子:
- 对于当前
root
结点,交换它的左右孩子left / right
; - 对
left / right
做同样的操作。
TreeNode invertTree(TreeNode root) {
if (root == null) return null; // base case
// 处理当前 root 结点
TreeNode temp = root.left;
root.left = root.right;
root.right = temp;
// 递归处理左右孩子
invertTree(root.left);
invertTree(root.right);
return root;
}
这里是先处理当前节点,再处理左右孩子,是 前序遍历顺序;还可以用 后序遍历 方式来做。
101. 对称二叉树
给定二叉树,判断它是否对称。
分析:可以用 递归 和 迭代 两种方法来做。
递归:如果一个树的左子树与右子树镜像对称,那么称这个树是对称的。而两棵树镜像对称,需要满足两个条件:
- 两树的根结点值相同;
- 每棵树的左子树都和另一棵树的右子树对称。(递归关系)
递归思路:
-
怎么判断一棵树是不是对称?
- 如果根结点为空,对称;
- 否则,它的左子树和右子树相互对称,则该树对称。
-
怎么判断左树和右树是否相互对称?
- 左树的左孩子和右树的右孩子对称,且左树的右孩子与右树的左孩子对称,则左右子树相互对称。
-
定义递归函数 A 用来判断两树是否相互对称,
def 函数A(左树,右树): if 左树.value == 右树.value && 函数A(左树.left, 右树.right) && 函数A(左树.right, 右树.left): return True
boolean isSymmetric(TreeNode root) {
check(root.left, root.right);
}
boolean check(TreeNode left, TreeNode right) {
// 递归出口
if(left == null && right == null) return true;
if(left == null || right == null) return false;
// 对称条件(两个条件)
return left.val == q.val && check(left.left, right.right)
&& check(left.right, right.left);
}
迭代:引入一个队列,是把将递归改写成迭代的常用方式,过程类似于层序遍历。首先将根结点的左右子结点入队,然后在后面的每次迭代中出队两个结点,比较它们的值是否相同,相同则将这两个结点的左右孩子以相反的顺序入队,不相同则说明不对称。
每次出队的两个结点都是在树中处于对称位置的。
boolean check(TreeNode left, TreeNode right) {
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(left);
queue.offer(right);
while(!queue.isEmpty()) {
// 每次出队处于对称位置的两个结点
TreeNode p = queue.poll();
TreeNode q = queue.poll();
// 边界情况
if(p == null && q == null) continue;
// 不对称的条件
if(p == null || q == null || p.val != q.val) return false;
// p, q 对称则继续向后迭代
queue.offer(p.left);
queue.offer(q.right);
queue.offer(p.right);
queue.offer(q.left);
}
return true;
}
111. 二叉树的最小深度
最小深度是从根节点到最近叶子节点的最短路径上的节点数量。
输入:
1
/ \
2 3
/ \
4 5
最小深度:2 : 1 → 2
递归:
要素的确定:
- 返回值:最小深度 —— 一个整数,所以函数签名可以为
int minDepth(TreeNode root)
。 - 终止条件:
if (root == null) return 0;
- 当前层递归的任务:计算当前子树的最小深度:
- 如果左子树为空,最小深度为
1 + 右子树的最小深度
; - 如果右子树为空,最小深度为
1 + 左子树的最小深度
; - 如果都不为空,最小深度为
1 + min(左子树最小深度, 右子树最小深度)
。
- 如果左子树为空,最小深度为
public int minDepth(TreeNode root) {
if (root == null) return 0;
if (root.left == null && root.right == null) return 1;
else if (root.left == null) return 1 + minDepth(root.right);
else if (root.right == null) return 1 + minDepth(root.left);
else return 1 + Math.min(minDepth(root.left), minDepth(root.right));
}
迭代(使用队列)
public int minDepth(TreeNode root) {
if (root == null) return 0;
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
int depth = 0;
while (!queue.isEmpty()) {
int size = queue.size();
depth++;
for (int i = 0; i < size; i++) {
TreeNode cur = queue.poll();
// 遇到第一个叶子结点,即可返回
if (cur.left == null && cur.right == null) return depth;
if (cur.left != null) queue.offer(cur.left);
if (cur.right != null) queue.offer(cur.right);
}
}
return depth;
}
110. 平衡二叉树
平衡二叉树:一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过 1。
方法一:递归(自顶向下)
定义函数 getHeight(node)
,用于计算树的高度,据此可以判断二叉树是否平衡。
自顶向下方式:对于遍历到的当前结点,先计算其左右子树的高度之差是否不大于 1,然后再递归判断左右子树是否为平衡二叉树。
public boolean isBalanced(TreeNode root) {
// 结束条件
if (root == null) return true;
// 当前层任务
if (Math.abs(getHeight(root.left) - getHeight(root.right)) > 1)
return false;
// 进一步递归
return isBalanced(root.left) && isBalanced(root.right);
}
public int getHeight(TreeNode root) {
if (root == null) return 0;
return 1 + Math.max(getHeight(root.left), getHeight(root.right));
}
方法二:递归(自底向上)
自顶向下的递归对每个结点会重复调用 getHeight
函数,自底向上方式可以避免这些重复。
所谓自底向上,就是先从最底层的叶子结点开始实际调用 getHeight
函数。对于当前遍历到的结点,先递归判断左右子树是否都平衡,然后再判断当前数是否平衡。在遍历过程中只要发现一个不平衡的子树,就立即返回这个不平衡的结果。实现方式为,在 getHeight
函数中,如果左右子树不平衡,就将当前树的结果返回为 -1
,如果平衡就正常返回当前树的高度。
public boolean isBalanced(TreeNode root) {
return getHeight(root) >= 0;
}
public 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; // 不平衡则返回 -1
return 1 + Math.max(leftHeight, rightHeight);
}
513. 找树左下角的值
找出二叉树最下面那层最左边的结点的值。
方法一:DFS
使用 maxDepth
、targetValue
分别记录最左最深结点的深度和结点值。DFS 函数将 curDepth
作为参数,表示当前遍历到的结点的深度,如果 curDepth
大于 maxDepth
,则更新 maxDepth
,并将当前结点作为目标结点。
int maxDepth;
int targetValue;
public int findBottomLeftValue(TreeNode root) {
if (root == null) return null;
maxDepth = 0;
dfs(root, 0);
return targetValue;
}
public void dfs(TreeNode root, int curDepth) {
// 终止条件
if (root == null) return;
// 当前任务
curDepth++;
if (curDepth > maxDepth) {
maxDepth = curDepth;
targetValue = root.val;
}
// 访问子结点
dfs(root.left, curDepth);
dfs(root.right, curDepth);
}
方法二:BFS
在层序遍历中向队列中放入结点时,先放右孩子,再放左孩子,这样最后访问到的结点就是答案。
public int findBottomLeftValue(TreeNode root) {
if (root == null) return null;
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
int prev = -1;
while (!queue.isEmpty()) {
TreeNode cur = queue.poll();
prev = cur.val;
if (cur.right != null) {
queue.offer(cur.right);
}
if (cur.left != null) {
queue.offer(cur.left);
}
}
return prev;
}
257. 二叉树的所有路径
给你一个二叉树的根节点 root
,按 任意顺序 ,返回所有从根节点到叶子节点的路径。
方法:DFS
DFS 三要素:
- 结束条件:到达叶结点,将当前路径加入结果集;
- 当前层任务:将当前结点加入路径;
- 访问子结点:对左右孩子调用 DFS 函数。
List<String> result;
public List<String> binaryTreePaths(TreeNode root) {
result = new ArrayList<>();
if (root == null) return result;
StringBuilder track = new StringBuilder();
dfs(root, track);
return result;
}
void dfs(TreeNode root, StringBuilder track) {
// 终止条件
if (root.left == null && root.right == null) {
track.append(root.val);
result.add(track.toString());
return;
}
// 当前任务
track.append(root.val);
track.append("->");
// 访问子结点
if (root.left != null) dfs(root.left, new StringBuilder(track));
if (root.right != null) dfs(root.right, new StringBuilder(track));
}
404. 左叶子之和
方法一:递归
三要素:
- 返回值:当前树中左叶子之和。
- 结束条件:当前树为空,则返回 0。
- 当前层任务:如果当前树根结点的左孩子是叶结点,则记录其值,累加到左子树和右子树的递归调用结果上。
int sumOfLeftLeaves(TreeNode root) {
// 结束条件
if (root == null) return 0;
// 当前层任务
TreeNode left = root.left;
int curValue = 0;
if (left != null && left.left == null && left.right == null)
curValue = left.val;
// 进一步递归
int leftResult = sumOfLeftLeaves(root.left);
int rightResult = sumOfLeftLeaves(root.right);
return curValue + leftResult + rightResult;
}
DFS 写法:
int sumOfLeftLeaves(TreeNode root) {
if (root == null) return 0;
return dfs(root);
}
int dfs(TreeNode root) {
int sum = 0;
TreeNode left = root.left;
if (left != null) {
if (left.left == null && left.right == null)
sum += left.val;
else
sum += dfs(left);
}
TreeNode right = root.right;
if (right != null && (right.left != null || right.right != null)) {
sum += dfs(right);
}
return sum;
}
112. 路径总和
给你二叉树的根节点 root
和一个表示目标和的整数 sum
。判断该树中是否存在 根节点到叶子节点 的路径,这条路径上所有节点值相加等于目标和 sum
。
方法:递归
- 函数的功能为:查看是否存在从当前结点到叶结点的路径,其总和为
sum
。 - 假定从根节点到当前节点的值之和为
val
,我们可以将这个大问题转化为一个小问题:是否存在从当前节点的子节点到叶子的路径,满足其路径和为sum - val
。 - 不难发现这满足递归的性质,若当前节点就是叶子节点,那么我们直接判断
sum
是否等于val
即可,否则我们只需要递归地询问它的子节点是否能满足条件即可。
boolean hasPathSum(TreeNode root, int sum) {
if (root == null) return false;
if (root.left == null && root.right == null) {
return root.val == sum;
}
return hasPathSum(root.left, sum - root.val)
|| hasPathSum(root.right, sum - root.val);
}
654. 最大二叉树
给定一个不含重复元素的整数数组 nums
。一个以此数组直接递归构建的 最大二叉树 定义如下:
- 二叉树的根是数组
nums
中的最大元素。 - 左子树是通过数组中 最大值左边部分 递归构造出的最大二叉树。
- 右子树是通过数组中 最大值右边部分 递归构造出的最大二叉树。
返回 nums
构建的最大二叉树。
先找到最大值所在索引,构造根结点 root,然后对左边数组和右边数组递归调用,作为 root 的左右子树。
public TreeNode constructMaximumBinaryTree(int[] nums) {
// base case
if (nums.length == 0) return null;
int length = nums.length;
int maxIndex = maxIdx(nums);
// 构造根结点
TreeNode root = new TreeNode(nums[maxIndex]);
// 递归构造左右孩子
int[] leftPart = new int[maxIndex];
int[] rightPart = new int[nums.length - maxIndex - 1];
for (int i = 0; i < maxIndex; i++)
leftPart[i] = nums[i];
for (int i = maxIndex+1, j = 0; i < nums.length; i++, j++)
rightPart[j] = nums[i];
root.left = constructMaximumBinaryTree(leftPart);
root.right = constructMaximumBinaryTree(rightPart);
return root;
}
int maxIdx(int[] nums) {
int maxIndex = 0;
for (int i = 1; i < nums.length; i++)
if (nums[i] > nums[maxIndex])
maxIndex = i;
return maxIndex;
}
105. 从前序与中序遍历序列构造二叉树
分析:preorder
和 inorder
中的元素分布有下面的特点:
每次从 前序遍历 能得到的信息是根结点,然后利用发现的根结点在 中序遍历 序列中能够找出 左右子树 包含的结点。再递归地从前序遍历中找到子树的根结点,循环往复。
为了在每次递归中确定当前子树的两个序列,需要知道这棵树在前序序列中的开始和结束位置,以及在中序序列中的开始和结束位置。
TreeNode buildTree(int[] preorder, int[] inorder) {
return build(preorder, 0, preorder.length-1,
inorder, 0, inorder.length-1);
}
TreeNode build(int[] preorder, int preStart, int preEnd,
int[] inorder, int inStart, int inEnd) {
// base case
if (preStart > preEnd) return null;
// 构造根结点
int rootValue = preorder[preStart];
TreeNode root = new TreeNode(rootValue);
// 在中序遍历序列中找到根结点
int rootIndex = 0;
for (int i = inStart; i <= inEnd; i++) {
if (inorder[i] == rootValue) {
rootIndex = i;
break;
}
}
// 构造左右子树(关键是在两个序列中找出左右子树的索引范围)
int leftSize = rootIndex - inStart;
root.left = build(preorder, preStart + 1, preStart + leftSize,
inorder, inStart, rootIndex - 1);
root.right = build(preorder, preStart + leftSize + 1, preEnd,
inorder, rootIndex + 1, inEnd);
// 返回当前子树的根结点
return root;
}
652. 寻找重复的子树
给定一棵二叉树,返回所有重复的子树。对于同一类的重复子树,你只需要返回其中任意一棵的根结点即可。两棵树重复是指它们具有相同的结构以及相同的结点值。
需要明确两个问题:
- 以自己为根结点的这棵子树长什么样?
- 以其他结点为根结点的子树长什么样?
对于第一个问题,怎么描述一棵子树?可以通过拼接字符串的方式把 二叉树序列化,例如使用子树的后序遍历序列标识该子树,用特殊符号,例如 #
表示空节点。
对于第二个问题,这里使用 HashMap
记录每种子树的序列及其出现次数,遍历每个结点时,查看当前序列在其中出现的次数,如果是 1,则将该序列加入结果集,随后该序列的次数会加一。这样保证了重复子树只会被加入一次。
HashMap<String, Integer> memo = new HashMap<>(); // 所有子树及其次数
LinkedList<TreeNode> result = new LinkedList<>(); // 重复子树的根结点
List<TreeNode> findDuplicateSubtrees(TreeNode root) {
traverse(root);
return result;
}
String traverse(TreeNode root) {
// 求出表示序列(后序遍历顺序)
if (root == null) return "#";
String left = traverse(root.left);
String right = traverse(root.right);
String subTree = left + "," + right + "," + root.val;
// 检查是否重复
int count = memo.getOrDefault(subTree, 0);
if (count == 1) result.add(subTree); // 发现重复只会加入一次
memo.put(subTree, count + 1); // 更新 subTree 的出现次数
return subTree;
}
297. 二叉树的序列化与反序列化
可以用前序遍历、后序遍历或层序遍历的方法来序列化和执行相应的反序列化,但不能用中序遍历,因为中序遍历不知道根结点位置。
前序遍历方法
String SEP = ",";
String NULL = "#";
// 序列化
String serialize(TreeNode root) {
StringBuilder builder = new StirngBuilder();
serialize(root, builder);
return builder.toString();
}
void serialize(TreeNode root, StringBuilder builder) {
// 空节点
if (root == null) {
builder.append(NULL).append(SEP);
return;
}
// 处理根结点
builder.append(root.val).append(SEP);
serialize(root.left, builder);
serialize(root.right, builder);
}
// 反序列化
TreeNode deserialize(String data) {
// 将序列转化成列表
LinkedList<String> nodes = new LinkedList<>();
for (String s : data.split(",")) nodes.addLast(s);
return deserializeAux(nodes);
}
TreeNode deserializeAux(LinkedList<String> nodes) {
if (nodes.isEmpty()) return null;
// 构造根结点
String first = nodes.removeFirst(); // 取出第一个结点
if (first.equals(NULL)) return null;
TreeNode root = new TreeNode(Integer.parseInt(first));
// 构造左右孩子
root.left = deserializeAux(nodes);
root.right = deserializeAux(nodes);
return root;
}
222. 完全二叉树的节点个数
如果是满二叉树,结点总数可以通过公式直接计算:. 利用这一点,在计算每个子树的结点个数时,先判断是否为满二叉树,如果是则直接计算,否则按照普通二叉树的方法递归计算。
int countNodes(TreeNode root) {
if (root == null) return 0;
TreeNode l = root, right = root;
int mostLeftHeight = 0, mostRightHeight = 0;
while (l != null) {
l = l.left;
mostLeftHeight++;
}
while (r != null) {
r = r.right;
mostRightHeight++;
}
if (mostLeftHeight == mostRightHeight)
return (int) Math.pow(2, mostLeftHeight) - 1;
return 1 + countNodes(root.left) + countNodes(root.right);
}
230. 二叉搜索树中第K小的元素
中序遍历,访问到的第 k 个元素就是要求的元素。
int findKthSmallest(TreeNode root, int k) {
traverse(root, k);
return result;
}
int counter = 1, result;
void traverse(TreeNode root, int k) {
if (root == null) return;
// 左
traverse(root.left, k);
// 根
if (counter == k) result = root.val;
counter++;
// 右
traverse(root.right, k);
}
上面是用递归方法写的,下面是用栈迭代实现的:
int findKthSmallest(TreeNode root, int k) {
Deque<TreeNode> stack = new LinkedList<>();
while (!stack.isEmpty() || root != null) {
while(root != null) {
stack.push(root);
root = root.left;
}
root = stack.pop();
--k;
if (k == 0) break;
root = root.right;
}
return root.val;
}
617. 合并二叉树
给定两棵二叉树,合并规则是:对应相同位置结点都存在,则值相加;否则不为空的结点作为新二叉树的结点。
方法一:递归(DFS)
TreeNode mergeTrees(TreeNode root1, TreeNode root2) {
// 递归出口
if(root1 == null) return root2;
if(root2 == null) return root1;
// 处理根结点
TreeNode root = new TreeNode(root1.val + root2.val);
// 递归合并左右孩子
root.left = mergeTrees(root1.left, root2.left);
root.right = mergeTrees(root1.right, root2.right);
return root;
}
方法二:迭代(BFS)
使用三个队列分别存储合并后的树结点(queue
)、原始的两个树的结点 (queue1, queue2
)。每一轮迭代中同时从 queue1, queue2
中出队两个元素,查看这两个结点的左右孩子是否为空,根据不同情况创建新节点后放入 queue
。
TreeNode mergeTrees(TreeNode root1, TreeNode root2) {
if(root1 == null) return root2;
if(root2 == null) return root1;
Queue<TreeNode> queue = new LinkedList<>();
Queue<TreeNode> queue1 = new LinkedList<>();
Queue<TreeNode> queue2 = new LinkedList<>();
queue1.offer(root1);
queue2.offer(root2);
queue.offer(new TreeNode(root1.val + root2.val));
// 开始迭代
while(!queue1.isEmpty() && !queue2.isEmpty()) {
TreeNode node = queue.poll(), node1 = queue1.poll(),
node2 = queue2.poll();
TreeNode left1 = node1.left, right1 = node1.right;
TreeNode left2 = node2.left, right2 = node2.right;
// 检查左孩子
if(left1 != null || left2 != null) {
// 都不为空
if(left1 != null && left2 != null) {
TreeNode left = new TreeNode(left1.val+left2.val);
node.left = left;
queue1.offer(left1);
queue2.offer(left2);
queue.offer(left);
}
// 一个为空则直接将另一个作为合并后的结点,后续迭代停止
else if(left1 != null) node.left = left1;
else if(left2 != null) node.left = left2;
}
// 以同样方式检查右孩子
if(right1 != null || right2 != null) {
// ...
}
}
return root;
}
572. 另一棵树的子树
给定两个二叉树 s,t
,判断第二个是否是第一个的子树。
方法:DFS 暴力匹配
先判断 subtree
是否为根结点的子树,如果不是则判断是否为 root.left, root.right
的子树。
boolean isSubtree(TreeNode s, TreeNode t) {
if (s == null) return false;
if (check(s, t)) return true;
return isSubtree(s.left, t) || isSubtree(s.right, t);
}
// 判断两棵树是否相同
boolean check(TreeNode s, TreeNode t) {
if(s == null && t == null)
return true;
else if (s == null || t == null || s.val != t.val)
return false;
return check(s.left, t.left) && check(s.right, t.right);
}
543. 二叉树的直径
直径长度是任意两个结点路径长度中的最大值。这条路径可能穿过也可能不穿过根结点。
方法:递归
对于一个结点 root
,以它为根的子树的直径为 root.left
和 root.right
的深度之和。在递归计算每个结点的直径时,不断更新直径的最大值,最后将其返回即可。
int maxDiameter = 0;
int diameterOfBinaryTree(TreeNode root) {
depth(root);
return maxDiameter;
}
int depth(TreeNode root) {
// 递归结束条件
if(root == null) return 0;
// 根据左右子树的深度确定当前子树的直径
int leftDepth = depth(root.left),
rightDepth = depth(root.right);
int diameter = leftDepth + rightDepth;
// 更新最大直径
maxDiameter = Math.max(maxDiameter, diameter);
// 返回当前子树的深度
return Math.max(leftDepth, rightDepth) + 1;
}
116. 填充每个节点的下一个右侧节点指针
给定二叉树,每个结点的 next
指针应该指向同一层的右边一个结点,但初始状态下都为空,请填充这些指针。
方法一:递归(适用于完全二叉树)
递归任务定义为:连接两个相邻结点(node1.next=node2
),同时连接每个结点的左右孩子结点,以及第一个结点的右孩子和第二个结点的左孩子。
Node connect(Node root){
if(root == null) return null;
connectTwoNodes(root.left, root.right);
return root;
}
void connectTwoNodes(Node node1, Node node2) {
if(node1 == null || node2 == null) return;
node1.next = node2;
connectTwoNodes(node1.left, node2.left);
connectTwoNodes(node2.left, node2.right);
connectTwoNodes(node1.right, node2.left);
}
方法二:迭代(BFS)
每次处理一层结点。
Node connect(Node root) {
if(root == null) return null;
Queue<Node> queue = new LinkedList<>();
queue.offer(root);
while(!queue.isEmpty()) {
// 每轮迭代处理一层
int size = queue.size();
for(int i = 0; i < size; i++) {
Node node = queue.poll();
// 只填充前 size-1 个结点
if(i < size-1) node.next = queue.peek();
// 下一层结点入队
if(node.left != null) queue.offer(node.left);
if(node.right != null) queue.offer(node.right);
}
}
return root;
}
方法三:使用当前层已经填充的 next
指针建立下一层的 next
关系
这个方法适用于完全二叉树。
两个相邻结点如果互为兄弟,容易建立 next
关系: node.left.next = node.right
;如果在不同的父结点之下,基于当前层已经填充的 next
指针,可以这样建立下一层的 next
关系: node.right.next = node.next.left
。
也就是说,遍历到第 N 层时,第 N 层已经在上一轮迭代中填充好了 next
指针,当前的目的是建立下一层,即第 N+1 层的 next
关系。
每一层的连接需要借助一个指针来串联,这个指针首先指向最左边结点,然后不断右移,连接完成时,它又指向下一层的最左边结点。
Node connect(Node root) {
if(root == null) return null;
Node leftMostNode = root; // 用它来表示每一层迭代开始时的指针指向
// 注意循环条件,迭代到倒数第二层即可
while(leftMostNode.left != null) {
Node p = leftMostNode;
// 注意这里的 p != null 和下面的 p.next != null
while(p != null) {
p.left.next = p.right; // 第一种情况
if(p.next != null) // 第二种情况
p.right.next = p.next.left;
p = p.next;
}
// 准备下一层的迭代
leftMostNode = leftMostNode.left;
}
return root;
}
117. 填充每个节点的下一个右侧节点指针 II
方法一:层序遍历
Node connect(Node root) {
if(root == null) return null;
Queue<Node> queue = new LinkedList<>();
queue.offer(root);
while(!queue.isEmpty()) {
int size = queue.size();
Node last = null; // 引线的针
for(int i = 0; i < size; i++) {
Node cur = queue.poll();
// 连接
if(i == 0) last = cur;
else {
last.next = cur;
last = last.next;
}
// 下一层入队
if(cur.left != null) queue.offer(cur.left);
if(cur.right != null) queue.offer(cur.right);
}
}
return root;
}
方法二:借助上一层的连接创建当前层的连接
Node connect(Node root) {
if(root == null) return null;
Node start = root;
while(start != null) {
// 每一层开始时,last 和 nextStart 需要初始化
Node last = null; // 引线的针
Node nextStart = null; // 每一层的开头结点
for(Node p = start; p != null; p = p.next) {
// 连接 p.child 那一层
if(p.left != null) {
// 连接
if(last == null)
last = p.left;
else {
last.next = p.left;
last = last.next;
}
// 记录新的开始结点,以供下一层连接
if(nextStart == null)
nextStart = p.left;
}
if(p.right != null) {
if(last == null)
last = p.right;
else {
last.next = p.right;
last = last.next;
}
if(nextStart == null)
nextStart = p.right;
}
} // end of for p
start = nextStart;
}
return root;
}
98. 验证二叉搜索树
判断一棵树二叉树是否是二叉搜索树。
方法一:递归
递归函数 helper(root, low, high)
表示判断以 root
为根的子树中的所有结点是否都在 (low, high)
范围内。如果在该范围内,则递归判断左子树:helper(root.left, low, root.val)
,以及右子树:helper(root.right, root.val, high)
。
boolean isValidBST(TreeNode root) {
return helper(root, Long.MIN_VALUE, Long.MAX_VALUE);
}
boolean helper(TreeNode root, int low, int high) {
if(root == null) return true;
if(root.val <= low || root.val >= high) return false;
return helper(root.left, low, root.val)
&& helper(root.right, root.val, high);
}
方法二:中序遍历
递归版本
long pre = Long.MIN_VALUE;
boolean isValidBST(TreeNode root) {
return inorder(root);
}
boolean inorder(TreeNode root) {
if(root == null) return true;
// 左子树
boolean left = inorder(root.left);
// 根结点
if(pre >= root.val) return false;
pre = root.val;
// 右子树
boolean right = inorder(root.right);
return left && right;
}
栈版本
boolean isValidBST(TreeNode root) {
Deque<TreeNode> stack = new LinkedList<>();
double pre = Long.MIN_VALUE;
while(!stack.isEmpty() || root != null) {
// 走到最左边
while(root != null) {
stack.push(root);
root = root.left;
}
root = stack.pop();
// 访问根结点
if(root.val <= pre) return false;
pre = root.val;
// 转到右子树
root = root.right;
}
return true;
}
501. 二叉搜索树中的众数
找到出现频率最高的所有元素。
BST 的中序遍历序列是一个有序数组,所以相同的数字是相连的,那么:
- 用
pre
指针指向前一个访问的结点,初始时pre==null
。 - 使用
count
来记录当前遍历到的数字已经出现了几次; - 用
maxCount
记录遍历过的数字中最大的次数。 - 用一个
List
对象记录所有众数。
遍历到一个结点时:
- 如果
pre == null
,说明当前结点是第一个结点,将count
设为1
。 - 如果
pre.val != root.val
,说明当前结点第一次出现,将count
设为1
。 - 如果
pre.val == root.val
,说明当前结点值再次出现,count++
。
这时检查 count
和 maxCount
的大小关系:
- 如果
count == maxCount
,将当前结点值也加入结果列表。 - 如果
count > maxCount
,则更新maxCount
,同时将结果列表清空,然后将当前结点值加入结果列表,因为之前记录的值都不再是最大频次结点了。
TreeNode pre;
int count;
int maxCount;
List<Integer> list;
public int[] findMode(TreeNode root) {
pre = null;
count = 0;
maxCount = 0;
list = new ArrayList<>();
inorder(root);
int[] result = new int[list.size()];
int i = 0;
for (int value : list)
result[i++] = value;
return result;
}
public void inorder(TreeNode root) {
if (root == null) return;
inorder(root.left);
int rootVal = root.val;
if (pre == null || pre.val != rootVal) {
count = 1;
} else {
count++;
}
pre = root;
if (count > maxCount) {
list.clear();
list.add(rootVal);
maxCount = count;
}
else if (count == maxCount) {
list.add(rootVal);
}
inorder(root.right);
}
二叉树的最近公共祖先
方法一:递归
TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if(root == null) return null;
// 如果 p,q 有一个是根结点,则lca是根结点
if(root.val==p.val || root.val==q.val) return root;
// 如果 p,q 都在左子树,则到左子树递归查找
if(isExist(root.left, p) && isExist(root.left, q))
return lowestCommonAncestor(root.left, p, q);
// 如果 p,q 都在右子树,则到右子树递归查找
if(isExist(root.right, p) && isExist(root.right, q))
return lowestCommonAncestor(root.right, p, q);
// 如果 p,q 分别属于左右子树,则lca是根结点
return root;
}
boolean isExist(TreeNode root, TreeNode node) {
if(root == null) return false;
if(root.val == node.val) return true;
boolean left = isExist(root.left, node);
boolean right = isExist(root.right, node);
return left || right;
}
方法二:记录每个结点的父结点
算法过程如下:
- 从根结点开始遍历二叉树,用哈希表记录每个结点的父结点。
- 从
p
结点开始向上记录它的各级祖先节点 (记为visited
集合)。 - 再从
q
结点开始向上遍历各级祖先节点,如果这个祖先节点存在于visited
中,则说明这个结点就是答案。
Map<Integer, TreeNode> node_parent = new HashMap<>();
Set<Integer> visited = new HashSet<>();
// 记录各个结点的父结点
void dfs(TreeNode root) {
if(root.left != null) {
node_parent.put(root.left.val, root);
dfs(root.left);
}
if(root.right != null) {
node_parent.put(root.right.val, root);
dfs(root.right);
}
}
TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
dfs(root);
while(p != null) {
visited.add(p.val);
p = node_parent.get(p.val); // 向上溯源
}
while(q != null) {
if(visited.contains(q.val))
return q;
q = node_parent.get(q.val); // 溯源
}
return null;
}
450. 删除二叉搜索树中的节点
找到要删除的结点 root
后,分几种情况:
- 没有孩子结点,直接删除
root
,返回null
。 - 只有左孩子 / 右孩子,则将左孩子 / 右孩子作为新的子树,直接返回。
- 有左右孩子,可以将右子树的最小结点(中序遍历中
root
的后继结点)提到root
位置,然后递归地将这个结点从右子树中删除。
public TreeNode deleteNode(TreeNode root, int key) {
if (root == null) return null;
if (root.val > key) {
root.left = deleteNode(root.left, key);
return root;
}
if (root.val < key) {
root.right = deleteNode(root.right, key);
return root;
}
else {
if (root.left == null && root.right == null) return null;
if (root.left == null) return root.right;
if (root.right == null) return root.left;
TreeNode rightMin = root.right;
while (rightMin.left != null) rightMin = rightMin.left;
root.right = deleteNode(root.right, rightMin.val);
rightMin.right = root.right;
rightMin.left = root.left;
return rightMin;
}
}
669. 修剪二叉搜索树
给你二叉搜索树的根节点 root
,同时给定最小边界 low
和最大边界 high
。通过修剪二叉搜索树,使得所有节点的值在 [low, high]
中。修剪树不应该改变保留在树中的元素的相对结构 (即,如果没有被移除,原有的父代子代关系都应当保留)。返回修剪好的二叉搜索树的新的根节点。
方法:递归
要素确定:
- 返回值:删除不在范围内的所有结点后,返回根结点。
- 终止条件:
root==null
,直接返回null
。 - 当前任务:
- 如果
root.val < low
,说明root
及其左子树都需要删除,而其右子树需要递归修剪,并将修剪的结果返回,替代root
。 - 如果
root.val > high
,说明root
及其右子树都需要删除,而其左子树需要递归修剪,并将修剪的结果返回,替代root
。 - 否则,递归修剪其左右子树。
- 如果
public TreeNode trimBST(TreeNode root, int low, int high) {
if (root == null) return null;
if (root.val < low) {
root = trimBST(root.right, low, high);
} else if (root.val > high) {
root = trimBST(root.left, low, high);
} else {
root.left = trimBST(root.left, low, high);
root.right = trimBST(root.right, low, high);
}
return root;
}
二叉树展开为单链表
链表顺序为二叉树前序遍历的顺序。
方法一:前序遍历
void preorderTraverse(TreeNode root, List<TreeNode> list) {
if(root == null) return;
list.add(root);
preorderTraverse(root.left, list);
preorderTraverse(root.right, list);
}
void flatten(TreeNode root) {
List<TreeNode> list = new ArrayList<>();
preorderTraverse(root, list);
// 建立前序序列结点的连接
for(int i = 1; i < list.size(); i++) {
list.get(i-1).left = null;
list.get(i-1).right = list.get(i);
}
}
方法二:前序遍历和展开同时进行
方法一是将前序遍历和展开分成了两步,这里将其合而为一,但因为涉及到前序遍历的内部细节,不能使用递归方式来实现前序遍历,而要使用迭代方式:在每一轮迭代中,将 上一个访问的结点 prev
的右孩子设为当前节点 cur
,然后将 prev
设为 cur
,继续遍历。
void flatten(TreeNode root) {
if(root == null) return;
Deque<TreeNode> stack = new LinkedList<>();
stack.push(root);
TreeNode prev = null;
while(!stack.isEmpty()) {
TreeNode cur = stack.pop();
if(prev != null) {
prev.left = null;
prev.right = cur;
}
if(cur.right != null)
stack.push(cur.right);
if(cur.left != null)
stack.push(cur.left);
prev = cur;
}
}
方法三:递归
先递归处理左右子树,然后把左子树放到右子树位置,最后把右子树作为左子树的右孩子。
void flatten(TreeNode root) {
if(root == null) return;
// 递归处理左右子树
flatten(root.left);
flatten(root.right);
// 处理当前根结点
TreeNode right = root.right;
TreeNode left = root.left;
root.left = null;
root.right = left; // 左子树变成右子树
TreeNode p = root;
while(p.right != null)
p = p.right;
p.right = right; // 右子树变成原来左子树的右子树
}
538 把二叉搜索树转换为累加树
给出二叉搜索树的根节点,该树的节点值各不相同,请你将其转换为累加树(Greater Sum Tree),使 每个节点 node 的新值等于原树中大于或等于 node.val 的值之和。
提醒一下,二叉搜索树满足下列约束条件:
- 节点的左子树仅包含键 小于 节点键的节点。
- 节点的右子树仅包含键 大于 节点键的节点。
- 左右子树也必须是二叉搜索树。
方法:反序中序遍历
按照 右孩子 -> 根结点 -> 左孩子 的顺序遍历,实时更新遍历过的结点之和。
TreeNode covertBST(TreeNode root) {
traverse(root);
return root;
}
int sum = 0;
void traverse(TreeNode root) {
if(root == null) return;
traverse(root.right);
sum += root.val;
root.val = sum;
traverse(root.left);
}
二叉搜索树常用操作
搜索一个元素
boolean isInBST(TreeNode root, int target) {
if (root == null) return false;
if (root.val == target) return true;
else if (root.val > target) return isInBST(root.left, target);
else return isInBST(root.right, target);
}
插入一个元素
TreeNode insert(TreeNode root, int target) {
if (root == null) return new TreeNode(target);
if (root.val > target)
root.left = insert(root.left, target);
if (root.val < target)
root.right = insert(root.right, target);
return root;
}
删除一个结点
待删除结点有三种情况:
- 没有左孩子:用右孩子替代自己。
- 没有右孩子:用左孩子替代自己。
- 有左右孩子:用左子树的最大值 / 右子树的最小值替代自己。
TreeNode deleteNode(TreeNode root, int target) {
if (root == null) return null;
if (root.val == target) { // 找到待删除结点
if (root.left == null) return root.right;
if (root.right == null) return root.left;
TreeNode minNode = getMinNode(root.right);
// 用右子树的最小值替代自己
root.val = minNode.val;
root.right = deleteNode(root.right, minNode.val);
} else if (root.val > target) {
root.left = deleteNode(root.left, target);
} else {
root.right = deleteNode(root.right, target);
}
}
TreeNode getMinNode(TreeNode root) {
if (root == null) return null;
TreeNode p = root;
while (p.left != null) {
p = p.left;
}
return p;
}
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步