引言
二叉树是数据结构中的基石,而遍历则是操作二叉树的基础。无论是前端框架中的虚拟DOM diff算法,还是后端数据库的索引结构,都离不开二叉树遍历的身影。本文将带你全面掌握二叉树的各种遍历方法,从递归到迭代,从深度优先到广度优先,结合实战代码示例,让你彻底搞懂二叉树遍历的精髓。
一、二叉树基础概念
1.1 什么是二叉树
二叉树是一种每个节点最多有两个子节点的树形结构,通常分为左子节点和右子节点。常见的二叉树类型包括满二叉树、完全二叉树、二叉搜索树等。
1.2 为什么需要遍历
遍历二叉树就是按某种顺序访问树中的所有节点,使每个节点被访问一次且仅被访问一次。遍历是许多二叉树操作的基础,如插入、删除、查找、排序等。
二、深度优先遍历(DFS)
深度优先遍历是沿着树的深度优先访问节点,分为前序、中序和后序三种方式,主要使用递归或栈实现。
2.1 前序遍历
遍历顺序:根节点 -> 左子树 -> 右子树
递归实现
var preorderTraversal = function(root) { // 二叉树 前序遍历 递归
let res = []
const preorder = function(cur) {
if (!cur) return
res.push(cur.val) // 访问根节点
preorder(cur.left) // 递归左子树
preorder(cur.right) // 递归右子树
}
preorder(root)
return res
};
迭代实现
利用栈先进后出的特性,先压右子节点再压左子节点:
var preorderTraversal = function(root) { // 二叉树 前序遍历 迭代法
let res = []
let stack = []
if (!root) return res
stack.push(root)
while (stack.length) {
let cur = stack.pop()
res.push(cur.val) // 访问根节点
if (cur.right) stack.push(cur.right) // 右子节点入栈
if (cur.left) stack.push(cur.left) // 左子节点入栈
}
return res
};
2.2 中序遍历
遍历顺序:左子树 -> 根节点 -> 右子树
递归实现
var inorderTraversal = function(root) { // 二叉树 中序遍历 递归
let res = []
const dfs = function(cur) {
if (!cur) return
dfs(cur.left) // 递归左子树
res.push(cur.val) // 访问根节点
dfs(cur.right) // 递归右子树
}
dfs(root)
return res
};
迭代实现
需要先遍历到最左节点,再回溯访问根节点和右子树:
var inorderTraversal = function(root) { // 二叉树 中序遍历 迭代法
let res = []
let stack = []
let cur = root
while (cur || stack.length) {
if (cur) {
stack.push(cur)
cur = cur.left
} else {
cur = stack.pop()
res.push(cur.val) // 访问根节点
cur = cur.right
}
}
return res
};
2.3 后序遍历
遍历顺序:左子树 -> 右子树 -> 根节点
递归实现
var postorderTraversal = function(root) { // 二叉树 后序遍历 递归
let res = []
const dfs = function(cur) {
if (!cur) return
dfs(cur.left) // 递归左子树
dfs(cur.right) // 递归右子树
res.push(cur.val) // 访问根节点
}
dfs(root)
return res
};
迭代实现
可以通过前序遍历变种(根->右->左)再反转结果得到:
var postorderTraversal = function(root) { // 二叉树 后序遍历 迭代法
let res = []
let stack = []
if (!root) return res
stack.push(root)
while (stack.length) {
let cur = stack.pop()
res.unshift(cur.val) // 结果头部插入
if (cur.left) stack.push(cur.left)
if (cur.right) stack.push(cur.right)
}
return res
};
2.4 统一迭代法
通过标记法(null节点)统一三种遍历的代码风格:
var preorderTraversal = function(root) { // 二叉树 前序遍历 统一迭代法
let res = []
let stack = []
if (root) stack.push(root)
while (stack.length) {
let cur = stack.pop()
if (!cur) {
res.push(stack.pop().val)
continue
}
if (cur.right) stack.push(cur.right)
if (cur.left) stack.push(cur.left)
stack.push(cur)
stack.push(null) // 标记节点
}
return res
};
三、广度优先遍历(BFS)
广度优先遍历(层序遍历)是从根节点开始,逐层访问树的节点,主要使用队列实现。
3.1 基本层序遍历
var levelOrder = function(root) {
let res = [] // 存储最终结果的二维数组
let queue = [] // 使用队列辅助BFS遍历
if (root) queue.push(root)
while (queue.length) {
let len = queue.length // 当前层节点数
let temp = [] // 临时存储当前层节点值
while (len--) {
let cur = queue.shift()
temp.push(cur.val)
if (cur.left) queue.push(cur.left)
if (cur.right) queue.push(cur.right)
}
res.push(temp)
}
return res
};
3.2 层序遍历的应用
3.2.1 二叉树的最大深度
var maxDepth = function(root) { // 层次遍历
let queue = []
let depth = 0
if (root) queue.push(root)
while (queue.length) {
let len = queue.length
while (len--) {
let cur = queue.shift()
if (cur.left) queue.push(cur.left)
if (cur.right) queue.push(cur.right)
}
depth++ // 每遍历完一层,深度加1
}
return depth
};
3.2.2 二叉树的最小深度
var minDepth = function(root) {
let queue = []
if (root) queue.push(root)
let depth = 0
while (queue.length) {
let len = queue.length
while (len--) {
let cur = queue.shift()
// 找到第一个叶子节点,返回当前深度+1
if (!cur.left && !cur.right) return depth + 1
if (cur.left) queue.push(cur.left)
if (cur.right) queue.push(cur.right)
}
depth++
}
return depth
};
3.2.3 每层节点的最大值
var largestValues = function(root) { // 层序遍历
let res = []
let queue = []
if (root) queue.push(root)
while(queue.length) {
let len = queue.length
let max = -Infinity // 初始化当前层最大值
while (len--) {
let cur = queue.shift()
max = Math.max(max, cur.val) // 更新最大值
if (cur.left) queue.push(cur.left)
if (cur.right) queue.push(cur.right)
}
res.push(max)
}
return res
};
3.2.4 填充每个节点的下一个右侧节点指针
var connect = function(root) {
let queue = []
if (root) queue.push(root)
while (queue.length) {
let len = queue.length
let pre = null
while (len--) {
let cur = queue.shift()
if (pre) pre.next = cur // 连接同一层节点
pre = cur
if (cur.left) queue.push(cur.left)
if (cur.right) queue.push(cur.right)
}
}
return root
};
四、遍历方法对比与应用场景
| 遍历方法 | 实现方式 | 时间复杂度 | 空间复杂度 | 应用场景 |
|---|---|---|---|---|
| 前序遍历 | 递归/迭代 | O(n) | O(n) | 复制树、前缀表达式 |
| 中序遍历 | 递归/迭代 | O(n) | O(n) | 二叉搜索树排序 |
| 后序遍历 | 递归/迭代 | O(n) | O(n) | 删除节点、后缀表达式 |
| 层序遍历 | 迭代(队列) | O(n) | O(n) | 树的深度/宽度、逐层处理 |
五、总结与拓展
二叉树遍历是数据结构中的基础操作,掌握各种遍历方法不仅能帮助我们更好地理解树形结构,也是解决复杂算法问题的基石。本文介绍的递归、迭代和统一迭代法各有特点:
- 递归实现简洁易懂,但可能受限于栈深度
- 迭代实现效率更高,适合大规模数据
- 统一迭代法通过标记节点,实现了三种DFS遍历的代码统一
在实际开发中,我们需要根据具体问题选择合适的遍历方法。例如,在处理DOM树时常用DFS,而在处理层级关系明确的数据时常用BFS。