一. 二叉树的遍历
94. 二叉树的中序遍历
中序遍历是二叉树遍历的一种方法,它按照“左子树-根节点-右子树”的顺序访问每个节点。具体到算法实现,有两种主要方式:递归和迭代。
1. 迭代方法
理解中序遍历的迭代方法
-
初始化栈和指针: 创建一个空栈来存储将要访问的树节点。同时,创建一个指针
current
指向根节点。 -
遍历左子树: 持续将
current
和current
的所有左子节点压入栈中,直到current
为空。这一步模拟了递归方法中不断深入左子树的过程。 -
处理节点并移至右子树: 当
current
为空时,说明已经到达最左侧节点,此时从栈中弹出一个节点,这是当前要处理(访问)的节点。将其值加入结果列表中,然后将current
指向弹出节点的右子节点。 -
重复以上步骤: 重复步骤2和3,直到栈为空且
current
也为空。此时,所有节点已按中序遍历的顺序被访问。
Java代码实现:
**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> list = new ArrayList<>();
Stack<TreeNode> stack = new Stack<>();
TreeNode current = root;
// 注意,是或的条件,current不是null和stack不是空有一个成立即可。
while (current != null || !stack.isEmpty()) {
// 遍历左子树,一直到最左边
while(current != null) {
// 只要current不为空,就先把current压入到栈中
stack.push(current);
// 移向左子树
current = current.left;
}
// 当讲current与current下面所有的左子树压入到栈中后,弹出栈节点。
// 并转向右子树
current = stack.pop();
// 加入结果集
list.add(current.val);
// 转向右子树
current = current.right;
}
return list;
}
}
解释
-
while (current != null || !stack.isEmpty())
: 这个循环条件确保了所有的节点都被访问。只要当前节点不为空或栈不为空,循环就继续。 -
内层的
while
循环: 这个循环不断把左子节点压入栈中,直到到达最左端的节点。 -
处理节点: 当达到最左端的节点时,开始从栈中弹出节点进行处理。这一步模拟了访问节点的操作。
-
转向右子树: 访问完一个节点后,转向该节点的右子树。如果右子树为空,循环会继续从栈中弹出下一个节点。
这种迭代方法有效地模拟了递归过程,同时避免了递归可能导致的栈溢出问题,特别适用于深度较大的二叉树。
2. 递归方法
中序遍历是二叉树遍历的一种方法,它按照“左子树-根节点-右子树”的顺序访问每个节点。具体到算法实现,有两种主要方式:递归和迭代。
class Solution {
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> result = new ArrayList<>();
inorder(root, result);
return result;
}
private void inorder(TreeNode node, List<Integer> result) {
if (node == null) {
return;
}
inorder(node.left, result); // 访问左子树
result.add(node.val); // 访问根节点
inorder(node.right, result); // 访问右子树
}
}
230. 二叉搜索树中第K小的元素
因为中序遍历是左中右,所以中序遍历可以正好符合二叉搜索树从小到大的排序。
此题解法使用一个数组 result
来存储两个值:当前的节点计数器(result[0]
)和第 k 小的元素(result[1]
)。当计数器达到 k 时,我们就找到了所需的元素,并将其存储在 result[1]
中。
class Solution {
public int kthSmallest(TreeNode root, int k) {
// 记录结果
在 Java 中,当你创建一个新的整型数组,例如 int[] result = new int[2];,数组中的每个元素都会自动被初始化为 0。
int[] result = new int[2]; // result[0] 存储计数器, result[1] 存储第 k 小的元素
inOrderTraverse(root, k, result);
return result[1];
}
private void inOrderTraverse(TreeNode node, int k, int[] result) {
if (node == null) {
return;
}
// 先遍历左子树
inOrderTraverse(node.left, k, result);
// 访问当前节点
if (++result[0] == k) {
result[1] = node.val;
return;
}
// 遍历右子树
inOrderTraverse(node.right, k, result);
}
}
class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode() {}
TreeNode(int val) { this.val = val; }
TreeNode(int val, TreeNode left, TreeNode right) {
this.val = val;
this.left = left;
this.right = right;
}
}
此处注意,为什么不用k递减的方法:
因为无论是基本数据类型还是它们的包装类作为参数传递给函数,函数内部对这些参数的任何修改都不会影响原始变量。这一点对于理解 Java 函数参数的行为非常关键。
如果想用基本数据类型,需要将他们转换成数组或者是对象的状态来实现。
详情请看我的另一篇文章
如果想用k递减的方式去实现,需要将k变成数组,这样就可以实现
class Solution {
public int kthSmallest(TreeNode root, int k) {
return mid(root, new int[]{k});
}
private int mid(TreeNode root, int[] k) {
if (root == null) {
return -1; // 表示未找到
}
int left = mid(root.left, k);
if (left != -1) {
return left; // 左子树已找到
}
if (--k[0] == 0) {
return root.val; // 当前节点是第 k 小的元素
}
return mid(root.right, k); // 继续搜索右子树
}
}
class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode() {}
TreeNode(int val) { this.val = val; }
TreeNode(int val, TreeNode left, TreeNode right) {
this.val = val;
this.left = left;
this.right = right;
}
}
二叉树的先序遍历(迭代DFS)
二叉树先序遍历即为DFS,就是借助Stack来实现,先将栈顶元素pop出来,然后先压右,再压左。
114. 二叉树展开为链表
- 初始化一个栈,将根节点压入栈中。
- 当栈不为空时,循环执行以下步骤:
- 弹出栈顶元素,记为当前节点
curr
。 - 如果
curr
的右子节点不为空,将它压入栈中。 - 如果
curr
的左子节点不为空,将它压入栈中,然后将curr
的左子节点设为 null。 - 如果栈不为空,将
curr
的右子节点设置为栈顶元素(这是因为在先序遍历中,当前节点的下一个节点是它左子节点的最右边的节点,或者是它的右子节点)。
- 弹出栈顶元素,记为当前节点
注意,此题的难点在于第三步多加一部将curr.left 设置为null, 和第四步。
class Solution {
public void flatten(TreeNode root) {
if (root == null) return;
Stack<TreeNode> stack = new Stack<>();
stack.push(root);
while (!stack.isEmpty()) {
TreeNode curr = stack.pop();
if (curr.right != null) {
stack.push(curr.right);
}
if (curr.left != null) {
stack.push(curr.left);
curr.left = null; // 将左子树设置为 null
}
if (!stack.isEmpty()) {
curr.right = stack.peek(); // 将右子节点设置为下一个节点
}
}
}
}
二. 宽度优先搜索
104. 二叉树的最大深度
1. 递归方法
基本思路是递归地计算左子树和右子树的深度,然后取二者中的较大值,并加上当前节点自身的深度(即1)。
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public int maxDepth(TreeNode root) {
if (root == null) {
return 0; // 空节点的深度为0
} else {
int leftDepth = maxDepth(root.left); // 计算左子树的深度
int rightDepth = maxDepth(root.right); // 计算右子树的深度
return Math.max(leftDepth, rightDepth) + 1; // 取左右子树深度的较大值,并加上当前节点的深度1
}
}
}
2. 迭代方法(宽度优先算法)
-
初始化队列和深度计数器: 首先创建一个队列来保存将要遍历的节点,并初始化深度为0。
-
遍历树的每一层: 使用一个循环来遍历树的每一层。每次循环开始时,队列中的节点数就是当前层的节点数。 每次开始for循环之前,队列中只包含同层的所有节点。
for
循环的作用是遍历二叉树的每一层。在每次外部的 while
循环迭代中,队列 queue
包含了树的一层所有节点。for
循环遍历这些节点,并为下一次的 while
循环迭代准备队列,即下一层的所有节点。
-
遍历当前层的所有节点: 对于当前层的每个节点,检查它的左右子节点。如果子节点不为空,则将其加入队列中。
-
深度增加: 完成当前层的遍历后,深度计数器增加1。
-
返回最大深度: 当队列为空时,所有层都已遍历完成,此时的深度计数器的值就是树的最大深度。
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public int maxDepth(TreeNode root) {
if (root == null) {
return 0;
}
int depth = 0;
宽度优先算法用队列,队列用链表
Queue<TreeNode> queue = new LinkedList<>();
别忘了首先将root加入进去
queue.add(root);
while (!queue.isEmpty()) {
先获取当前队列的大小。这个大小就是同高度的所有节点个数。
int size = queue.size();
将同一高度的所有节点都弹出,并将他们的所有子节点都推入队列中
for (int i = 0; i < size; i ++) {
TreeNode current = queue.poll();
先压左再压右
if (current.left != null) {
queue.add(current.left);
}
if (current.right != null) {
queue.add(current.right);
}
}
循环完同一高度之后,就将深度加一。
depth ++;
}
return depth;
}
}
解释一下for循环里面的步骤:
-
确定当前层的节点数: 在进入
for
循环之前,我们通过int size = queue.size();
获取当前队列的大小,即当前层中的节点数。 -
遍历当前层的所有节点:
for
循环遍历这些节点。对于每个节点:- 从队列中移除(
poll()
)。 - 检查其左子节点和右子节点。
- 如果左子节点或右子节点非空,将它们加入队列。这样做是为了在下一次
while
循环迭代时遍历这些子节点。
- 从队列中移除(
-
为下一层准备: 当
for
循环结束时,当前层的所有节点都已被处理,并且它们的子节点(即下一层的节点)已加入队列。此时,外部的while
循环将再次检查队列,如果队列不为空,则意味着还有更多层需要遍历。 -
重复直到遍历完所有层: 这个过程会一直重复,直到队列为空,即没有更多的层需要遍历。此时,我们已经访问了树的每一层。
使用 for
循环确保了我们在移动到下一层之前,能够完全遍历当前层的所有节点。这是广度优先搜索(BFS)算法的关键部分,它确保了我们按层次顺序遍历树的节点。
543. 二叉树的直径
迭代方法(需要借助最大深度)
此题难点是想到要用接著一个全局变量来记录最大深度
思路也是难点,需要想到这个最大的直径实际就是左子树的深度加上右子树的深度。
-
定义一个全局变量,用于存储直径的最大值。
-
深度优先搜索(DFS):对于每个节点,我们递归地计算它的左子树和右子树的深度。
-
计算直径:对于每个节点,其直径可以定义为左子树的深度与右子树的深度之和。我们比较当前的直径与全局最大直径,如果当前直径更大,则更新全局最大直径。
-
返回值:在递归过程中,每个节点需要返回其子树的最大深度给其父节点。
class Solution {
int maxDiameter = 0;
public int diameterOfBinaryTree(TreeNode root) {
maxDepth(root);
return maxDiameter;
}
private int maxDepth(TreeNode node) {
if (node == null) {
return 0;
}
// 计算左子树的深度
int leftDepth = maxDepth(node.left);
// 计算右子树的深度
int rightDepth = maxDepth(node.right);
// 更新最大直径
注意当左子树的深度和右子树的深度相加,正好是所求的的路径长度。正好不算根节点的深度
maxDiameter = Math.max(maxDiameter, leftDepth + rightDepth);
// 返回节点的最大深度
return Math.max(leftDepth, rightDepth) + 1;
}
}
102.二叉树的层序遍历(宽度优先搜索)
思路:
运用BFS,同上一题的BFS搜索,此题因为要求了同一层的在一个list中,所以用for循环,如果没有要求同一层在一个list中,就不用for循环了。
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public List<List<Integer>> levelOrder(TreeNode root) {
if (root == null) {
return new ArrayList<>();
}
Queue<TreeNode> queue = new LinkedList<>();
List<List<Integer>> result = new ArrayList<>();
queue.add(root);
while (!queue.isEmpty()) {
int size = queue.size();
List<Integer> list = new ArrayList<>();
for (int i = 0; i < size; i++) {
TreeNode current = queue.poll();
list.add(current.val);
if (current.left != null) {
queue.add(current.left);
}
if (current.right != null) {
queue.add(current.right);
}
}
result.add(list);
}
return result;
}
}
266.翻转二叉树
1. 递归方法
递归方法是利用递归来反转每个节点的左右子树。以下是详细的步骤:
-
检查基本情况:如果当前节点为
null
,则返回null
。这意味着我们已经到达了叶节点的子节点。 -
递归翻转左子树:对当前节点的左子节点调用
invertTree
函数,它会递归地翻转整个左子树。 -
递归翻转右子树:对当前节点的右子节点调用
invertTree
函数,它会递归地翻转整个右子树。 -
交换左右子树:将当前节点的左子树设置为步骤2的结果,右子树设置为步骤3的结果。
-
返回当前节点:返回修改后的当前节点,它现在是翻转后的子树的根节点。
class Solution {
public TreeNode invertTree(TreeNode root) {
// 基本情况:如果树为空,直接返回null
if (root == null) {
return null;
}
// 递归地翻转左右子树
TreeNode left = invertTree(root.left);
TreeNode right = invertTree(root.right);
// 交换左右子树
root.left = right;
root.right = left;
// 返回当前节点
return root;
}
}
二叉树的递归方法总结。
当去写一个递归方法时,不要去强行的理解一些递归的逻辑,去一层一层的想如何嵌套的。就把递归的方法当作一种Service,一个业务方法,这个业务方法就是解决这个问题。比如这个题中invertTree解决的问题就是反转左右子树。那么只需要将root.right翻转之后,放入到root.left。再把root.left翻转之后变成root.right。
再比如求二叉树的深度,
maxDepth函数解决求二叉树的高度的需求。那么就需要把root.left的高度求出来,再把root.right的高度求出来,最后再取它俩的最大值就好。
2. 迭代方法(BFS)
迭代方法通常使用队列来层序遍历树,并在每一层交换节点的左右子树。以下是详细的步骤:
-
初始化队列:创建一个队列,用于存储树的节点。首先将根节点加入队列。
-
遍历树:当队列不为空时,重复以下步骤:
a. 弹出当前节点:从队列中取出一个节点。
b. 交换子节点:交换当前节点的左右子节点。
c. 添加子节点到队列:如果左子节点不为空,将其加入队列;如果右子节点不为空,也将其加入队列。
-
继续直到队列为空:当队列为空时,所有节点都被处理过,树已经翻转。
-
返回根节点:函数返回原始的根节点,这时它代表翻转后的整棵树。
import java.util.LinkedList;
import java.util.Queue;
class Solution {
public TreeNode invertTree(TreeNode root) {
if (root == null) {
return null;
}
Queue<TreeNode> queue = new LinkedList<>();
queue.add(root);
while (!queue.isEmpty()) {
TreeNode current = queue.poll();
// 交换当前节点的左右子树
TreeNode temp = current.left;
current.left = current.right;
current.right = temp;
// 把子节点加入队列以便后续处理
if (current.left != null) {
queue.add(current.left);
}
if (current.right != null) {
queue.add(current.right);
}
}
return root;
}
}
101. 对称二叉树
1. 递归方法
此题不是直接对原函数递归,而是创建一个辅助函数来进行操作,此辅助函数是判断接收的两个节点是否对称。
-
创建一个辅助函数:这个函数接收两个节点作为参数,比较它们是否对称。
-
比较节点:
- 如果两个节点都是
null
,返回true
(它们对称)。 - 如果其中一个是
null
,另一个不是,返回false
(它们不对称)。 - 如果两个节点的值不相等,返回
false
。
- 如果两个节点都是
-
递归比较:对左子树的左节点和右子树的右节点进行比较,以及左子树的右节点和右子树的左节点进行比较。
-
返回结果:如果所有的比较都是对称的,返回
true
;否则,返回false
。
class Solution {
public boolean isSymmetric(TreeNode root) {
return root == null || 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;
return (left.val == right.val) && isMirror(left.left, right.right) && isMirror(left.right, right.left);
}
}
2.迭代方法(BFS)
此题将新节点加入队列的顺序是难点。
-
创建一个队列:用于存储将要比较的节点对。
-
初始化队列:将根节点的左右子节点加入队列。
-
遍历队列:当队列不为空时,重复以下步骤:
- 弹出两个节点进行比较。
- 检查它们的值是否相等。如果不相等,返回
false
。 - 将左节点的左子节点和右节点的右子节点加入队列。
- 将左节点的右子节点和右节点的左子节点加入队列。
-
返回结果:如果所有的比较都是对称的,返回
true
;否则,返回false
。
import java.util.LinkedList;
import java.util.Queue;
class Solution {
public boolean isSymmetric(TreeNode root) {
Queue<TreeNode> queue = new LinkedList<>();
queue.add(root);
queue.add(root);
while (!queue.isEmpty()) {
TreeNode t1 = queue.poll();
TreeNode t2 = queue.poll();
此处几个判断很重要,和上面BFS不一样的地方。以前都是判断t1.left或者t1.right != null。现在是对取出来的进行判断,既可以判断是否对称,也避免了null.null的空指针报错。
if (t1 == null && t2 == null) continue;
if (t1 == null || t2 == null) return false;
if (t1.val != t2.val) return false;
queue.add(t1.left);
queue.add(t2.right);
queue.add(t1.right);
queue.add(t2.left);
}
return true;
}
}
三. 二叉搜索树
二叉搜索树(BST) 的性质:对于树中的每个节点,其左子树中的所有元素都比它小,右子树中的所有元素都比它大。
108. 将有序数组转换为二叉搜索树
思路:
如上面的几道题一样,同样是构建一个辅助递归方法‘constructBST’
来构建二叉搜索树。constructBST
方法接受当前考虑的数组部分(通过左右边界指示)并返回该部分所构成的BST的根节点。
由于输入数组是有序的,为了保持BST的性质并使得树保持高度平衡,我们可以采用如下策略:
- 选择数组中间的元素作为树的根节点,以保证左右子树的大小尽可能相等,从而维持平衡。
- 对根节点左侧的数组元素递归地执行相同的操作,创建左子树。
- 对根节点右侧的数组元素递归地执行相同的操作,创建右子树。
方法的执行流程如下:
-
递归的终止条件:首先检查
left
和right
的关系。如果left
大于right
,意味着当前没有元素可用于创建树节点,因此返回null
。 -
选择中间元素作为根节点:计算
left
和right
的中点mid
。这个中点元素将作为当前子树的根节点。这样做的目的是为了平衡左右子树的大小。 -
创建根节点:使用
nums[mid]
创建一个新的TreeNode
。这个节点成为当前子数组构建的BST的根。 -
递归构建左子树:调用
constructBST
方法,传入相同的数组和更新后的边界(left
到mid - 1
),以构建当前节点的左子树。这个递归调用处理数组中位于当前根节点左侧的部分。 -
递归构建右子树:类似地,调用
constructBST
方法,传入更新后的边界(mid + 1
到right
),以构建当前节点的右子树。这个递归调用处理数组中位于当前根节点右侧的部分。 -
返回构建的节点:返回当前构建的根节点(带有其左右子树)。
class Solution {
public TreeNode sortedArrayToBST(int[] nums) {
// 调用递归方法来构建二叉搜索树
return constructBST(nums, 0, nums.length - 1);
}
private TreeNode constructBST(int[] nums, int left, int right) {
// 如果左边界大于右边界,则递归结束
if (left > right) {
return null;
}
// 选择中间位置的元素作为当前子树的根
// (left + right) / 2 也可以,但下面的写法可以防止溢出
int mid = left + (right - left) / 2;
// 创建当前节点
TreeNode node = new TreeNode(nums[mid]);
// 递归构建左子树, 注意需要mid - 1
node.left = constructBST(nums, left, mid - 1);
// 递归构建右子树, 注意需要mid + 1
node.right = constructBST(nums, mid + 1, right);
// 返回当前节点
return node;
}
}
98. 验证二叉搜索树
1. 递归方法(DFS)
要解决这个问题,即验证一个二叉树是否为有效的二叉搜索树(BST),关键在于理解BST的定义:对于树中的每个节点,其左子树中的所有节点都必须小于它,右子树中的所有节点都必须大于它。此外,每个子树也必须是BST。
一个有效的方法是使用递归。在递归过程中,我们需要保持每个节点值的有效范围,确保左子节点的值小于当前节点的值,而右子节点的值大于当前节点的值。同时,我们需要确保这个范围在递归的每一层都被正确地维护。
class Solution {
public boolean isValidBST(TreeNode root) {
return isValidBST(root, Long.MIN_VALUE, Long.MAX_VALUE);
}
private boolean isValidBST(TreeNode node, long min, long max) {
// 空节点默认是有效的BST
if (node == null) {
return true;
}
// 如果节点值不在其有效范围内,则不是有效的BST
注意必须有=
if (node.val <= min || node.val >= max) {
return false;
}
// 检查左子树:更新最大值为当前节点的值
// 检查右子树:更新最小值为当前节点的值
return isValidBST(node.left, min, node.val) && isValidBST(node.right, node.val, max);
}
}
在这个解决方案中:
-
isValidBST(TreeNode root)
是主方法,它调用辅助方法isValidBST(TreeNode node, long min, long max)
。 -
辅助方法接收三个参数:当前节点
node
,以及该节点值应在的最小值min
和最大值max
。 -
对于每个节点,我们检查其值是否在
(min, max)
范围内。如果不在这个范围,说明它违反了BST的性质,我们返回false
。 -
然后我们递归地对左子树和右子树进行检查。对于左子树,最大值更新为当前节点的值,对于右子树,最小值更新为当前节点的值。
-
如果左子树和右子树都是有效的BST,那么当前树也是有效的BST。
通过这种方式,我们可以确保在树的每一层,所有节点都符合BST的定义,从而验证整棵树是否为有效的BST。
2. 迭代方法(深度优先搜索)
在这个代码中:
TreeNodeWrapper
是一个辅助类,用于存储树的节点以及与该节点相关联的最小值和最大值范围。- 使用
Stack<TreeNodeWrapper>
来存储节点和它们的值范围。 - 每次从栈中弹出一个节点,并检查该节点的值是否在其允许的范围内。
- 如果节点的值不在允许的范围内,返回
false
。 - 否则,将其左子节点和右子节点(连同更新后的值范围)压入栈中,继续迭代。
- 如果栈为空,意味着所有节点都满足BST的条件,因此返回
true
。
import java.util.Stack;
class Solution {
public boolean isValidBST(TreeNode root) {
if (root == null) {
return true;
}
Stack<TreeNodeWrapper> stack = new Stack<>();
stack.push(new TreeNodeWrapper(root, Long.MIN_VALUE, Long.MAX_VALUE));
while (!stack.isEmpty()) {
TreeNodeWrapper current = stack.pop();
TreeNode node = current.node;
long lower = current.lower;
long upper = current.upper;
if (node == null) {
continue;
}
if (node.val <= lower || node.val >= upper) {
return false;
}
stack.push(new TreeNodeWrapper(node.right, node.val, upper));
stack.push(new TreeNodeWrapper(node.left, lower, node.val));
}
return true;
}
private static class TreeNodeWrapper {
TreeNode node;
long lower;
long upper;
TreeNodeWrapper(TreeNode node, long lower, long upper) {
this.node = node;
this.lower = lower;
this.upper = upper;
}
}
}
四. 构造二叉树
105. 从前序与中序遍历序列构造二叉树
-
理解先序遍历和中序遍历的特点:
- 先序遍历的顺序是“根-左-右”,所以先序遍历数组的第一个元素总是树的根节点。
- 中序遍历的顺序是“左-根-右”,所以在中序遍历数组中,根节点左边的元素都是左子树的部分,右边的元素都是右子树的部分。
-
递归构造树:
- 找到先序遍历数组中的第一个元素,这是当前树的根节点。
- 在中序遍历数组中找到根节点,这将数组分为左子树和右子树的部分。
- 递归地使用左子树的先序遍历和中序遍历数组构造左子树。
- 递归地使用右子树的先序遍历和中序遍历数组构造右子树。
在这个代码中,build
函数用于递归地构造子树。它接收当前子树的先序遍历和中序遍历的子数组,并返回构造的子树的根节点。代码首先找到当前子树的根节点,然后确定左子树的长度,接着递归地构造左子树和右子树。
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public TreeNode buildTree(int[] preorder, int[] inorder) {
return build(preorder, 0, preorder.length - 1, inorder, 0, inorder.length - 1);
}
public TreeNode build(int[] preorder, int preStart, int preEnd, int[] inorder, int inStart, int inEnd) {
if (preStart > preEnd) {
return null;
}
int rootValue = preorder[preStart];
TreeNode root = new TreeNode(rootValue);
int rootIndex = inStart;
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;
}
}
在递归方法中,构造二叉树的 build
函数接收一些参数,用于指定当前子树在先序遍历和中序遍历数组中的对应部分。这些参数的含义如下:
-
preorder
:这是一个整数数组,表示二叉树的先序遍历序列。 -
preStart
:这是当前子树在先序遍历数组preorder
中的起始索引。 -
preEnd
:这是当前子树在先序遍历数组preorder
中的结束索引。 -
inorder
:这是另一个整数数组,表示二叉树的中序遍历序列。 -
inStart
:这是当前子树在中序遍历数组inorder
中的起始索引。 -
inEnd
:这是当前子树在中序遍历数组inorder
中的结束索引。
build
函数使用这些参数来确定当前递归步骤处理的子树在两个遍历数组中的位置。函数的工作流程如下:
- 根据
preStart
从preorder
数组中获取当前子树的根节点值。 - 在
inorder
数组中找到根节点值的位置,这将中序遍历数组分为左右两部分,分别对应子树的左子树和右子树。 - 递归地调用
build
函数来构建左子树和右子树。对于左子树,我们使用左子树在先序遍历和中序遍历数组中的相应部分;对于右子树,同样使用右子树在这两个数组中的相应部分。
在 build
函数中,这些参数用于确定当前子树在先序和中序遍历数组中的位置。让我们一步步来分析:
-
preStart + 1
: 在先序遍历数组中,第一个元素(位于preStart
)总是当前子树的根节点。因此,左子树的先序遍历将从preStart + 1
开始。 -
preStart + leftSize
: 为了找到左子树在先序遍历数组中的结束位置,我们需要知道左子树的大小(即节点数量)。这可以通过中序遍历数组来确定。在中序遍历中,根节点将数组分为两部分:左边是左子树,右边是右子树。因此,左子树的大小是根节点在中序遍历数组中的索引 (rootIndex
) 减去左子树起始位置的索引 (inStart
)。所以,左子树在先序遍历数组中的结束位置是preStart + leftSize
。 -
inStart, rootIndex - 1
: 这些是左子树在中序遍历数组中的起始和结束位置。由于中序遍历的顺序是左子树 -> 根节点 -> 右子树,左子树部分自然是从inStart
开始,到根节点的前一个位置(rootIndex - 1
)结束。
迭代解法:
class Solution {
public TreeNode buildTree(int[] preorder, int[] inorder) {
if (preorder.length == 0) {
return null;
}
Stack<TreeNode> stack = new Stack<>();
TreeNode root = new TreeNode(preorder[0]);
stack.push(root);
int inorderIndex = 0;
for (int i = 1; i < preorder.length; i++) {
int preorderVal = preorder[i];
TreeNode node = stack.peek();
if (node.val != inorder[inorderIndex]) {
node.left = new TreeNode(preorderVal);
stack.push(node.left);
} else {
while (!stack.isEmpty() && stack.peek().val == inorder[inorderIndex]) {
node = stack.pop();
inorderIndex++;
}
node.right = new TreeNode(preorderVal);
stack.push(node.right);
}
}
return root;
}
}
五. 二叉树的路径问题(DFS)
437. 路径总和 III
解法: (使用递归的DFS)
这个问题可以通过深度优先搜索(DFS)算法来解决。我们需要考虑两种情况:一是路径从根节点开始,二是路径不从根节点开始。对于第一种情况,我们从根节点开始递归地搜索每个节点;对于第二种情况,我们对每个节点执行相同的操作,但是我们把当前节点当作根节点来处理。
这个方法的基本思路是:
pathSum
方法用于计算以任意节点开始的路径数。pathsFromNode
方法用于计算从特定节点开始的路径数,这包括了从该节点的子节点开始的路径。
此题的难点是想出要分两种情况去讨论。应该想到有一种情况是包含根节点。
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public int pathSum(TreeNode root, int targetSum) {
if (root == null) {
return 0;
}
long ts = (long) targetSum;
// 从根节点开始的路径数 + 从左子节点开始的路径数 + 从右子节点开始的路径数
return pathsFromNode(root, ts) + pathSum(root.left, targetSum) + pathSum(root.right, targetSum);
}
private int pathsFromNode(TreeNode node, long targetSum) {
if (node == null) {
return 0;
}
// 当前节点值是否等于目标值 + 从左子节点开始的路径数 + 从右子节点开始的路径数
return (node.val == targetSum ? 1 : 0) + pathsFromNode(node.left, targetSum - node.val) + pathsFromNode(node.right, targetSum - node.val);
}
}
124. 二叉树中的最大路径和
解题思路:
注意! 路径有区别于二叉树。在DFS时,路径只能选择左或右的一边与根节点相连作为当前子树的最大路径和。而不像二叉树那样可以返回根加左加右! 这点是很容易忽视的。
我们的策略是:检查每个节点,看看包含这个节点的最好路径是什么。这条路径可以是:
- 仅包含该节点自身。
- 包含该节点和它左边的最佳路径。
- 包含该节点和它右边的最佳路径。
- 包含该节点,它左边的最佳路径,以及它右边的最佳路径。
为了找到整个花园的最佳路径,我们需要检查每个节点,计算包括这个节点的最佳路径,然后记录下这些路径中的最大值。
下面是这个想法的具体实现:
- 我们从根节点开始,然后递归地检查每个节点。
- 对于每个节点,我们计算四种情况中的最大值(上面提到的四种情况),然后更新一个全局变量来记录这个最大值。
- 我们也需要返回当前节点能提供的最大单边路径和,这是因为当我们检查这个节点的父节点时,我们只能从它的左边或右边的子节点中选择一个。
当计算左子树的最大贡献值,如果为负则忽略这个子树。
maxGain辅助函数的意义是比较maxSum与当前子树的最大路径。 并返回当前子树所能提供的最大路径。注意maxPathSum需要返回的是maxSum。与maxGain辅助函数返回值不一样。
class Solution {
int maxSum = Integer.MIN_VALUE;
public int maxPathSum(TreeNode root) {
maxGain(root);
return maxSum;
}
private int maxGain(TreeNode node) {
if (node == null) {
return 0;
}
// 计算左子树的最大贡献值,如果为负则忽略这个子树
int leftGain = Math.max(maxGain(node.left), 0);
// 计算右子树的最大贡献值,如果为负则忽略这个子树
int rightGain = Math.max(maxGain(node.right), 0);
// 当前节点的最大路径和为该节点的值加上左右子树的最大贡献值
int priceNewpath = node.val + leftGain + rightGain;
// 更新全局最大路径和
maxSum = Math.max(maxSum, priceNewpath);
// 返回当前节点的最大贡献值
return node.val + Math.max(leftGain, rightGain);
}
}