深入浅出二叉树遍历的非递归算法 2019-11-15(未经允许,禁止转载)

1、二叉树遍历的递归算法

递归实现二叉树的遍历非常直观,回顾一下递归的代码:

  • 前序遍历
def preOrder(Tree node):
    if node == None:
        return
    visit(node.data)
    preOrder(node.leftchild)
    preOrder(node.rightchild)
  • 中序遍历
def inOrder(Tree node):
    if node == None:
        return 
    preOrder(node.leftchild)
    visit(node.data)
    preOrder(node.rightchild)
  • 后序遍历
def postOrder(Tree node):
    if node == None:
        return 
    preOrder(node.leftchild)
    preOrder(node.rightchild)
    visit(node.data)

他们的直观区别仅仅在于visit(node.data)的位置或者说顺序发生了改变

2、二叉树遍历非递归算法转化的来龙去脉

为什么要转化为非递归算法

递归会消耗较多的栈空间,并且不断的函数调用和返回会消耗更多的时间

借助栈非递归化

递归,本质上是函数帧不断出入栈的过程。那么,我们可以直接利用栈的数据结构来模拟递归的过程,转化成非递归的代码实现

非递归化的结点入出栈过程

入栈+出栈+访问

  • 什么时候结点入栈
  • 什么时候结点出栈
  • 什么时候访问结点

要解决上面3个问题,可以对应递归形式的代码:

  • 递归调用导致当前函数环境入栈;
  • 递归返回导致栈顶的函数环境出栈;
  • visit行是对当前结点进行访问,并不涉及任何出入栈操作。其中,访问结点可以是打印,也可以是将结点的值加入某个列表,等等

因为对结点的访问和出入栈没有关系,因此我们先忽略递归代码中的visit语句。此时观察三种遍历的递归代码,可以发现它们的结点出入栈顺序一模一样,重复一次,出入栈顺序一模一样

这里的代码分析很重要!!!

def xxxOrder(Tree node):
    if node == None:
        return 
    
    # A.因为下面要进入递归调用的xxxOrder(node.leftchild),后面还得回到node的
    # 因此只要一执行xxxOrder(node.leftchild),node就会入栈
    xxxOrder(node.leftchild)
    # B.当xxxOrder(node.leftchild)执行完毕返回,即左子树探索完了
    # 控制权自然回到结点node所在的这个函数体,那么结点node就不需要存了。所以,当xxxOrder(node.leftchild)执行完毕返回,node出栈
    
    # C.因为下面要进入递归调用的xxxOrder(node.rightchild),后面也还得回到node的
    # 因此只要一执行xxxOrder(node.rightchild),node就又会再次入栈
    xxxOrder(node.rightchild)
    # D.当xxxOrder(node.rightchild)执行完毕返回,即右子树探索完毕,node出栈

可以清晰地看到,递归时,结点node要走完自己的函数体,经历了2次入栈出栈过程

那么,我们有必要完全模仿这样的2次入出栈的过程吗?当然没有必要
在弄清楚结点出入栈的过程之外,我们更要弄清楚结点出入栈的作用到底是什么

结点node入出栈的作用,很重要!!!

  • 对于前序遍历,结点node在调用xxxOrder(node.leftchild)之前就被访问并入栈,在xxxOrder(node.leftchild)返回后出栈,此时node自己以及它的左子树全部都被访问了,它的出栈仅仅是为了获得接下来的node.rightchild。也就是说,node的第一次出栈,是为了接下来获得它的右孩子。这时,node没有再次入栈的必要
  • 对于中序遍历,结点node在xxxOrder(node.leftchild)返回后出栈。node出栈是为了访问node本身+获得接下来的node.rightchild,此时node的左子树和自己本身都被访问了,在获得node.rightchild之后,接下来的一切都和node无关。这时,node也没有再次入栈的必要
  • 对于后序遍历,结点node在xxxOrder(node.leftchild)返回后出栈,是为了获得接下来的node.rightchild;同时,结点node还需要再次入栈,因为它需要在xxxOrder(node.rightchild)返回后,访问自己。因此,后序遍历是比较特殊的

3、非递归形式的基础代码

基于上面分析,自然而然,非递归化的基本代码就可以写出来了!

对于前序遍历和中序遍历:

def search(TreeNode):
    
    stack = list()
    
    # A.当TreeNode不为None时, 不断入栈TreeNode
    while TreeNode != None:
        # visit(TreeNode)  # 前序
        stack.append(TreeNode)
        TreeNode = TreeNode.left
        
    # 当栈不为空时
    while stack:
        # B.栈顶元素出栈
        top = stack.pop(-1)
        # visit(top)  # 中序
        
        # C.以top.right为根结点,重复ABC
        t = top.right  # 如果是多叉树,则是 for t in top.lefted_childs:
        while t != None:
            # visit(t)  # 前序
            stack.append(t)
            t = t.left

对于后序遍历:

def search(TreeNode):
    
    stack = list()
    last_pop_node = None
    
    # 当TreeNode不为None时, 不断入栈TreeNode
    while TreeNode != None:
        stack.append(TreeNode)
        TreeNode = TreeNode.left
        
    # 当栈不为空时
    while stack:
        
        # 获取栈顶结点top,并判断是否出栈
        top = stack[-1]
        t = top.right
        # 如果top无右子树 or 上一个pop的结点是top的右孩子,说明top的右子树访问完毕,出栈top并访问
        if t is None or t == last_pop_node:
            last_pop_node = stack.pop(-1)
            visit(last_pop_node)
        
        # 否则top不出栈,top的右子树入栈
        else:
            while t != None:
                stack.append(t)
                t = t.left

例题:

给定一个二叉树,(以层次遍历顺序的list作为输入),返回它的 后序 遍历
示例:
输入: [1,null,2,3]
1
\
2
/
3

输出: [3,2,1]

# Definition for a binary tree node.
# class TreeNode:
#     def __init__(self, x):
#         self.val = x
#         self.left = None
#         self.right = None

class Solution:
    def postorderTraversal(self, root: TreeNode) -> List[int]:
        result = []
        stack = list()
        last_pop_node = None
        
        # 当root不为None时, 不断入栈root
        while root != None:
            stack.append(root)
            root = root.left
            
        # 当栈不为空时
        while stack:
            
            # 获取栈顶结点top,并判断是否出栈
            top = stack[-1]
            t = top.right
            # 如果top无右子树or上一个pop的结点是top的右孩子,说明top的右子树访问完毕
            if t is None or t == last_pop_node:
                last_pop_node = stack.pop(-1)
                result.append(last_pop_node.val)
            
            # 否则top不出栈,top的右子树入栈
            else:
                while t != None:
                    stack.append(t)
                    t = t.left
        return result

你可能感兴趣的:(深入浅出二叉树遍历的非递归算法 2019-11-15(未经允许,禁止转载))