1. 二叉树基础与遍历方法精讲
1.1 二叉树数据结构解析
二叉树是每个节点最多有两个子节点的树结构,通常称为左子节点和右子节点。在算法面试和实际应用中,二叉树是最基础也是最重要的数据结构之一。理解二叉树的各种遍历方式及其应用场景,是解决更复杂树形结构问题的基石。
二叉树的节点通常定义为:
java复制class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode(int x) { val = x; }
}
1.2 深度优先遍历(DFS)详解
1.2.1 前序遍历(Pre-order)
前序遍历的顺序是:根节点 → 左子树 → 右子树。这种遍历方式常用于树的复制、序列化等场景。
递归实现:
java复制void preOrder(TreeNode root, List<Integer> result) {
if (root == null) return;
result.add(root.val); // 先访问根节点
preOrder(root.left, result); // 再遍历左子树
preOrder(root.right, result); // 最后遍历右子树
}
迭代实现(使用栈):
java复制List<Integer> preorderTraversal(TreeNode root) {
List<Integer> result = new ArrayList<>();
if (root == null) return result;
Deque<TreeNode> stack = new LinkedList<>();
stack.push(root);
while (!stack.isEmpty()) {
TreeNode node = stack.pop();
result.add(node.val);
// 注意先压右节点,再压左节点
if (node.right != null) stack.push(node.right);
if (node.left != null) stack.push(node.left);
}
return result;
}
1.2.2 中序遍历(In-order)
中序遍历的顺序是:左子树 → 根节点 → 右子树。二叉搜索树的中序遍历结果是有序的。
递归实现:
java复制void inOrder(TreeNode root, List<Integer> result) {
if (root == null) return;
inOrder(root.left, result); // 先遍历左子树
result.add(root.val); // 再访问根节点
inOrder(root.right, result); // 最后遍历右子树
}
迭代实现:
java复制List<Integer> inorderTraversal(TreeNode root) {
List<Integer> result = new ArrayList<>();
Deque<TreeNode> stack = new LinkedList<>();
TreeNode curr = root;
while (curr != null || !stack.isEmpty()) {
// 先到达最左节点
while (curr != null) {
stack.push(curr);
curr = curr.left;
}
curr = stack.pop();
result.add(curr.val);
curr = curr.right; // 转向右子树
}
return result;
}
1.2.3 后序遍历(Post-order)
后序遍历的顺序是:左子树 → 右子树 → 根节点。常用于树的删除、表达式求值等场景。
递归实现:
java复制void postOrder(TreeNode root, List<Integer> result) {
if (root == null) return;
postOrder(root.left, result); // 先遍历左子树
postOrder(root.right, result); // 再遍历右子树
result.add(root.val); // 最后访问根节点
}
迭代实现(反转前序):
java复制List<Integer> postorderTraversal(TreeNode root) {
List<Integer> result = new ArrayList<>();
if (root == null) return result;
Deque<TreeNode> stack = new LinkedList<>();
stack.push(root);
while (!stack.isEmpty()) {
TreeNode node = stack.pop();
result.add(node.val);
// 注意压栈顺序与前序相反
if (node.left != null) stack.push(node.left);
if (node.right != null) stack.push(node.right);
}
Collections.reverse(result); // 反转得到后序结果
return result;
}
1.3 广度优先遍历(BFS/层序遍历)
层序遍历按照树的层级从上到下、从左到右访问节点,通常使用队列实现。
基础实现:
java复制List<List<Integer>> levelOrder(TreeNode root) {
List<List<Integer>> result = new ArrayList<>();
if (root == null) return result;
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
while (!queue.isEmpty()) {
int levelSize = queue.size();
List<Integer> currentLevel = new ArrayList<>();
for (int i = 0; i < levelSize; i++) {
TreeNode node = queue.poll();
currentLevel.add(node.val);
if (node.left != null) queue.offer(node.left);
if (node.right != null) queue.offer(node.right);
}
result.add(currentLevel);
}
return result;
}
提示:层序遍历是解决许多二叉树问题的通用方法,如求最大深度、最小深度、右视图等。
2. 二叉树经典问题解析
2.1 二叉树的最大深度(LeetCode 104)
2.1.1 递归解法
java复制public int maxDepth(TreeNode root) {
if (root == null) return 0;
return Math.max(maxDepth(root.left), maxDepth(root.right)) + 1;
}
时间复杂度:O(n),每个节点访问一次
空间复杂度:O(height),递归栈的深度等于树的高度
2.1.2 迭代解法(层序遍历)
java复制public int maxDepth(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();
for (int i = 0; i < size; i++) {
TreeNode node = queue.poll();
if (node.left != null) queue.offer(node.left);
if (node.right != null) queue.offer(node.right);
}
depth++;
}
return depth;
}
2.2 平衡二叉树判断(LeetCode 110)
2.2.1 自顶向下递归
java复制public boolean isBalanced(TreeNode root) {
if (root == null) return true;
return Math.abs(height(root.left) - height(root.right)) <= 1
&& isBalanced(root.left)
&& isBalanced(root.right);
}
private int height(TreeNode node) {
if (node == null) return 0;
return Math.max(height(node.left), height(node.right)) + 1;
}
2.2.2 自底向上递归(优化)
java复制public boolean isBalanced(TreeNode root) {
return checkHeight(root) != -1;
}
private int checkHeight(TreeNode node) {
if (node == null) return 0;
int left = checkHeight(node.left);
if (left == -1) return -1;
int right = checkHeight(node.right);
if (right == -1) return -1;
if (Math.abs(left - right) > 1) return -1;
return Math.max(left, right) + 1;
}
2.3 二叉树的右视图(LeetCode 199)
2.3.1 层序遍历解法
java复制public List<Integer> rightSideView(TreeNode root) {
List<Integer> result = new ArrayList<>();
if (root == null) return result;
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
while (!queue.isEmpty()) {
int size = queue.size();
for (int i = 0; i < size; i++) {
TreeNode node = queue.poll();
if (i == size - 1) result.add(node.val); // 只记录每层最后一个节点
if (node.left != null) queue.offer(node.left);
if (node.right != null) queue.offer(node.right);
}
}
return result;
}
2.3.2 DFS解法
java复制public List<Integer> rightSideView(TreeNode root) {
List<Integer> result = new ArrayList<>();
dfs(root, 0, result);
return result;
}
private void dfs(TreeNode node, int depth, List<Integer> result) {
if (node == null) return;
if (depth == result.size()) {
result.add(node.val);
}
dfs(node.right, depth + 1, result); // 先访问右子树
dfs(node.left, depth + 1, result); // 再访问左子树
}
2.4 二叉树的所有路径(LeetCode 257)
2.4.1 回溯解法
java复制public List<String> binaryTreePaths(TreeNode root) {
List<String> result = new ArrayList<>();
if (root == null) return result;
backtrack(root, new StringBuilder(), result);
return result;
}
private void backtrack(TreeNode node, StringBuilder path, List<String> result) {
int len = path.length();
if (len != 0) path.append("->");
path.append(node.val);
if (node.left == null && node.right == null) {
result.add(path.toString());
} else {
if (node.left != null) backtrack(node.left, path, result);
if (node.right != null) backtrack(node.right, path, result);
}
path.setLength(len); // 回溯
}
2.4.2 分治解法
java复制public List<String> binaryTreePaths(TreeNode root) {
List<String> paths = new ArrayList<>();
if (root == null) return paths;
if (root.left == null && root.right == null) {
paths.add(root.val + "");
return paths;
}
for (String path : binaryTreePaths(root.left)) {
paths.add(root.val + "->" + path);
}
for (String path : binaryTreePaths(root.right)) {
paths.add(root.val + "->" + path);
}
return paths;
}
3. 二叉树进阶问题解析
3.1 填充每个节点的下一个右侧节点指针(LeetCode 116)
3.1.1 层序遍历解法
java复制public 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();
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;
}
3.1.2 常数空间解法
java复制public Node connect(Node root) {
if (root == null) return null;
Node leftmost = root;
while (leftmost.left != null) {
Node head = leftmost;
while (head != null) {
// 连接同一个父节点的左右子节点
head.left.next = head.right;
// 连接不同父节点的相邻子节点
if (head.next != null) {
head.right.next = head.next.left;
}
head = head.next;
}
leftmost = leftmost.left;
}
return root;
}
3.2 路径总和问题(LeetCode 112)
3.2.1 递归解法
java复制public boolean hasPathSum(TreeNode root, int targetSum) {
if (root == null) return false;
// 到达叶子节点时检查
if (root.left == null && root.right == null) {
return root.val == targetSum;
}
return hasPathSum(root.left, targetSum - root.val) ||
hasPathSum(root.right, targetSum - root.val);
}
3.2.2 迭代解法(DFS)
java复制public boolean hasPathSum(TreeNode root, int targetSum) {
if (root == null) return false;
Stack<TreeNode> nodeStack = new Stack<>();
Stack<Integer> sumStack = new Stack<>();
nodeStack.push(root);
sumStack.push(targetSum - root.val);
while (!nodeStack.isEmpty()) {
TreeNode node = nodeStack.pop();
int currSum = sumStack.pop();
if (node.left == null && node.right == null && currSum == 0) {
return true;
}
if (node.right != null) {
nodeStack.push(node.right);
sumStack.push(currSum - node.right.val);
}
if (node.left != null) {
nodeStack.push(node.left);
sumStack.push(currSum - node.left.val);
}
}
return false;
}
3.3 完全二叉树的节点个数(LeetCode 222)
3.3.1 普通二叉树解法
java复制public int countNodes(TreeNode root) {
if (root == null) return 0;
return 1 + countNodes(root.left) + countNodes(root.right);
}
3.3.2 利用完全二叉树特性的解法
java复制public int countNodes(TreeNode root) {
if (root == null) return 0;
int leftHeight = getLeftHeight(root);
int rightHeight = getRightHeight(root);
if (leftHeight == rightHeight) {
return (1 << leftHeight) - 1; // 2^h - 1
} else {
return 1 + countNodes(root.left) + countNodes(root.right);
}
}
private int getLeftHeight(TreeNode node) {
int height = 0;
while (node != null) {
height++;
node = node.left;
}
return height;
}
private int getRightHeight(TreeNode node) {
int height = 0;
while (node != null) {
height++;
node = node.right;
}
return height;
}
4. 二叉树问题实战技巧
4.1 递归设计原则
- 明确递归函数的定义:清楚函数的功能、参数和返回值
- 确定终止条件:避免无限递归
- 处理当前层逻辑:根据问题需求处理当前节点
- 递归调用:处理子问题
- 清理状态(回溯):如果需要回溯,注意恢复状态
4.2 遍历方式选择指南
- 前序遍历:适合需要先处理父节点再处理子节点的场景(如树的复制)
- 中序遍历:二叉搜索树的有序遍历
- 后序遍历:需要先处理子节点再处理父节点的场景(如计算子树信息)
- 层序遍历:需要按层级处理节点的场景(如求深度、宽度等)
4.3 常见问题排查
- 空指针异常:总是检查节点是否为null
- 递归栈溢出:对于深度很大的树,考虑使用迭代方法
- 逻辑错误:确保递归调用正确处理了所有子节点
- 性能问题:避免重复计算,可以使用记忆化技术
4.4 二叉树问题解题模板
java复制// 递归模板
返回值类型 solveProblem(TreeNode root) {
// 1. 处理空节点情况
if (root == null) return ...;
// 2. 处理叶子节点情况(可选)
if (root.left == null && root.right == null) return ...;
// 3. 递归处理左子树
左子树结果 = solveProblem(root.left);
// 4. 递归处理右子树
右子树结果 = solveProblem(root.right);
// 5. 合并结果
return 合并(左子树结果, 右子树结果, root.val);
}
// 迭代模板(层序遍历)
返回值类型 solveProblem(TreeNode root) {
if (root == null) return ...;
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
while (!queue.isEmpty()) {
int size = queue.size();
for (int i = 0; i < size; i++) {
TreeNode node = queue.poll();
// 处理当前节点
if (node.left != null) queue.offer(node.left);
if (node.right != null) queue.offer(node.right);
}
}
return ...;
}
在实际刷题过程中,我发现理解递归的调用过程非常重要。可以通过画递归树来帮助理解,特别是对于复杂的递归问题。另外,对于迭代解法,要熟练掌握栈和队列的使用场景,前中后序遍历通常使用栈,而层序遍历使用队列。