第一章:二叉树遍历的核心思想与面试定位
二叉树遍历是数据结构中的基础但关键的内容,广泛应用于算法设计与系统开发中。掌握其核心思想不仅有助于理解树形结构的访问逻辑,更是应对技术面试中高频考题的必备技能。遍历的本质是按照某种顺序访问所有节点,确保不重不漏,常见的有三种深度优先方式:前序、中序和后序,以及一种广度优先方式:层序遍历。遍历方式的本质区别
- 前序遍历:先访问根节点,再遍历左子树,最后右子树(根→左→右)
- 中序遍历:先遍历左子树,再访问根节点,最后右子树(左→根→右)
- 后序遍历:先遍历左右子树,最后访问根节点(左→右→根)
- 层序遍历:按层级从上到下、每层从左到右逐个访问
递归实现示例(Go语言)
// 定义二叉树节点
type TreeNode struct {
Val int
Left *TreeNode
Right *TreeNode
}
// 前序遍历:根 -> 左 -> 右
func preorderTraversal(root *TreeNode) []int {
var result []int
if root == nil {
return result
}
result = append(result, root.Val) // 访问根
result = append(result, preorderTraversal(root.Left)...) // 遍历左子树
result = append(result, preorderTraversal(root.Right)...) // 遍历右子树
return result
}
常见应用场景对比
| 遍历类型 | 典型用途 |
|---|---|
| 前序遍历 | 复制树、序列化树结构 |
| 中序遍历 | 二叉搜索树的有序输出 |
| 后序遍历 | 释放树节点、计算表达式树 |
| 层序遍历 | 求树高、判断完全二叉树 |
graph TD
A[根节点] --> B[左子树]
A --> C[右子树]
B --> D[左节点]
B --> E[右节点]
C --> F[左节点]
C --> G[右节点]
第二章:递归与迭代基础题精讲
2.1 前序遍历的递归与迭代统一解法
递归实现:直观清晰的逻辑结构
前序遍历遵循“根-左-右”的访问顺序,递归写法自然贴合这一逻辑。
void preorder(TreeNode* root) {
if (!root) return;
cout << root->val << " "; // 访问根节点
preorder(root->left); // 遍历左子树
preorder(root->right); // 遍历右子树
}
该实现简洁明了,函数调用栈隐式保存了回溯路径。
迭代实现:显式栈模拟调用过程
使用栈显式维护待处理节点,可避免递归带来的深层调用开销。- 初始化栈并压入根节点
- 循环弹出栈顶并访问
- 先压右子再压左子,保证左子优先处理
2.2 中序遍历的栈模拟实现技巧
在二叉树遍历中,中序遍历要求访问顺序为“左-根-右”。当无法使用递归时,利用栈模拟递归调用过程是一种高效替代方案。核心思路
通过显式栈模拟函数调用栈,先深入遍历左子树,再处理节点,最后转向右子树。代码实现
public List<Integer> inorderTraversal(TreeNode root) {
Stack<TreeNode> stack = new Stack<>();
List<Integer> result = new ArrayList<>();
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;
}
上述代码中,curr 用于追踪当前访问节点,stack 存储待处理的父节点。内层 while 实现左路下降,pop 操作回溯并访问,随后转向右子树,完整复现中序逻辑。
2.3 后序遍历的双栈法与逆序构造思路
双栈法的核心思想
后序遍历要求访问顺序为“左-右-根”,而利用栈结构天然的LIFO特性,可通过两个栈协同工作实现该逻辑。第一个栈用于模拟前序遍历(根-右-左),第二个栈用于反转访问顺序。- 将根节点压入栈1
- 从栈1弹出节点并压入栈2,然后将其左、右子节点依次压入栈1
- 最终从栈2依次弹出即得后序序列
public List<Integer> postorderTraversal(TreeNode root) {
if (root == null) return new ArrayList<>();
Stack<TreeNode> stack1 = new Stack<>();
Stack<TreeNode> stack2 = new Stack<>();
stack1.push(root);
while (!stack1.isEmpty()) {
TreeNode node = stack1.pop();
stack2.push(node); // 存入栈2
if (node.left != null) stack1.push(node.left);
if (node.right != null) stack1.push(node.right);
}
List<Integer> result = new ArrayList<>();
while (!stack2.isEmpty()) {
result.add(stack2.pop().val);
}
return result;
}
上述代码中,栈1按“根-右-左”入栈,栈2接收逆序输出,最终实现“左-右-根”的后序效果。
2.4 层序遍历的队列实现与层级分割
层序遍历,又称广度优先遍历,依赖队列的先进先出特性实现节点的逐层访问。通过将每层节点入队,并在处理时记录当前层的节点数量,可实现层级分割。队列驱动的遍历流程
使用标准队列结构,初始将根节点入队。每次外层循环开始前,获取队列当前长度,即为当前层的节点数。通过固定长度的内层循环,确保仅处理该层所有节点。func levelOrder(root *TreeNode) [][]int {
if root == nil { return nil }
var result [][]int
queue := []*TreeNode{root}
for len(queue) > 0 {
levelSize := len(queue)
var currentLevel []int
for i := 0; i < levelSize; i++ {
node := queue[0]
queue = queue[1:]
currentLevel = append(currentLevel, node.Val)
if node.Left != nil { queue = append(queue, node.Left) }
if node.Right != nil { queue = append(queue, node.Right) }
}
result = append(result, currentLevel)
}
return result
}
上述代码中,levelSize 记录每层节点数,内层循环仅处理这些节点,从而实现层级分割。子节点在处理过程中被加入队列尾部,保证下一层按序遍历。
2.5 二叉树最大深度的DFS与BFS对比分析
深度优先搜索(DFS)实现
DFS通过递归方式遍历子树,逐层深入计算最大深度。
def maxDepthDFS(root):
if not root:
return 0
left = maxDepthDFS(root.left)
right = maxDepthDFS(root.right)
return max(left, right) + 1
该方法时间复杂度为O(n),每个节点访问一次;空间复杂度为O(h),h为树高,取决于递归栈深度。
广度优先搜索(BFS)实现
BFS利用队列逐层扩展,记录层级数即为最大深度。
from collections import deque
def maxDepthBFS(root):
if not root:
return 0
queue = deque([root])
depth = 0
while queue:
depth += 1
for _ in range(len(queue)):
node = queue.popleft()
if node.left:
queue.append(node.left)
if node.right:
queue.append(node.right)
return depth
性能对比分析
| 算法 | 时间复杂度 | 空间复杂度 | 适用场景 |
|---|---|---|---|
| DFS | O(n) | O(h) | 树较深,结构偏斜 |
| BFS | O(n) | O(w) | 树较宽,需早期终止 |
其中h为树的高度,w为最大宽度。DFS在递归深度受限时可能栈溢出,而BFS在宽树中占用更多内存。
第三章:路径与求和类高频题剖析
3.1 路径总和 I:递归终止条件设计
在二叉树路径总和问题中,递归终止条件的设计是算法正确性的核心。合理的终止判断能有效避免无效遍历。基础终止场景分析
当节点为空时,说明当前路径已走到尽头,无法构成有效路径,应立即返回:
if root == nil {
return false
}
该条件作为递归的最基本出口,确保空节点不参与后续计算。
目标值匹配判断
若当前节点为叶子节点(左右子树均为空),且剩余目标值等于当前节点值,则找到一条有效路径:
if root.Left == nil && root.Right == nil && targetSum == root.Val {
return true
}
此条件必须在空节点判断之后执行,保证逻辑顺序正确。
- 递归函数需同步更新目标值:targetSum - root.Val
- 最终结果为左子树或右子树任一满足路径条件
3.2 路径总和 II:回溯路径记录技巧
在二叉树中寻找所有从根到叶子节点的路径,使其路径和等于目标值,是回溯算法的经典应用场景。与路径总和 I 不同,本题要求记录完整路径,因此需维护当前路径状态并适时回退。回溯的核心机制
通过递归遍历左右子树,在进入节点时将值加入路径,满足条件时复制路径快照。返回前必须移除当前节点,保证路径正确性。func pathSum(root *TreeNode, targetSum int) [][]int {
var result [][]int
var path []int
dfs(root, targetSum, &path, &result)
return result
}
func dfs(node *TreeNode, sum int, path *[]int, result *[][]int) {
if node == nil {
return
}
*path = append(*path, node.Val) // 记录当前节点
if node.Left == nil && node.Right == nil && sum == node.Val {
temp := make([]int, len(*path))
copy(temp, *path) // 拷贝当前路径
*result = append(*result, temp)
}
dfs(node.Left, sum-node.Val, path, result) // 递归左子树
dfs(node.Right, sum-node.Val, path, result) // 递归右子树
*path = (*path)[:len(*path)-1] // 回溯:移除当前节点
}
上述代码中,path 是共享切片,每次递归后必须回退最后一个元素,避免路径污染。使用 copy 创建独立副本,防止后续修改影响已收集结果。
3.3 求根到叶子节点的数字总和问题
在二叉树中,从根节点到每个叶子节点的路径可以表示一个数字。例如,路径 `1->2->3` 表示数字 123。目标是计算所有根到叶子路径所形成数字的总和。递归思路解析
采用深度优先搜索(DFS),在遍历过程中累积当前路径的数值。每当到达叶子节点时,将当前值加入总和。
def sumNumbers(root):
def dfs(node, current_sum):
if not node:
return 0
current_sum = current_sum * 10 + node.val
if not node.left and not node.right: # 叶子节点
return current_sum
return dfs(node.left, current_sum) + dfs(node.right, current_sum)
return dfs(root, 0)
上述代码中,current_sum 记录从根到当前节点的数值。每下一层,原值乘以 10 并加上当前节点值,模拟数位左移。
时间与空间复杂度
- 时间复杂度:O(N),N 为节点总数,每个节点访问一次
- 空间复杂度:O(H),H 为树的高度,由递归栈深度决定
第四章:构造与重建类难题突破
4.1 前序与中序构造二叉树的索引映射优化
在重建二叉树的过程中,前序遍历确定根节点,中序遍历划分左右子树。传统方法需频繁查找根节点在中序序列中的位置,时间复杂度为 O(n²)。索引映射优化策略
通过哈希表预存储中序遍历中各节点值与其索引的映射关系,将查找操作降至 O(1)。func buildTree(preorder []int, inorder []int) *TreeNode {
indexMap := make(map[int]int)
for i, val := range inorder {
indexMap[val] = i
}
var dfs func(int, int, int, int) *TreeNode
dfs = func(preLeft, preRight, inLeft, inRight int) *TreeNode {
if preLeft > preRight { return nil }
rootVal := preorder[preLeft]
root := &TreeNode{Val: rootVal}
pivot := indexMap[rootVal]
leftSize := pivot - inLeft
root.Left = dfs(preLeft+1, preLeft+leftSize, inLeft, pivot-1)
root.Right = dfs(preLeft+leftSize+1, preRight, pivot+1, inRight)
return root
}
return dfs(0, len(preorder)-1, 0, len(inorder)-1)
}
代码中,indexMap 存储中序索引,递归函数通过计算子树大小直接划分前序区间,避免重复搜索。
4.2 中序与后序构造二叉树的分治策略
在二叉树重建问题中,中序和后序遍历序列可用于唯一还原树结构。核心思想是利用分治策略:后序遍历的最后一个节点为当前子树的根节点,通过该节点在中序遍历中划分左右子树。算法步骤
- 从后序数组末尾获取根节点值
- 在中序数组中定位该值,分割左右子树区间
- 递归构建右子树与左子树(注意顺序)
代码实现
func buildTree(inorder []int, postorder []int) *TreeNode {
if len(postorder) == 0 { return nil }
rootVal := postorder[len(postorder)-1]
root := &TreeNode{Val: rootVal}
i := 0
for ; i < len(inorder); i++ {
if inorder[i] == rootVal { break }
}
root.Left = buildTree(inorder[:i], postorder[:i])
root.Right = buildTree(inorder[i+1:], postorder[i:len(postorder)-1])
return root
}
上述代码通过切片划分子问题,每次递归处理一个子树。参数说明:inorder 表示中序序列,postorder 为后序序列;关键在于确定根节点位置后,正确划分左右子树区间。
4.3 根据前序遍历序列构建BST的单调栈解法
在已知二叉搜索树(BST)前序遍历序列的前提下,可利用单调栈在线性时间内重构原始树结构。核心思想是维护一个单调递减的节点值栈,依据BST性质判断当前节点应作为左子树还是回溯后作为右子树插入。算法流程
- 初始化空栈,按前序顺序遍历每个节点
- 若当前值小于栈顶,则为左子节点,直接入栈
- 否则不断出栈,直到栈为空或栈顶小于当前值,最后将当前节点作为最后一个出栈节点的右子节点
代码实现
type TreeNode struct {
Val int
Left *TreeNode
Right *TreeNode
}
func bstFromPreorder(preorder []int) *TreeNode {
root := &TreeNode{Val: preorder[0]}
stack := []*TreeNode{root}
for i := 1; i < len(preorder); i++ {
node := &TreeNode{Val: preorder[i]}
if preorder[i] < stack[len(stack)-1].Val {
stack[len(stack)-1].Left = node
} else {
for len(stack) > 0 && stack[len(stack)-1].Val < preorder[i] {
pop := stack[len(stack)-1]
stack = stack[:len(stack)-1]
node = pop
}
node.Right = &TreeNode{Val: preorder[i]}
stack = append(stack, node.Right)
}
stack = append(stack, &TreeNode{Val: preorder[i]})
}
return root
}
上述代码通过维护单调栈实现O(n)时间复杂度的构建过程,适用于大规模BST重建场景。
4.4 序列化与反序列化二叉树的DFS编码方案
在分布式系统或持久化场景中,将二叉树结构转换为字符串表示(序列化)以及从字符串重建原始结构(反序列化)是常见需求。深度优先搜索(DFS)提供了一种简洁高效的编码策略。递归前序遍历编码
采用前序遍历结合分隔符记录节点值,空节点用特殊符号(如`#`)表示,可唯一还原树结构。func serialize(root *TreeNode) string {
if root == nil {
return "#"
}
left := serialize(root.Left)
right := serialize(root.Right)
return strconv.Itoa(root.Val) + "," + left + "," + right
}
该函数递归拼接当前节点值与左右子树编码结果,逗号分隔,`#`标记空节点,保证结构信息完整。
反序列化重建
利用前序遍历顺序,逐个读取值并构建节点,递归恢复左右子树。- 每次消费一个值,若为`#`则返回nil
- 否则创建新节点,并递归构建其左右子树
第五章:从刷题到offer——二叉树面试全复盘
高频考点与思维模式
二叉树在面试中频繁出现,核心考察点包括递归、DFS/BFS遍历、路径求和、对称性判断及BST性质应用。掌握“分解问题+递归处理”的思维模式至关重要。经典题目实战:路径总和 II
给定二叉树和目标和,返回所有从根到叶路径总和等于目标值的路径。关键在于回溯法维护当前路径:
func pathSum(root *TreeNode, targetSum int) [][]int {
var result [][]int
var path []int
dfs(root, targetSum, path, &result)
return result
}
func dfs(node *TreeNode, remaining int, path []int, result *[][]int) {
if node == nil {
return
}
path = append(path, node.Val)
if node.Left == nil && node.Right == nil && remaining == node.Val {
temp := make([]int, len(path))
copy(temp, path)
*result = append(*result, temp)
}
dfs(node.Left, remaining-node.Val, path, result)
dfs(node.Right, remaining-node.Val, path, result)
path = path[:len(path)-1] // 回溯
}
常见陷阱与优化策略
- 忽略空树边界条件,导致空指针异常
- 未及时回溯路径,引发结果污染
- DFS使用全局变量时未重置状态
- 对BST未利用有序性,盲目遍历整棵树
真实面试案例对比
| 公司 | 题目变体 | 考察重点 |
|---|---|---|
| 路径和等于目标的非根节点起始路径 | 双层DFS + 前缀和优化 | |
| Amazon | 镜像对称路径统计 | 递归结构比对 + 路径哈希记录 |
733

被折叠的 条评论
为什么被折叠?



