中序遍历(Inorder Traversal)是二叉树遍历中最基础也最重要的方式之一。它的遍历顺序遵循"左子树-根节点-右子树"的原则,这种遍历方式特别适合需要按顺序处理节点值的场景。
理解中序遍历的关键在于把握三个要点:
这种遍历方式之所以重要,是因为对于二叉搜索树(BST)来说,中序遍历会得到一个升序排列的节点值序列。在实际应用中,这种特性被广泛用于数据库索引、文件系统排序等场景。
递归法实现中序遍历是最直观的方式,它直接反映了中序遍历的定义。算法的核心思想可以概括为:
这种"分而治之"的策略完美契合了二叉树的递归结构特性。每次递归调用都处理一个更小的子树,直到遇到空节点开始回溯。
typescript复制function inorderTraversal(root: TreeNode | null): number[] {
const arr: number[] = [];
function helper(node: TreeNode | null) {
if (node === null) return;
helper(node.left); // 递归左子树
arr.push(node.val); // 访问当前节点
helper(node.right); // 递归右子树
}
helper(root);
return arr;
}
这段代码有几个关键点需要注意:
helper来保持递归状态递归法的时间复杂度是O(n),因为每个节点都会被访问一次。空间复杂度则取决于树的高度,最坏情况下(树退化为链表)是O(n),平均情况下是O(log n)。
提示:在实际工程中,递归法虽然简洁,但对于深度很大的树可能会导致栈溢出。这时就需要考虑使用迭代法。
迭代法使用显式的栈结构来模拟递归的隐式调用栈。它的基本思路是:
这种方法巧妙地利用栈的LIFO特性,实现了与递归相同的遍历顺序。
typescript复制function inorderTraversal(root: TreeNode | null): number[] {
const result: number[] = [];
const stack: TreeNode[] = [];
let current = root;
while (current !== null || stack.length > 0) {
// 深入左子树
while (current !== null) {
stack.push(current);
current = current.left;
}
// 回溯到父节点
current = stack.pop()!;
result.push(current.val);
// 转向右子树
current = current.right;
}
return result;
}
这段代码的几个关键点:
current指针跟踪当前节点迭代法的时间复杂度同样是O(n),空间复杂度也是O(h)(h为树高)。与递归法相比:
递归法的优势在于:
迭代法的优势在于:
在实际测试中(以100万个节点的完全二叉树为例):
对于大多数应用场景,两种方法都能很好地工作。但在以下情况应优先选择迭代法:
Morris遍历是一种空间复杂度为O(1)的算法,它通过修改树的结构(临时)来实现遍历:
typescript复制function morrisInorder(root: TreeNode | null): number[] {
const result: number[] = [];
let current = root;
while (current !== null) {
if (current.left === null) {
result.push(current.val);
current = current.right;
} else {
let predecessor = current.left;
while (predecessor.right !== null && predecessor.right !== current) {
predecessor = predecessor.right;
}
if (predecessor.right === null) {
predecessor.right = current;
current = current.left;
} else {
predecessor.right = null;
result.push(current.val);
current = current.right;
}
}
}
return result;
}
这种算法虽然节省空间,但会修改树结构(最后会恢复),适合对空间要求严格的场景。
忘记终止条件:导致无限递归
typescript复制// 错误示例
function helper(node) {
helper(node.left); // 没有终止条件!
arr.push(node.val);
helper(node.right);
}
递归顺序错误:不是严格的中序
typescript复制// 错误示例 - 前序遍历
function helper(node) {
arr.push(node.val); // 先访问根节点
helper(node.left);
helper(node.right);
}
栈状态可视化:在关键点打印栈内容
typescript复制console.log('Stack:', stack.map(n => n.val));
当前节点跟踪:标记当前处理节点
typescript复制console.log('Current:', current?.val);
结果检查:验证遍历顺序是否正确
typescript复制console.log('Partial result:', result);
对于递归法:
对于迭代法:
在实际项目中,我通常会先实现递归版本验证算法正确性,然后在性能关键路径上替换为迭代版本。对于特别大的树结构,Morris算法是值得考虑的选择,但要注意它的临时修改特性可能带来的线程安全问题。