二叉树的前中后序遍历到底是什么?

原文: http://zhuanlan.zhihu.com/p/1963989382580109799

1. 前言吐槽

刷过 Leetcode 的牛马都知道二叉树是逃不过去的坎儿,尤其是遍历,修改等等。遍历又分成层序遍历,前序中序后序遍历,然后实现方法又分成迭代和递归,每次去敲代码都因为一些细节卡死,比如什么时候打印,什么时候修改,好不容易代码通过了,隔了一段时间,好家伙又忘记怎么写的了

本质上还是对于二叉树理解不够透彻,就比如很长一段时间后,很可能根本不记得前中后序的遍历规则是什么了。这篇文章就是基于刷过的各种文章来做个小结,以便未来忘记了再来快速回忆一下(希望不要再回忆了。。。)

2. “序”到底是什么

序 顾名思义就是顺序。而要有顺序,就得有参考坐标。一般来说,我们会把二叉树的根节点作为参考坐标,类似的每次递归子二叉树的时候,对应二叉树的根节点也就是当前递归轮次的参考坐标了。

参考坐标很好理解,那为什么分成前中后序呢?为什么不是先后(左右)序呢?

“中”序的中又是指什么呢?这些概念最近刷到文献[1]后才豁然开朗。

我们以下面二叉树的节点 2 为例。可以看到遍历节点 2 通常会有 3 次:

  • 第一次是从根节点 1 到节点 2
  • 第二次是节点 2 到节点 3,然后节点 3 返回到节点 2
  • 第三次是节点 4 返回 到节点 2

对于每个节点都是会经过 3 次,即使是节点 7 也是如此,即使它的左右节点都是空,但是也可以看作从 左右空节点返回节点 7。

至此,其实就是前中后序的由来(或者一种方便理解的解释)。第一次进过节点就是该节点的前序,第二次就是中序,第三次就是后序。

一个比较有意思的说法:所以对于任意一个节点而言,后序包含的信息量是最大的,还是以节点 2 为例,它的前序只有节点 1 的信息,中序增加了节点 3 (及其子节点)的信息,后序增加了节点 4(及其子节点)的信息。

3. 递归遍历

了解了前中后序的含义后,那我们自然也就是到为什么前序遍历打印的结果是(根节点,左节点,右节点)了,因为打印操作是在第一次进入该节点执行的,对应到代码就是:

def traverse(root):
 if not root:
  return None
 
 # 第一次进入该节点
 print(root.val)
 traverse(root.left)
 traverse(root.right)

那中序呢?很简单,就是在第二次进入节点的时候执行的操作

def traverse(root):
 if not root:
  return None
 
 # 第一次进入该节点
 traverse(root.left)
 # 第二次进入该节点
 print(root.val)
 traverse(root.right)

后序同理

def traverse(root):
 if not root:
  return None
 
 # 第一次进入该节点
 traverse(root.left)
 # 第二次进入该节点
 traverse(root.right)
 # 第三次进入该节点
 print(root.val)

你可能会说这很简单啊,只要记住前中后序的对应递归代码就好了呀。但是对我而言,上面的逻辑能够帮助我跳脱出递归的代码逻辑,从一个更高的维度来写代码,逻辑会更加清晰,有一种呼吸道新鲜空气的感觉,而不是陷在递归的逻辑沼泽里难以呼吸(有同感的吗 hh)

4. 迭代遍历

除了递归遍历,另一种遍历方法是迭代遍历,就和 for 循环一样显式的写出来遍历过程,这种遍历方式更好理解,在求解二叉树的深度的时候会有奇效

from collections import deque
def iterative(root):
 Q = deque(root)
 depth = 0
 while Q:
  num = len(Q) # 当前层的队列长度
  for i in range(num):
   node = Q.popleft()
   if Q:
    # 下面两行会把左右节点都插入到队列,但有可能是空节点
    # 所以为了下一轮遍历,需要判断试一下是否为空节点
    Q.append(node.left)
    Q.append(node.right)
  depth += 1
 return depth

迭代遍历通常会用到 deque (double-ended queue 双端队列),为了实现层序遍历,那逻辑很简单,就是先把第 i 层(从 0 开始)的所有节点插入到队列里,并记下数量 $n_i$,然后遍历这些数量的节点。 遍历第 i 层节点的同时继续把这些节点的左右节点插入到队列了,遍历完了就是就等价于把第$i+1$层节点也都插入到队列里了,类似地也记录下这一层的节点数量,重复迭代就实现了层序遍历。

有个特别注意的点:Q.append(node)是往队列右侧增加新的节点,为了保证顺序一致,我们需要用node = Q.popleft()来从左边取节点,如果用node = Q.pop()就是从右边取节点,这样就会出现逻辑错误

至此,你了解了二叉树的递归遍历,迭代遍历,前中后序遍历。接下来我们可以继续尝试用迭代遍历实现前中后序遍历:

  • 前序
# Definition for a binary tree node.
# class TreeNode:
#     def __init__(self, val=0, left=None, right=None):
#         self.val = val
#         self.left = left
#         self.right = right
from collections import deque
class Solution:
    def preorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
        if not root:
            return []
        Q = deque([root])
        vals = []
        while Q:
            node = Q.popleft()
            if node:
                vals.append(node.val)
                Q.appendleft(node.right)
                Q.appendleft(node.left)
        return vals
  • 中序
# Definition for a binary tree node.
# class TreeNode:
#     def __init__(self, val=0, left=None, right=None):
#         self.val = val
#         self.left = left
#         self.right = right
from collections import deque
class Solution:
    def inorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
        if not root:
            return []
        Q = deque([])
        node = root
        vals = []
        while Q or node:
            while node:
            # while node and not hasattr(node, 'viist'):
                # node.visit = True
                Q.append(node)
                node = node.left
            node = Q.pop()
            vals.append(node.val)
            node = node.right
        return vals
  • 后序
# Definition for a binary tree node.
# class TreeNode:
#     def __init__(self, val=0, left=None, right=None):
#         self.val = val
#         self.left = left
#         self.right = right
from collections import  deque
class Solution:
    def postorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
        if not root:
            return []
        vals = []
        Q = deque([])
        node = root
        while Q or node:
            while node:
                Q.append(node)
                if node.left:
                    node = node.left
                else:
                    node = node.right
            node = Q.pop()
            vals.append(node.val)
            if Q and Q[-1].left == node:
                node = Q[-1].right
            else:
                node = None
        return vals

参考:

[1] https://labuladong.online/algo/essential-technique/binary-tree-summary-2/#%E4%BA%8C%E5%8F%89%E6%A0%91%E7%9A%84%E9%87%8D%E8%A6%81%E6%80%A7

Flag Counter