cv.cm

编码 · 数据结构

二叉树遍历详解:前序、中序、后序和层序

阅读约 5 分钟

简短回答

遍历二叉树,就是按固定顺序恰好访问每个节点一次。三种深度优先顺序的区别只在于何时访问节点本身:前序是 根、左、右;中序是 左、根、右;后序是 左、右、根。层序遍历是广度优先,用队列一行一行地访问整棵树。四种遍历的时间都是 O(n)。

遍历顺序为什么重要

树没有唯一的自然顺序,所以算法会选择适合任务的那一种。对二叉搜索树做中序遍历,会按从小到大的顺序得到所有键。前序先访问父节点再访问孩子,复制一棵树或把树写出来以便重建时需要它。后序先访问孩子再访问父节点,删除一棵树、计算文件夹大小或求值表达式树时需要它。层序最先找到最浅的节点,也能逐行打印一棵树。

同一棵树上的四种顺序

我们使用这棵树:根节点 1 有孩子 2 和 3;节点 2 有孩子 4 和 5;节点 3 有右孩子 6。

        1
       / \
      2   3
     / \   \
    4   5   6
顺序规则结果
前序根、左、右1 2 4 5 3 6
中序左、根、右4 2 5 1 3 6
后序左、右、根4 5 2 6 3 1
层序逐行,从左到右1 2 3 4 5 6

记住前三种的简单办法:名字说明了节点放在哪里。前(pre)表示在两棵子树之前,中(in)表示在它们之间,后(post)表示在它们之后。左子树总是先于右子树被访问。

递归实现

递归版本直接对应定义。每个函数在不同的一行访问节点。

class Node:
    def __init__(self, val, left=None, right=None):
        self.val, self.left, self.right = val, left, right
def preorder(node, out):
    if node:
        out.append(node.val)
        preorder(node.left, out)
        preorder(node.right, out)
    return out
def inorder(node, out):
    if node:
        inorder(node.left, out)
        out.append(node.val)
        inorder(node.right, out)
    return out
def postorder(node, out):
    if node:
        postorder(node.left, out)
        postorder(node.right, out)
        out.append(node.val)
    return out
root = Node(1, Node(2, Node(4), Node(5)), Node(3, None, Node(6)))
print(preorder(root, []))   # [1, 2, 4, 5, 3, 6]
print(inorder(root, []))    # [4, 2, 5, 1, 3, 6]
print(postorder(root, []))  # [4, 5, 2, 6, 3, 1]

用栈实现迭代的前序和中序

递归使用调用栈。为了避免在很深的树上触及递归限制,可以自己管理栈。对于前序,弹出一个节点并访问它,然后先压入右孩子再压入左孩子,这样左孩子会先被处理。

对于中序,尽可能向左走,沿途把节点压入栈。当不能再往左走时,弹出一个节点并访问它,然后转向它的右孩子。

def preorder_iter(root):
    out, stack = [], [root] if root else []
    while stack:
        node = stack.pop()
        out.append(node.val)
        if node.right:
            stack.append(node.right)
        if node.left:
            stack.append(node.left)
    return out
def inorder_iter(root):
    out, stack, node = [], [], root
    while stack or node:
        while node:              # go as far left as possible
            stack.append(node)
            node = node.left
        node = stack.pop()
        out.append(node.val)
        node = node.right
    return out
print(preorder_iter(root))  # [1, 2, 4, 5, 3, 6]
print(inorder_iter(root))   # [4, 2, 5, 1, 3, 6]

迭代的后序和层序

迭代的后序最棘手。一种清晰的做法是使用标志位:每个节点压入两次,第二次标记为“孩子处理完后再访问”。层序用队列而不是栈;一次处理队列中的一整层,还能顺便得到每个节点的深度。

from collections import deque
def postorder_iter(root):
    out, stack = [], [(root, False)] if root else []
    while stack:
        node, children_done = stack.pop()
        if children_done:
            out.append(node.val)
        else:
            stack.append((node, True))
            if node.right:
                stack.append((node.right, False))
            if node.left:
                stack.append((node.left, False))
    return out
def level_order(root):
    levels, queue = [], deque([root] if root else [])
    while queue:
        level = []
        for _ in range(len(queue)):   # nodes of the current level
            node = queue.popleft()
            level.append(node.val)
            if node.left:
                queue.append(node.left)
            if node.right:
                queue.append(node.right)
        levels.append(level)
    return levels
print(postorder_iter(root))  # [4, 5, 2, 6, 3, 1]
print(level_order(root))     # [[1], [2, 3], [4, 5, 6]]

后序的另一个常见技巧:先运行修改过的前序(根、右、左),再把结果反转。

时间与空间复杂度

遍历方式时间额外空间原因
前序 / 中序 / 后序(递归)O(n)O(h)调用栈每层保存一个栈帧
前序 / 中序 / 后序(迭代)O(n)O(h)显式栈最多保存一条路径加上兄弟节点
层序O(n)O(w)队列最多保存完整的一层
Morris 中序遍历O(n)O(1)临时把空的右指针改接成线索

这里 n 是节点数,h 是高度,w 是最大宽度。对平衡树来说,h 约为 log2(n),w 可达约 n/2;对链状的树来说,h 是 n,w 是 1。

常见错误

  • 在迭代的前序中先压左孩子再压右孩子。栈是 LIFO,所以想先处理的孩子必须最后压入。
  • 忘记空树:每个函数对 root is None 都应返回空结果。
  • 在层序中用 list 加 pop(0) 当队列。每次弹出是 O(n);请使用 collections.deque。
  • 对很深的树依赖递归。Python 到达递归限制(默认约 1000)时会抛出 RecursionError。
  • 指望单一的遍历结果就能确定一棵树。仅有前序是有歧义的;前序加中序(值互不相同时)才足以重建。

何时使用哪种遍历

任务遍历方式
按顺序列出 BST 的键、验证 BST、求第 k 小中序
复制或序列化一棵树、打印目录大纲前序
删除一棵树、计算子树大小或高度、求值表达式树后序
最小深度、右视图、按层打印、最近的节点层序

前序、中序和后序都是深度优先搜索的形式;层序则是应用在树上的广度优先搜索。同样的思路可以推广到一般的图,只是还需要一个已访问节点的集合。

常见问题

前序、中序和后序有什么区别?

只在于访问节点的位置。前序在子树之前访问节点,中序在左子树和右子树之间访问,后序在两棵子树之后访问。

哪种遍历能得到二叉搜索树的有序输出?

中序遍历。在 BST 中,所有左边的键更小,所有右边的键更大,所以按 左、根、右 访问会得到升序的键。

层序遍历就是 BFS 吗?

是的。层序遍历就是树上的广度优先搜索。它使用队列,并按深度顺序访问节点。

为什么用迭代遍历而不是递归?

递归深度等于树的高度,而 Python 超过递归限制就会抛出 RecursionError。显式栈没有这种限制,也便于中途暂停或提前结束。

能用 O(1) 的额外空间遍历一棵树吗?

可以。Morris 遍历会临时把每个节点的中序前驱链接回该节点,因此不需要栈,并在结束前恢复这棵树。它的时间是 O(n)。

全部编码教程