对于二叉树的中序遍历,一般有三种解法:
- 递归法
- 基于栈的迭代法
- Morris 中序遍历
下面我们会按照题目要求,分别给出这三种解法。
解法一:递归法
- 算法概括:这是一种最直观的解法,通过递归的方式,首先遍历左子树,然后访问当前节点,最后遍历右子树。
- 算法详细描述:递归的方式其实就是模拟了系统栈,所以和基于栈的迭代法本质上是相同的。我们定义一个辅助函数
inorder,如果当前节点为空,直接返回;如果当前节点不为空,先递归遍历左子树,然后访问当前节点,最后递归遍历右子树。 - 算法推导过程:以一个三节点二叉树为例,首先遍历左子树,然后访问根节点,最后遍历右子树。
- 空间复杂度和时间复杂度:时间复杂度为O(n),空间复杂度为O(n),其中 n 是二叉树的节点数。这是因为需要遍历所有节点,且最坏情况下,递归深度与节点数相同。
- 解法的优缺点:递归法是最直观的解法,代码简洁,但是在二叉树的节点数非常大的情况下,可能会导致函数调用栈溢出。
- 怎样更好的理解解法:可以通过在纸上画出递归过程,帮助理解递归的遍历顺序。
class TreeNode {
val: number
left: TreeNode | null
right: TreeNode | null
constructor(val?: number, left?: TreeNode | null, right?: TreeNode | null) {
this.val = (val === undefined ? 0 : val)
this.left = (left === undefined ? null : left)
this.right = (right === undefined ? null : right)
}
}
function inorderTraversal(root: TreeNode | null): number[] {
let res: number[] = [];
function inorder(node: TreeNode | null) {
if (!node) return;
inorder(node.left);
res.push(node.val);
inorder(node.right);
}
inorder(root);
return res;
}
解法二:基于栈的迭代法
- 算法概括:通过一个栈来模拟系统栈的行为,实现二叉树的中序遍历。
- 算法详细描述:初始化一个空栈,先将所有的左孩子节点压入栈中,当左孩子节点为空时,从栈顶取出一个节点,访问这个节点,然后将右孩子节点压入栈中,重复这个过程,直到栈为空。
- 算法推导过程:我们依次遍历左子树,然后访问当前节点,最后遍历右子树。与递归法的遍历顺序相同,只是递归法是通过系统栈来实现的,而迭代法是通过用户栈来实现的。
- 空间复杂度和时间复杂度:时间复杂度为O(n),空间复杂度为O(n)。与递归法相同,也需要遍历所有节点,并且在最坏情况下,栈的大小与节点数相同。
- 解法的优缺点:基于栈的迭代法可以有效地避免函数调用栈溢出的问题,适合处理节点数非常大的二叉树。但是,代码比递归法稍微复杂一些。
- 怎样更好的理解解法:通过在纸上画出栈的变化过程,帮助理解迭代的遍历顺序。
function inorderTraversal(root: TreeNode | null): number[] {
let res: number[] = [];
let stack: (TreeNode | null)[] = [];
let curr = root;
while (curr || stack.length) {
while (curr) {
stack.push(curr);
curr = curr.left;
}
curr = stack.pop()!;
res.push(curr.val);
curr = curr.right;
}
return res;
}
解法三:Morris 中序遍历
- 算法概括:Morris 中序遍历是一种无栈且不用递归的遍历方式,其空间复杂度为O(1)。
- 算法详细描述:Morris 中序遍历的核心思想是利用二叉树节点中的 null 指针域(即左孩子或右孩子为空的指针域)来实现线索的功能,即将 null 指针域指向某个具有特定含义的节点,从而避免了使用栈或递归。
- 算法推导过程:Morris 中序遍历的步骤是:首先从根节点开始,寻找当前节点的前驱节点,如果前驱节点的右孩子为空,那么将前驱节点的右孩子指向当前节点,并将当前节点更新为其左孩子;如果前驱节点的右孩子为当前节点,那么将前驱节点的右孩子设为 null,并将当前节点的值加入到结果中,然后将当前节点更新为
其右孩子。重复这个过程,直到当前节点为空。
- 空间复杂度和时间复杂度:时间复杂度为O(n),空间复杂度为O(1)。虽然需要遍历所有节点,但由于没有使用栈和递归,因此空间复杂度为常数级。
- 解法的优缺点:Morris 中序遍历的优点是空间复杂度低,无需使用额外的栈或者递归。但这种方法会改变原二叉树的结构,而且理解起来相对复杂。
- 怎样更好的理解解法:可以通过在纸上画出遍历过程和前驱节点的改变,帮助理解遍历的顺序和改变的过程。
function inorderTraversal(root: TreeNode | null): number[] {
let res: number[] = [];
let curr = root;
let pre: TreeNode | null;
while (curr) {
if (!curr.left) {
res.push(curr.val);
curr = curr.right;
} else {
pre = curr.left;
while (pre.right && pre.right !== curr) {
pre = pre.right;
}
if (!pre.right) {
pre.right = curr;
curr = curr.left;
} else {
pre.right = null;
res.push(curr.val);
curr = curr.right;
}
}
}
return res;
}
总结
以上就是二叉树的中序遍历的三种常见解法:递归法、基于栈的迭代法和 Morris 中序遍历法。它们各有优缺点,需要根据实际的需求和条件来选择合适的解法。理解这三种解法的关键是理解二叉树的中序遍历顺序:左子树 -> 根节点 -> 右子树。其中,递归法是最直观的解法,但可能会导致函数调用栈溢出;基于栈的迭代法可以避免函数调用栈溢出的问题,适合处理大规模数据;Morris 中序遍历法虽然理解起来复杂,但空间复杂度最低,且无需使用额外的栈或递归,这是其独特的优点。