← 返回题目列表

什么是 Morris 遍历?为什么它能做到 O(1) 额外空间?

高频 困难 第 20 / 30 题 更新于 2026/07/29
二叉树Morris遍历线索化

简化版

Morris 遍历利用空闲的右指针临时建立“前驱节点到当前节点”的线索,从而不用递归栈或显式栈完成遍历。中序遍历时,若当前节点有左子树,就找到左子树最右节点作为前驱,建立或拆除线索;时间 O(n),额外空间 O(1)。

详细版

中序 Morris 的规则是:当前节点没有左子树,直接访问并走右边;有左子树时,找当前节点的中序前驱 pre。如果 pre.right == null,说明第一次到当前节点,建立 pre.right = cur 并转向左子树;如果 pre.right == cur,说明左子树已经访问完,拆线索、访问当前节点、转向右子树。

List<Integer> inorderTraversal(TreeNode root) {
    List<Integer> ans = new ArrayList<>();
    TreeNode cur = root;
    while (cur != null) {
        if (cur.left == null) {
            ans.add(cur.val);
            cur = cur.right;
        } else {
            TreeNode pre = cur.left;
            while (pre.right != null && pre.right != cur) pre = pre.right;
            if (pre.right == null) {
                pre.right = cur;
                cur = cur.left;
            } else {
                pre.right = null;
                ans.add(cur.val);
                cur = cur.right;
            }
        }
    }
    return ans;
}

Morris 会临时修改树结构,所以必须在线索第二次遇到时恢复。

完整版教学

一、普通遍历为什么需要额外空间

递归遍历看似没有显式数据结构,但调用栈保存了“访问完左子树后要回到哪个节点”。迭代遍历则用栈显式保存这批返回点。Morris 的目标就是不使用这些栈空间,改用树上原本为空的指针临时记录返回路径。

递归栈保存: 左子树访问完 -> 回到当前节点
Morris 保存: 前驱.right -> 当前节点

它不是改变遍历顺序,而是把“回来的路”临时写到树里。遍历结束后再把这些临时指针恢复为空。

二、中序前驱为什么能带你回来

在中序遍历中,当前节点 cur 的访问顺序是:左子树全部节点,然后 cur,然后右子树。左子树中最后被访问的节点,就是当前节点的中序前驱,也就是左子树一路向右走到的最右节点。让这个前驱的 right 临时指向 cur,就能在左子树访问完后回到 cur。

      cur
     /
   left
      \
      pre

建立线索: pre.right = cur

因为 pre 原本是左子树最右节点,它的右指针在普通二叉树中应为空。这个空位正好可以临时存线索。

三、两次遇到前驱分别代表什么

找到前驱 pre 后,如果 pre.right == null,说明还没进入左子树,需要建立线索并向左走。如果 pre.right == cur,说明这是通过线索第二次回到 cur,左子树已经处理完,应该拆掉线索并访问 cur。

条件含义动作
pre.right == null第一次到 cur建线索,去左子树
pre.right == cur左子树已完成拆线索,访问 cur,去右子树
cur.left == null没有左子树直接访问 cur

这张表是 Morris 中序的核心,只要记住它,代码不会乱。

四、为什么总时间仍然是 O(n)

看起来每个节点都可能去左子树找最右节点,似乎会重复很多次。但每条临时线索最多建立一次、拆除一次,每条边也只会被有限次经过。整体访问和指针检查次数与节点数成线性关系,所以时间复杂度是 O(n)。

每个节点:
作为 cur 被处理若干常数次
作为 pre 的右线索最多建立 1 次、拆除 1 次
总计仍是线性

额外空间 O(1) 指的是不使用递归栈和显式栈;输出列表如果算入结果空间,当然是 O(n)。

五、前序和后序 Morris 的变化

前序 Morris 和中序很像,只是访问当前节点的时机提前:第一次建立线索时访问 cur,因为前序是根、左、右。后序 Morris 更复杂,通常需要引入虚拟头节点,并在拆线索时逆序访问某段右边界。面试中若只问 Morris,先把中序讲透最重要。

中序: 第二次回到 cur 时访问
前序: 第一次到 cur 时访问
后序: 拆线索时处理边界逆序

如果岗位不是算法强相关,能准确说出中序 Morris 的机制和恢复指针,通常已经足够。

六、常见误区与追问

记忆钩子:Morris 的本质是“借空右指针当回程票”,用完必须归还。

  • 误区:Morris 完全不修改树。 它会临时修改右指针,只是遍历结束前必须恢复。
  • 误区:找到前驱后总是访问当前节点。 中序要等第二次通过线索回来时才访问当前节点。
  • 误区:Morris 适合所有场景。 如果树结构不能被临时修改,或遍历中可能被并发读取,就不适合。
  • 追问:为什么空间是 O(1)? 它只用几个指针变量,没有递归栈和显式栈。
  • 追问:如何避免死循环? 找前驱时条件必须是 pre.right != null && pre.right != cur,第二次遇到要拆线索。
  • 追问:前序 Morris 和中序区别? 前序在第一次建立线索时访问当前节点,中序在拆线索时访问。

七、加强记忆

Morris 遍历可以记成“线索化临时回家路”。普通递归靠栈记住从左子树回来后访问谁,Morris 靠前驱节点的空右指针记住这个返回点。第一次见前驱就建线索,第二次见前驱就拆线索并继续,这样既不丢路,也能在结束时把树恢复原样。