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

原创 2017年05月22日 11:56:11

如果你喜欢编译原理,请参看视频
用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).

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

版权声明:本文为博主原创文章,未经博主允许不得转载。

Morris二叉树遍历算法

在遍历儿叉树时,常常使用的是递归遍历,或者是借助于栈来迭代,在遍历过程中,每个节点仅访问一次,所以这样遍历的时间复杂度为O(n),空间复杂度为O(n),并且递归的算法易于理解和实现,二叉树的递归遍历算...
  • guimingyue
  • guimingyue
  • 2014年03月21日 11:42
  • 2593

二叉树神级遍历算法——Morris遍历(C++版)

题目: 设计一个算法实现二叉树的三种遍历(前序遍历 中序遍历 后序遍历)。 要求时间复杂度为O(n) 空间复杂度为O(1)。   思路: 空间复杂度O(1)的要求很严格。常规的递归实现是显然不能满足要...
  • u013575812
  • u013575812
  • 2015年11月27日 14:36
  • 2331

二叉树遍历 Morris O(1)空间复杂度

本文主要解决一个问题,如何实现二叉树的前中后序遍历,有两个要求: 1. O(1)空间复杂度,即只能使用常数空间; 2. 二叉树的形状不能被破坏(中间过程允许改变其形状)。 通常,实现二叉树的...
  • wangmanjie
  • wangmanjie
  • 2016年04月18日 18:38
  • 1157

时间复杂度为O(n),空间复杂度为O(1)

1.字符串左移,void*pszStringRotate(char *pszString, intnCharsRotate),比如 ABCDEFG,移3 位变 DEFGABC,要求空间复杂度 O(1)...
  • u012309042
  • u012309042
  • 2014年09月02日 17:05
  • 2611

二叉树的销毁(O(1)空间复杂度)

本文主要介绍二叉树的删除。 一、递归 算法思想: 在释放某节点时,该节点的左、右子树都已经释放,所以,应该采用后序遍历。二、非递归 同时要求:不能使用栈,空间复杂度必须为O(1) 算法思想:...
  • cyuyanenen
  • cyuyanenen
  • 2016年06月10日 17:11
  • 433

时间复杂度和空间复杂度 1

算法的时间复杂度和空间复杂度合称为算法的复杂度。 1.时间杂度 (1)时间频度 一个算法执行所耗费的时间,从理论上是不能算出来的,必须上机运行测试才能知道。但我们不可能也没有必要对每个算法...
  • u011252685
  • u011252685
  • 2015年04月19日 22:33
  • 557

二叉树后序遍历的四种方法

在二叉树三种顺序的遍历中,后序遍历相对较麻烦一些,其实对于递归方法来说,三种方法大同小异,思路与实现都很简单。后序遍历的迭代法与Morris方法比较麻烦。这里介绍后序遍历的四种方法,其实还是递归、迭代...
  • u012877472
  • u012877472
  • 2015年10月27日 13:52
  • 587

如何判断一个整数数组中是否有重复元素?要求时间复杂度O(n),空间复杂度O(1)

题目: 写一个函数判断一个int类型的数组是否是有效的。  所谓有效是指:假设数组大小为n,那么这个int数组里的值为0~n-1之间的数,并且每个数只能出现一次,否则就是无效数组。  例如[...
  • wuli2496
  • wuli2496
  • 2014年12月26日 15:58
  • 1609

二叉树的非递归遍历(不用栈、O(1)空间)

本文主要解决一个问题,如何实现二叉树的前中后序遍历,有两个要求: O(1)空间复杂度,即只能使用常数空间; 二叉树的形状不能被破坏(中间过程允许改变其形状)。 通常,实现二叉树的前序(preorder...
  • cyuyanenen
  • cyuyanenen
  • 2016年06月09日 14:23
  • 2520

Morris算法进行二叉树遍历

二叉树作为计算机中的一个重要数据结构,在很多领域都会涉及到,而提到二叉树,我们首先想到的就是其3种遍历方式--前序、中序和后序,对于这3种遍历方式,我们很容易通过使用递归或者迭代(http://blo...
  • yangfeisc
  • yangfeisc
  • 2015年05月12日 19:45
  • 1104
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:二叉树面试算法:空间复杂度为 O(1)的Morris遍历法
举报原因:
原因补充:

(最多只允许输入30个字)