• 二叉树的非递归遍历,还有一点黑科技


     二叉树的前中后序遍历,可以用递归秒解,看起来不值一提。但如果不允许采用递归,要怎么实现呢?

    还是先来看看递归算法的实现吧:

    def    visit( root):
        if  root  is  not  null:
            #1
            visit(root.left)
            #2
            visit(root.right)
            #3

    上面展示的代码中有三个位置(#1,#2,#3)可以用来插入访问当前节点的代码,分别对应了前中后三种遍历。这三种不同的设定,实际上表达的是访问节点的不同时机。

    我们可以用进栈和出栈来模拟这些递归的过程,在跟#1,#2,#3相对应的时机访问节点,来形成三种不同的访问顺序。先画一棵树当做例子吧。

     

    1. 前序遍历

    试着手动执行以下前序遍历。前序遍历的过程是先访问当前节点,再访问当前节点的左子树,待整个左子树被访问完,再回到当前节点,然后再访问右子树。为了使往左之后还能回到当前节点并往右,

    我们要先把当前节点压入栈中,然后才往左走。我们先以节点1为当前节点,访问它,使它入栈,从节点1往左走,于是当前节点变为节点2。重复这个(访问,入栈,往左)的过程,直到当前节点为空。

    对应上图的树,这个时候已访问节点是 1, 2, 4,并且栈中原始从栈底到栈顶也是1,2,4。此时当前节点为空,不可能再往左走,于是尝试往右。节点4退栈,试着从节点4往右一步,失败。

    重复这个(退栈,尝试往右一步)的操作,直到往右成功,或者栈被清空。当我们把节点2退栈,并尝试往右时成功,于是当前节点就变成节点2的右子节点5。然后又重复上面

    的(访问,入栈,往左),(退栈,尝试往右),一直进行下去直到当前节点为空且栈为空。

    用代码表达就是酱紫的:

    def preOrder(root):
            if root is null:
                return
            stack, current=[ ], root
            while current!=null or stack:
                if current!=null:
                    #访问当前节点
                    stack.append(current)    #入栈
                    current=current.left      #往左
                else:
                    top=stack.pop()          #退栈
                    if top.right:              #尝试往右
                        current=top.right

    2. 中序遍历

    于是中序遍历可以类似地得到。中序遍历可以概括为:重复(入栈,往左),直到不能再往左,然后重复(退栈,访问,尝试往右)。

    代码可以类似地得到:

    def inOrder(root):
            if root is null:
                return
            stack, current=[ ], root
            while current!=null or stack:
                if current!=null:
                    stack.append(current)    #入栈
                    current=current.left      #往左
                else:
                    top=stack.pop()          #退栈
              #访问top节点
                    if top.right:              #尝试往右
                        current=top.right

     

    3. 后序遍历

    采用类似的思路实现后序遍历是有点难度的。显然我们应该在节点退栈之后再访问它,而且节点应该是再它的左右子树都被遍历完之后才退栈并被访问。

    对于上面那棵树的节点2,我们会有三次到达这个节点:1)在节点1往左试探时,2)在节点2的左子树被遍历完之后回来节点2, 3)在右子树被遍历完之后回到节点2。

    以上三次,对应应该执行不同的操作:1)节点2入栈,2)尝试往节点2的右子树去遍历,3)访问节点2。现在的问题在于,怎么区分后两种情况?即怎么知道是从左子树回来还是从右子树回来的?

    其实也简单,只要记录上一个访问的节点是啥就可以了。例如我刚刚访问完节点2的右子树,于是记住上一个被访问的节点是节点5。那么当从5回到2的时候,就知道应该访问节点2,而不是从

    节点2尝试往右。所以站在节点2的角度来看,迫使我们去访问节点2可能性有俩:要么2的右子树为空,要么2的右子树刚已经被遍历完了。于是代码应该写成这个样子:

    def postOrder(root):
            if root is null:
                return
            stack, current, last=[ ], root, null
            while current!=null or stack:
                if current!=null:
                    stack.append(current)    #入栈
                    current=current.left      #往左
                else:
                    top=stack.top()          #查看当前节点
                if top.right is not null and top.right!=last:  #尝试往右
                    current=top.right
                else:
                    stack.pop( )       #出栈
                    #访问top节点
                    last=top

    实际上,这些不同次序的遍历之间暗藏着很多黑科技。比如,感觉上面实现的后续遍历相比于前序和中序遍历而言不够简洁呀,有别的办法嘛?哦肤阔死!

    黑科技1. 改前序遍历。前序得到的是(根左右)的顺序。而我们要得顺序是(左右根)。其实吧,把前序遍历中访问左右子树的顺序对调一下,就得到了(根右左)的顺序,

    再翻转一下,就得到了(左右根)的顺序,即后续遍历的结果。

    黑科技2. 改层序遍历。层序遍历依靠队列来实现,对于(根,左,右)这仨节点而言,入队的顺序是先根,然后访问下一层,即左,右。这样出队的顺序是,(根;左,右)。

    这里如果我们把队列换成栈,根先入栈,被访问,然后访问下一层,即左右。于是出栈的顺序是,(根;右,左)。再翻转也能得到(左,右;根)的顺序。

    贴一下黑科技2的代码,简洁清爽,美丽新世界!

    def postorderTraversal(root):
            if not root:
                return []
            result, stack=[], [root]
            while stack:
                current=stack.pop()
                result+=curr.val,
                if current.left:
                    stack+=current.left,
                if current.right:
                    stack+=current.right,
            return result[::-1]

     

  • 相关阅读:
    一、基础篇--1.1Java基础-反射的用途和实现
    一、基础篇--1.1Java基础-抽象类和接口的区别
    一、基础篇--1.1Java基础-重载和重写的区别
    一、基础篇--1.1Java基础-String、StringBuilder、StringBuffer
    一、基础篇--1.1Java基础-包装类的装箱和拆箱
    一、基础篇--1.1Java基础-int 和 Integer 有什么区别,Integer的值缓存范围
    一、基础篇--1.1Java基础-Exception、Error、RuntimeException与一般异常有何异同
    一、基础篇--1.1Java基础-final, finally, finalize 的区别
    c++ 中 *++ptr,++*ptr等的区别
    c++ new与char*,动态数组,sizeof的注意点总结
  • 原文地址:https://www.cnblogs.com/acetseng/p/4789805.html
Copyright © 2020-2023  润新知