二叉树面试算法:空间复杂度为 O(1)的Morris遍历法

28 篇文章 2 订阅
2 篇文章 0 订阅

如果你喜欢编译原理,请参看视频
用java开发C语言编译器

如果你喜欢面试算法,请参看视频
如何进入google,算法面试技能全面提升指南

如果你对机器学习感兴趣,请参看一下链接:
机器学习:神经网络导论

如果你喜欢操作系统内核,请参看视频
Linux kernel Hacker, 从零构建自己的内核

对二叉树节点的遍历一般来说有中序,后序,和前序三种遍历方法,如果二叉树的高用h来表示,那三种遍历方法所需要的空间复杂度为O(h). 例如对于中序遍历来说,如果我们使用递归来实现的话,代码如下:

void inorderTraval(TreeNode root) {
    if (root == null) {
        return;
    }

    inorderTraval(root.left);
    System.out.print(root.value + " ");
    inorderTraval(root.right);
}

上面的实现中,有函数的递归调用,递归的深度等于二叉树的高度,也就是说递归导致的调用堆栈的高度等于二叉树的高度,这样的话,程序虽然没有显示的通过new 来分配内存,但实际上消耗的内存大小也是 O(h). 如果二叉树的高度很大,例如搜索引擎把几十亿张网页按照权重来组成二叉树的话,那么二叉树的高度也要几十万作用,因此按照传统的中序遍历,需要消耗大量的内存。

本节要讲的Morris遍历法,能以O(1)的空间复杂度实现二叉树的中序遍历。例如给定下面二叉树:
这里写图片描述

采用中序遍历的话,二叉树节点的访问情况如下:

1,2,3,4,5,6,7,8,9,10

给定某个节点,在中序遍历中,直接排在它前面的节点,我们称之为该节点的前序节点,例如节点5的前序节点就是4,同理,节点10的前序节点就是9.

在二叉树中如何查找一个节点的前序节点呢?如果该节点有左孩子,那么从左孩子开始,沿着右孩子指针一直想有走到底,得到的节点就是它的前序节点,例如节点6的左孩子是4,沿着节点4的右指针走到底,那就是节点5,节点9的左孩子是7,沿着它的右指针走到底对应的节点就是8.

如果左孩子的右节点指针是空,那么左孩子就是当前节点的前序节点。

如果当前节点没有左孩子,并且它是其父节点的右孩子,那么它的前序节点就是它的父节点,例如8的前序节点是7,10的前序节点是9.

如果当前节点没有左孩子,并且它是父节点的左孩子,那么它没有前序节点,并且它自己就是首节点,例如节点1.

值得注意的是,前序节点的右指针一定是空的。

Morris遍历算法的步骤如下:

1, 根据当前节点,找到其前序节点,如果前序节点的右孩子是空,那么把前序节点的右孩子指向当前节点,然后进入当前节点的左孩子。

2, 如果当前节点的左孩子为空,打印当前节点,然后进入右孩子。

3,如果当前节点的前序节点其右孩子指向了它本身,那么把前序节点的右孩子设置为空,打印当前节点,然后进入右孩子。

我们以上面的例子走一遍。首先访问的是根节点6,得到它的前序节点是5,此时节点5的右孩子是空,所以把节点5的右指针指向节点6:
这里写图片描述

进入左孩子,也就到了节点4,此时节点3的前序节点3,右孩子指针是空,于是节点3的右孩子指针指向节点4,然后进入左孩子,也就是节点2
这里写图片描述

此时节点2的左孩子1没有右孩子,因此1就是2的前序节点,并且节点1的右孩子指针为空,于是把1的右孩子指针指向节点2,然后从节点2进入节点1:
这里写图片描述

此时节点1没有左孩子,因此打印它自己的值,然后进入右孩子,于是回到节点2.根据算法步骤,节点2再次找到它的前序节点1,发现前序节点1的右指针已经指向它自己了,所以打印它自己的值,同时把前序节点的右孩子指针设置为空,同时进入右孩子,也就是节点3.于是图形变为:
这里写图片描述

此时节点3没有左孩子,因此打印它自己的值,然后进入它的右孩子,也就是节点4. 到了节点4后,根据算法步骤,节点4先获得它的前序节点,也就是节点3,发现节点3的右孩子节点已经指向自己了,所以打印它自己的值,也就是4,然后把前序节点的右指针设置为空,于是图形变成:

这里写图片描述

接着从节点4进入右孩子,也就是节点5,此时节点5没有左孩子,所以直接打印它本身的值,然后进入右孩子,也就是节点6,根据算法步骤,节点6获得它的前序节点5,发现前序节点的右指针已经指向了自己,于是就打印自己的值,把前序节点的右指针设置为空,然后进入右孩子。

接下来的流程跟上面一样,就不再重复了。我们看看具体的实现代码:


public class MorrisTraval {
    private TreeNode root = null;
    public MorrisTraval(TreeNode r) {
        this.root = r;
    }

    public void travel() {
        TreeNode n = this.root;

        while (n != null) {
            if (n.left == null) {
                System.out.print(n.vaule + " ");
                n = n.right;
            } else {
                TreeNode pre = getPredecessor(n);

                if (pre.right == null) {
                    pre.right = n;
                    n = n.left;
                }else if (pre.right == n) {
                    pre.right = null;
                    System.out.print(n.vaule + " ");
                    n = n.right;
                }

            }
        }
    }

    private TreeNode getPredecessor(TreeNode n) {
        TreeNode pre = n;
        if (n.left != null) {
            pre = pre.left;
            while (pre.right != null && pre.right != n) {
                pre = pre.right;
            }
        }

        return pre;
    }

}

getPredecessor 作用是获得给定节点的前序节点,travel 接口做的就是前面描述的算法步骤,在while循环中,进入一个节点时,先判断节点是否有左孩子,没有的话就把节点值打印出来,有的话,先获得前序节点,然后判断前序节点的右孩子指针是否指向自己,是的话把自己的值打印出来,进入右孩子,前序孩子的右孩子指针是空的话,就把右孩子指针指向自己,然后进入左孩子。

Morris遍历,由于要把前缀节点的右指针指向自己,所以暂时会改变二叉树的结构,但在从前缀节点返回到自身时,算法会把前缀节点的右指针重新设置为空,所以二叉树在结构改变后,又会更改回来。

在遍历过程中,每个节点最多会被访问两次,一次是从父节点到当前节点,第二次是从前缀节点的右孩子指针返回当前节点,所以Morris遍历算法的复杂度是O(n)。在遍历过程中,没有申请新内存,因此算法的空间复杂度是O(1).

更多技术信息,包括操作系统,编译器,面试算法,机器学习,人工智能,请关照我的公众号:
这里写图片描述

  • 5
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值