二叉树遍历:前序/中序/后序遍历(递归与迭代)、层序遍历、二叉树的最大深度
二叉树遍历,可以说是面试中的「送分题」,也是「送命题」。
说它送分,是因为套路固定。说它送命,是因为很多人只背了递归,一让写迭代就卡壳。我当年面试某大厂时,面试官让我手写非递归的中序遍历,我愣是想了五分钟才磕磕绊绊写出来。嗯,从那以后,我就把迭代遍历刻进了肌肉记忆。
一、二叉树的遍历方式总览
先搞清楚一件事:遍历顺序是由「根节点」的位置决定的。
| 遍历方式 | 访问顺序 | 典型应用场景 |
|---|---|---|
| 前序遍历 | 根 → 左 → 右 | 序列化二叉树、复制树结构 |
| 中序遍历 | 左 → 根 → 右 | 二叉搜索树的有序输出 |
| 后序遍历 | 左 → 右 → 根 | 删除树、计算子树大小 |
| 层序遍历 | 逐层从左到右 | 求树宽、最短路径 |
说白了,你只要记住「根在哪,就叫什么序」。根在前就是前序,根在中就是中序,根在后就是后序。
二、递归遍历——最直观,但别只会背
递归写法,三行代码搞定。但我建议你理解它的调用栈,而不是死记硬背。
// 前序遍历
func preorder(_ root: TreeNode?) {
guard let root = root else { return }
print(root.val) // 根
preorder(root.left) // 左
preorder(root.right) // 右
}
// 中序遍历
func inorder(_ root: TreeNode?) {
guard let root = root else { return }
inorder(root.left)
print(root.val)
inorder(root.right)
}
// 后序遍历
func postorder(_ root: TreeNode?) {
guard let root = root else { return }
postorder(root.left)
postorder(root.right)
print(root.val)
}
三、迭代遍历——面试真正的考点
递归虽然简单,但面试官往往要求你写迭代版本。为什么?因为递归有栈溢出风险,而且能考察你对栈这种数据结构的理解。
3.1 前序遍历(迭代)
前序遍历的迭代写法是最简单的。你想想看,根先访问,然后左子树,再右子树。用栈的话,应该先压右子节点,再压左子节点。这样出栈时左子节点先被处理。
func preorderIteration(_ root: TreeNode?) -> [Int] {
var result = [Int]()
var stack = [TreeNode]()
var node = root
while node != nil || !stack.isEmpty {
while node != nil {
result.append(node!.val) // 访问根
stack.append(node!)
node = node!.left // 向左走
}
node = stack.removeLast()
node = node?.right // 转向右子树
}
return result
}
3.2 中序遍历(迭代)
中序遍历的迭代写法,我个人觉得是最容易出错的。核心思想是:一路向左压栈,直到没有左子节点,然后出栈访问,再转向右子树。
func inorderIteration(_ root: TreeNode?) -> [Int] {
var result = [Int]()
var stack = [TreeNode]()
var node = root
while node != nil || !stack.isEmpty {
while node != nil {
stack.append(node!)
node = node!.left
}
node = stack.removeLast()
result.append(node!.val) // 访问根
node = node?.right
}
return result
}
3.3 后序遍历(迭代)
后序遍历的迭代写法稍微麻烦一点。因为根节点最后访问,需要记录上一个访问的节点,防止重复入栈。
func postorderIteration(_ root: TreeNode?) -> [Int] {
var result = [Int]()
var stack = [TreeNode]()
var node = root
var lastVisited: TreeNode? = nil
while node != nil || !stack.isEmpty {
while node != nil {
stack.append(node!)
node = node!.left
}
let top = stack.last!
if top.right == nil || top.right === lastVisited {
result.append(top.val)
lastVisited = stack.removeLast()
} else {
node = top.right
}
}
return result
}
这里的关键是:当右子树为空,或者右子树已经被访问过了,才访问当前节点。否则先去处理右子树。
四、层序遍历——BFS 的典型应用
层序遍历用队列实现,这个套路在「树的广度优先搜索」里随处可见。
func levelOrder(_ root: TreeNode?) -> [[Int]] {
guard let root = root else { return [] }
var result = [[Int]]()
var queue = [root]
while !queue.isEmpty {
var level = [Int]()
let count = queue.count
for _ in 0..<count {
let node = queue.removeFirst()
level.append(node.val)
if let left = node.left { queue.append(left) }
if let right = node.right { queue.append(right) }
}
result.append(level)
}
return result
}
五、二叉树的最大深度
求最大深度,说白了就是求根节点到最远叶子节点的距离。递归写法极其优雅:
func maxDepth(_ root: TreeNode?) -> Int {
guard let root = root else { return 0 }
return max(maxDepth(root.left), maxDepth(root.right)) + 1
}
一行核心逻辑:左子树深度和右子树深度的最大值,加上当前节点这一层。
迭代写法呢?用层序遍历,每遍历一层深度加一:
func maxDepthIteration(_ root: TreeNode?) -> Int {
guard let root = root else { return 0 }
var queue = [root]
var depth = 0
while !queue.isEmpty {
let count = queue.count
for _ in 0..<count {
let node = queue.removeFirst()
if let left = node.left { queue.append(left) }
if let right = node.right { queue.append(right) }
}
depth += 1
}
return depth
}
六、面试实战建议
- 先写递归,再优化迭代: 面试时如果时间紧张,先给出递归版本,表明你理解核心逻辑。然后主动说「我还可以用迭代实现,避免栈溢出」。
- 画图辅助: 写迭代遍历时,我习惯在纸上画一个简单的二叉树,手动模拟栈的变化。这样不容易出错。
- 边界条件: 空树、单节点树、只有左子树的树、只有右子树的树。这些情况都要在脑子里过一遍。
总结一下: 二叉树遍历是算法面试的「基本功」。递归考思路,迭代考细节。层序遍历和最大深度是 BFS 的入门题。把这些吃透了,后面遇到「之字形遍历」「右视图」「最近公共祖先」等问题,你就能举一反三了。
好了,这一章的内容就到这里。下一章我们会聊二叉搜索树的增删改查,那又是另一番天地了。