跳转至

EA4-Morris遍历

以下是 Morris 遍历 的核心 C 语言实现(前序和中序),它是一种 O(1) 空间复杂度 的二叉树遍历方法,不使用递归也不使用栈,通过临时修改树的空指针(线索化) 来实现。


✅ Morris 遍历核心思想

利用叶子节点的 null 指针(尤其是右指针)指向其中序后继,形成线索,遍历完成后恢复原结构。

关键角色:当前节点 curr 和它的 前驱(predecessor)


一、Morris 中序遍历(Inorder)

C
void morrisInorder(TreeNode* root) {
    TreeNode* curr = root;

    while (curr) {
        // 情况1:没有左子树 → 直接访问并进入右子树
        if (curr->left == NULL) {
            printf("%d ", curr->data);  // 访问根(此时是中序位置)
            curr = curr->right;         // 转向右子树
        }
        // 情况2:有左子树 → 找左子树的最右节点(即中序前驱)
        else {
            TreeNode* predecessor = curr->left;
            // 找到左子树的最右节点(注意:不能等于 curr)
            while (predecessor->right && predecessor->right != curr) {
                predecessor = predecessor->right;
            }

            // 子情况2.1:前驱的右指针为空 → 第一次到达,建立线索
            if (predecessor->right == NULL) {
                predecessor->right = curr;  // 指向当前节点(线索)
                curr = curr->left;          // 继续遍历左子树
            }
            // 子情况2.2:前驱右指针已指向 curr → 第二次到达,拆除线索
            else {
                predecessor->right = NULL;  // 恢复原结构
                printf("%d ", curr->data);  // 访问根(中序位置)
                curr = curr->right;         // 遍历右子树
            }
        }
    }
}

二、Morris 前序遍历(Preorder)

与中序几乎相同,仅在 第一次建立线索时访问根节点

C
void morrisPreorder(TreeNode* root) {
    TreeNode* curr = root;

    while (curr) {
        if (curr->left == NULL) {
            printf("%d ", curr->data);  // 无左子树,直接访问
            curr = curr->right;
        } else {
            TreeNode* predecessor = curr->left;
            while (predecessor->right && predecessor->right != curr) {
                predecessor = predecessor->right;
            }

            if (predecessor->right == NULL) {
                predecessor->right = curr;
                printf("%d ", curr->data);  // ✅ 第一次到此,前序访问根
                curr = curr->left;
            } else {
                predecessor->right = NULL;  // 恢复
                curr = curr->right;         // 不再访问根
            }
        }
    }
}

三、Morris 后序遍历(较复杂,非常少见)

通常不推荐手写。可通过反转路径方式实现,此处略过。


🔍 难点与记忆技巧

难点 讲解与记忆方法
1. 为什么要修改指针? 利用空指针“借道”返回父节点,避免用栈记录路径。遍历完立即恢复,不影响原树
2. 什么是前驱(predecessor)? 在中序序列中,curr 的前一个是其左子树的最右节点。记住:“左子树的最右” = 中序前驱
3. 如何判断是第一次还是第二次? 通过 predecessor->right == NULL 表示第一次(建线索),== curr 表示第二次(拆线索)。这是 Morris 的“握手标志”。
4. 前序 vs 中序 的访问时机?
  • 前序:在 predecessor->right == NULL 时访问(第一次)
  • 中序:在 predecessor->right == curr 时访问(第二次)
✅ 记忆口诀:“前序早访问,中序晚访问”
5. 为什么只改右指针? 因为左子树遍历完要返回根,所以用 right 指向根。这正是“线索二叉树”的思想。

📌 Morris 遍历特点总结

项目 说明
时间复杂度 O(n) — 每条边最多走两次
空间复杂度 O(1) — 无栈无递归
是否修改树? 是(临时),但最终恢复
适用场景 嵌入式、内存受限环境;面试高频考点
缺点 代码较难理解;并发环境下不安全(修改了结构)

✅ 记忆口诀(背下来)

“有左找前驱,无左直输出;
空则建线索,回则拆并访;
前序早访问,中序晚一步。”


总结:Morris 遍历是算法设计的精妙体现 —— 用时间换空间,用线索化替代栈。掌握前序和中序即可,重点理解“两次访问 + 线索建立与拆除”机制。