二叉树的中序遍历

187 阅读5分钟

对于二叉树的中序遍历,一般有三种解法:

  1. 递归法
  2. 基于栈的迭代法
  3. 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 中序遍历法虽然理解起来复杂,但空间复杂度最低,且无需使用额外的栈或递归,这是其独特的优点。