Morris Traversal-常量空间下遍历二叉树

Morris遍历是一种在常数空间复杂度下遍历二叉树的线性时间复杂度算法。它通过利用叶子节点的空指针指向相关母结点来实现回溯。本文详细介绍了Morris遍历在中序、前序和后序遍历中的实现步骤,特别指出前序和中序遍历相对简单,而后序遍历较为复杂,不常用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

通常遍历二叉树有前序(preorder)、中序(inorder)、后序(postorder)遍历有两个常用的方法:一是递归(recursive),二是使用栈实现的迭代版本(stack+iterative)。这两种方法都是O(n)的空间复杂度(递归本身占用stack空间或者用户自定义的stack)。递归的方法很简单,栈使用的方法见楼主另一篇文章,栈和队列在遍历二叉树中的使用。文章总结了常见的几种思路给大家参考。但这些方法都在空间复杂度为O(n),这里还有另一种经典算法,可以做到常数空间使用下遍历,并且时间复杂度仍是线性。就是Morris Traversal。
二叉树遍历最重要的问题就是如何访问完子节点后再回到母结点,否则没法访问其他的子节点。这个问题也叫回溯。之前我们使用栈来存储节点,按照适当的顺序弹出就行,现在使用常量空间,Morris的方法是利用叶子节点的空指针指向相关的母结点,从而完成回溯。下面按照三种遍历分别给出源码及过程。

一、中序遍历

步骤:
1. 如果当前结点的左孩子为空,输出当前节点,并将右孩子作为当前节点;
2. 如果当前节点的左孩子不为空,在当前节点的左子树中寻找当前节点在中序遍历下的前驱节点:
(1)如果前驱节点的右孩子为空,那么将当前节点设为该节点的右孩子,当前节点的左孩子作为新的当前节点。
(2)如果前驱节点的右孩子为当前节点,那么证明已经访问过左子树,输出当前节点,然后当前节点为自己的右孩子。
3. 重复1.2,直至当前节点为空。

morris inorder

void inorderMorrisTraversal(TreeNode *root) {
    TreeNode *cur = root, *prev = NULL;
    while (cur != NULL)
    {
        if (cur->left == NULL)          // 1.
        {
            printf("%d ", cur->val);
            cur = cur->right;
        }
        else
        {
            // find predecessor
            prev = cur->left;
            while (prev->right != NULL && prev->right != cur)
                prev = prev->right;

            if (prev->right == NULL)   // 2.a)
            {
                prev->right = cur;
                cur = cur->left;
            }
            else                       // 2.b)
            {
                prev->right = NULL;
                printf("%d ", cur->val);
                cur = cur->right;
            }
        }
    }
}
二、前序遍历

前序遍历与中序遍历相似,代码上只有一行不同,不同就在于输出的顺序。

步骤:

  1. 如果当前节点的左孩子为空,则输出当前节点并将其右孩子作为当前节点。

  2. 如果当前节点的左孩子不为空,在当前节点的左子树中找到当前节点在中序遍历下的前驱节点。

    a) 如果前驱节点的右孩子为空,将它的右孩子设置为当前节点。输出当前节点(在这里输出,这是与中序遍历唯一一点不同)。当前节点更新为当前节点的左孩子。

    b) 如果前驱节点的右孩子为当前节点,将它的右孩子重新设为空。当前节点更新为当前节点的右孩子。

  3. 重复以上1、2直到当前节点为空。
    morris preorder

void preorderMorrisTraversal(TreeNode *root) {
    TreeNode *cur = root, *prev = NULL;
    while (cur != NULL)
    {
        if (cur->left == NULL)
        {
            printf("%d ", cur->val);
            cur = cur->right;
        }
        else
        {
            prev = cur->left;
            while (prev->right != NULL && prev->right != cur)
                prev = prev->right;

            if (prev->right == NULL)
            {
                printf("%d ", cur->val);  // the only difference with inorder-traversal
                prev->right = cur;
                cur = cur->left;
            }
            else
            {
                prev->right = NULL;
                cur = cur->right;
            }
        }
    }
}
三、后序遍历

后续遍历稍显复杂,需要建立一个临时节点dump,令其左孩子是root。并且还需要一个子过程,就是倒序输出某两个节点之间路径上的各个节点。

步骤:

当前节点设置为临时节点dump。

  1. 如果当前节点的左孩子为空,则将其右孩子作为当前节点。

  2. 如果当前节点的左孩子不为空,在当前节点的左子树中找到当前节点在中序遍历下的前驱节点。

    a) 如果前驱节点的右孩子为空,将它的右孩子设置为当前节点。当前节点更新为当前节点的左孩子。

    b) 如果前驱节点的右孩子为当前节点,将它的右孩子重新设为空。倒序输出从当前节点的左孩子到该前驱节点这条路径上的所有节点。当前节点更新为当前节点的右孩子。

  3. 重复以上1、2直到当前节点为空。

morris postorder

void reverse(TreeNode *from, TreeNode *to) // reverse the tree nodes 'from' -> 'to'.
{
    if (from == to)
        return;
    TreeNode *x = from, *y = from->right, *z;
    while (true)
    {
        z = y->right;
        y->right = x;
        x = y;
        y = z;
        if (x == to)
            break;
    }
}

void printReverse(TreeNode* from, TreeNode *to) // print the reversed tree nodes 'from' -> 'to'.
{
    reverse(from, to);

    TreeNode *p = to;
    while (true)
    {
        printf("%d ", p->val);
        if (p == from)
            break;
        p = p->right;
    }

    reverse(to, from);
}

void postorderMorrisTraversal(TreeNode *root) {
    TreeNode dump(0);
    dump.left = root;
    TreeNode *cur = &dump, *prev = NULL;
    while (cur)
    {
        if (cur->left == NULL)
        {
            cur = cur->right;
        }
        else
        {
            prev = cur->left;
            while (prev->right != NULL && prev->right != cur)
                prev = prev->right;

            if (prev->right == NULL)
            {
                prev->right = cur;
                cur = cur->left;
            }
            else
            {
                printReverse(cur->left, prev);  // call print
                prev->right = NULL;
                cur = cur->right;
            }
        }
    }
}

总的来说,morris这种算法前序和中序比较常用,后序太过繁杂,不好理解,并不常用。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值