二叉树基础与遍历

数据结构与算法专题 · 树形数据结构的基石

专题:数据结构与算法系统学习

关键词:数据结构, 算法, 二叉树, 前序遍历, 中序遍历, 后序遍历, 层序遍历, 递归, Morris遍历

一、二叉树基础概念

1.1 二叉树的定义

二叉树(Binary Tree)是每个节点最多只有两个子节点(称为左孩子和右孩子)的树形数据结构。二叉树是树形结构中最基本也最常用的一种形式,是理解更复杂树结构(如二叉搜索树、平衡二叉树、红黑树、B树等)的基础。

数学上,二叉树可以递归定义:二叉树要么是空树,要么由一个根节点和两个分别称为左子树和右子树的二叉树组成。这个递归定义构成了后续所有二叉树算法的基础。

1 / \ 2 3 / \ \ 4 5 6

1.2 基本术语

深度与高度的区分:深度是从根节点向下数,高度是从叶子节点向上数。一棵只有根节点的树,根节点的深度为0,高度也为0。整棵树的高度等于根节点的高度,也等于最大深度值。

1.3 二叉树的数学性质

二、二叉树的表示方法

2.1 链式存储(最常用)

链式存储是二叉树最经典的表示方式。每个节点包含三个部分:数据域、指向左子节点的指针(left)、指向右子节点的指针(right)。

class TreeNode: def __init__(self, val=0, left=None, right=None): self.val = val self.left = left self.right = right # 构建示例二叉树 # 1 # / \ # 2 3 # / \ \ # 4 5 6 root = TreeNode(1) root.left = TreeNode(2) root.right = TreeNode(3) root.left.left = TreeNode(4) root.left.right = TreeNode(5) root.right.right = TreeNode(6)

三叉链表:在需要频繁回溯父节点的场景中,可以在节点结构中额外增加一个 parent 指针,指向父节点,这种表示方式称为三叉链表。例如在红黑树的插入和删除操作中,使用三叉链表可以简化向上调整的过程。

2.2 数组存储(适用于完全二叉树)

数组存储利用二叉树节点编号与数组下标的对应关系。对于完全二叉树,这种存储方式空间利用率高且访问效率好。

# 数组表示完全二叉树 # 节点 i 的左孩子下标: 2*i + 1 # 节点 i 的右孩子下标: 2*i + 2 # 节点 i 的父节点下标: (i-1) // 2 # 示例: 完全二叉树 [1, 2, 3, 4, 5, 6, 7] # 1 # / \ # 2 3 # / \ / \ # 4 5 6 7 tree = [1, 2, 3, 4, 5, 6, 7] def left_child(index): child = 2 * index + 1 return child if child < len(tree) else -1 def right_child(index): child = 2 * index + 2 return child if child < len(tree) else -1 def parent(index): return (index - 1) // 2 if index > 0 else -1

数组存储的局限性:对于退化成链表的斜树,数组存储会浪费大量空间。例如一棵深度为 k 的右斜树,需要 2k - 1 个数组位置,实际只存储了 k 个节点,空间浪费极大。因此数组存储仅适用于完全二叉树或接近完全二叉树的场景(如堆、线段树)。

三、二叉树的遍历

二叉树的遍历是指按照某种规则访问树中所有节点各一次的过程。根据访问根节点的顺序不同,分为前序、中序、后序三种深度优先遍历(DFS),以及宽度优先的层序遍历(BFS)。

以下列二叉树为例,分别演示四种遍历结果:

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, 6BFS

3.1 前序遍历(Preorder Traversal)

前序遍历首先访问根节点,然后递归遍历左子树,最后递归遍历右子树。这种遍历方式在复制一棵树或输出树的结构化表示时非常有用。

# 前序遍历 - 递归实现 def preorder_recursive(root): result = [] def dfs(node): if not node: return result.append(node.val) # 根 dfs(node.left) # 左 dfs(node.right) # 右 dfs(root) return result # 前序遍历 - 迭代实现(显式栈) def preorder_iterative(root): if not root: return [] result, stack = [], [root] while stack: node = stack.pop() result.append(node.val) # 注意:栈是后进先出,先压右子树再压左子树 if node.right: stack.append(node.right) if node.left: stack.append(node.left) return result

3.2 中序遍历(Inorder Traversal)

中序遍历先递归遍历左子树,然后访问根节点,最后递归遍历右子树。二叉搜索树的中序遍历结果是一个递增的有序序列,这是中序遍历最重要的性质。

# 中序遍历 - 递归实现 def inorder_recursive(root): result = [] def dfs(node): if not node: return dfs(node.left) # 左 result.append(node.val) # 根 dfs(node.right) # 右 dfs(root) return result # 中序遍历 - 迭代实现 def inorder_iterative(root): result, stack = [], [] curr = root while curr or stack: while curr: stack.append(curr) curr = curr.left # 一路向左 curr = stack.pop() result.append(curr.val) # 访问节点 curr = curr.right # 转向右子树 return result

3.3 后序遍历(Postorder Traversal)

后序遍历先递归遍历左子树,然后递归遍历右子树,最后访问根节点。后序遍历在删除二叉树或计算以节点为根的子树大小时非常有用。

# 后序遍历 - 递归实现 def postorder_recursive(root): result = [] def dfs(node): if not node: return dfs(node.left) # 左 dfs(node.right) # 右 result.append(node.val) # 根 dfs(root) return result # 后序遍历 - 迭代实现(双栈法) def postorder_iterative(root): if not root: return [] result, stack = [], [root] while stack: node = stack.pop() result.append(node.val) # 按根右左的顺序入结果 if node.left: # 先压左,再压右 stack.append(node.left) if node.right: stack.append(node.right) return result[::-1] # 反转得左右根 # 后序遍历 - 迭代实现(单栈标记法) def postorder_single_stack(root): result, stack = [], [] curr, last_visited = root, None while curr or stack: while curr: stack.append(curr) curr = curr.left curr = stack[-1] if not curr.right or curr.right == last_visited: result.append(curr.val) last_visited = curr stack.pop() curr = None else: curr = curr.right return result

3.4 层序遍历(Level Order Traversal)

层序遍历按从上到下、从左到右的顺序逐层访问节点,本质上是广度优先搜索(BFS)在二叉树上的应用。层序遍历可以方便地求解树宽、判断完全二叉树、进行序列化等。

# 层序遍历 - 队列实现 from collections import deque def level_order(root): if not root: return [] result, queue = [], deque([root]) while queue: level_size = len(queue) level = [] for _ in range(level_size): node = queue.popleft() level.append(node.val) if node.left: queue.append(node.left) if node.right: queue.append(node.right) result.append(level) # 二维数组,每层一组 return result # 示例输出(以上图二叉树为例) # [[1], [2, 3], [4, 5, 6]] # 层序遍历 - 简化版(一维输出) def level_order_flat(root): if not root: return [] result, queue = [], deque([root]) while queue: node = queue.popleft() result.append(node.val) if node.left: queue.append(node.left) if node.right: queue.append(node.right) return result # [1, 2, 3, 4, 5, 6]

遍历口诀:前序遍历:根左右;中序遍历:左根右;后序遍历:左右根。牢记这三句话,递归遍历的代码结构就掌握了。三种遍历的区别仅在于 result.append(node.val) 这一行的位置不同。

四、递归与迭代的对比分析

对比维度递归实现迭代实现
代码简洁性非常简洁,与数学定义一致相对复杂,需要手动管理栈
空间复杂度O(h),h为树高(系统调用栈)O(h),h为树高(显式栈)
栈溢出风险树高过大时存在可控制,通常无溢出
性能开销函数调用开销大仅循环操作,效率更高
可读性优秀,符合递归思维一般,需理解栈的变化过程

选择建议:面试和工程中,优先使用递归实现,代码简洁且不易出错。当树深度可能超过1000层(Python默认递归深度限制约1000)时,改用迭代实现或设置 sys.setrecursionlimit()。

4.1 递归的深层理解

递归遍历的核心思想是分治法:将二叉树问题分解为"访问根"和"递归处理左右子树"两个子问题。每个子问题的解法和原问题相同,只是规模更小。递归的终止条件是遇到空节点(None),此时直接返回。

递归函数的执行过程可以用调用栈(Call Stack)来理解。每次递归调用都会在系统栈上压入一个新的栈帧,包含局部变量和返回地址。当当前调用返回时,栈帧弹出,程序回到上一层调用点继续执行。

# 理解递归调用栈 - 以前序遍历为例 # 对二叉树 [1, 2, 3] 执行 preorder_recursive(1) # # 调用栈变化: # dfs(1) -> 输出1 -> dfs(2) -> 输出2 -> dfs(None)返回 -> dfs(None)返回 # -> 回到 dfs(1) -> dfs(3) -> 输出3 -> dfs(None)返回 -> dfs(None)返回 # -> 回到 dfs(1) -> 结束 # # 结果: [1, 2, 3]

4.2 迭代的核心思想

迭代遍历本质上是手动模拟递归调用栈的行为。我们使用一个显式的栈来替代系统调用栈。理解迭代遍历的关键在于理解节点的入栈和出栈时机:

五、Morris遍历 —— 常数空间的遍历算法

Morris遍历由 J. H. Morris 在1979年提出,其核心思想是利用叶子节点的空闲指针(空指针)来建立临时的回溯路径,从而在不使用额外栈或队列的情况下完成树的遍历。Morris遍历的空间复杂度为 O(1),时间复杂度仍为 O(n)。

Morris遍历的原理:在当前节点的左子树中找到最右节点(即中序遍历的前驱节点),将其右指针指向当前节点,从而建立一条从叶子节点回溯到祖先节点的临时路径。遍历完成后恢复原始树结构。

5.1 Morris中序遍历

# Morris中序遍历 - 空间复杂度 O(1) def morris_inorder(root): result = [] curr = root while curr: if not curr.left: # 左子树为空,直接访问当前节点 result.append(curr.val) curr = curr.right else: # 找到当前节点的前驱节点(左子树的最右节点) predecessor = curr.left while predecessor.right and predecessor.right != curr: predecessor = predecessor.right if not predecessor.right: # 建立线索:前驱节点的右指针指向当前节点 predecessor.right = curr curr = curr.left else: # 恢复树结构:断开线索 predecessor.right = None result.append(curr.val) curr = curr.right return result

5.2 Morris前序遍历

# Morris前序遍历 def morris_preorder(root): result = [] curr = root while curr: if not curr.left: result.append(curr.val) curr = curr.right else: predecessor = curr.left while predecessor.right and predecessor.right != curr: predecessor = predecessor.right if not predecessor.right: # 前序在建立线索时访问节点(根之前) result.append(curr.val) predecessor.right = curr curr = curr.left else: predecessor.right = None curr = curr.right return result

Morris遍历的优缺点:优点在于空间复杂度仅为 O(1),适合内存受限的场景(如嵌入式系统、大文件处理)。缺点在于遍历过程中会临时修改树的结构(虽然后续会恢复),不适合并发环境;且代码实现不如递归直观,理解和调试成本较高。在工程实践中,Morris遍历更多地作为一种算法思维的体现,实际应用不如递归和迭代广泛。

六、时间复杂度与空间复杂度分析

遍历方法时间复杂度空间复杂度说明
递归遍历(前/中/后序)O(n)O(h)h 为树高,最坏 O(n)(斜树),平均 O(log n)
迭代遍历(前/中/后序)O(n)O(h)显式栈,空间开销与递归类似但可控
层序遍历(队列)O(n)O(w)w 为最大宽度,最坏 O(n)(完全二叉树底层)
Morris遍历O(n)O(1)常数空间,但每个节点最多被访问两次

时间复杂度分析:每种遍历方法都恰好访问每个节点一次,所以时间复杂度均为 O(n),其中 n 是二叉树中的节点总数。Morris遍历中每个节点最多被访问两次(一次建立线索,一次访问),因此常数项稍大,但渐近复杂度仍然是 O(n)。

空间复杂度分析:

七、基于遍历的经典应用

7.1 二叉树的序列化与反序列化

序列化将二叉树转换为字符串表示,以便于存储和传输;反序列化则从字符串重建二叉树。前序遍历+标记空节点是最常用的序列化方案。

# 二叉树的序列化 - 前序遍历实现 def serialize(root): def dfs(node, sb): if not node: sb.append("None") return sb.append(str(node.val)) dfs(node.left, sb) dfs(node.right, sb) result = [] dfs(root, result) return ",".join(result) # 反序列化 def deserialize(data): def dfs(it): val = next(it) if val == "None": return None node = TreeNode(int(val)) node.left = dfs(it) node.right = dfs(it) return node return dfs(iter(data.split(",")))

7.2 求二叉树的最大深度

# 递归法 - 后序遍历思想 def max_depth(root): if not root: return 0 left_depth = max_depth(root.left) right_depth = max_depth(root.right) return max(left_depth, right_depth) + 1 # 迭代法 - 层序遍历思想 def max_depth_bfs(root): if not root: return 0 depth, queue = 0, deque([root]) while queue: depth += 1 for _ in range(len(queue)): node = queue.popleft() if node.left: queue.append(node.left) if node.right: queue.append(node.right) return depth

7.3 判断平衡二叉树

# 判断平衡二叉树 - 后序遍历 + 提前剪枝 def is_balanced(root): def check(node): if not node: return 0 left = check(node.left) if left == -1: return -1 right = check(node.right) if right == -1: return -1 if abs(left - right) > 1: return -1 return max(left, right) + 1 return check(root) != -1

7.4 从前序与中序遍历序列构造二叉树

# 经典面试题:从前序和中序遍历重建二叉树 def build_tree(preorder, inorder): if not preorder or not inorder: return None # 前序第一个节点为根 root_val = preorder[0] root = TreeNode(root_val) # 在中序中找到根的位置 root_idx = inorder.index(root_val) # 递归构建左右子树 root.left = build_tree(preorder[1:1 + root_idx], inorder[:root_idx]) root.right = build_tree(preorder[1 + root_idx:], inorder[root_idx + 1:]) return root

7.5 判断对称二叉树

# 判断二叉树是否对称(镜像对称) def is_symmetric(root): def check(left, right): if not left and not right: return True if not left or not right: return False return (left.val == right.val and check(left.left, right.right) and check(left.right, right.left)) return check(root.left, root.right) if root else True # 层序判断法 def is_symmetric_bfs(root): if not root: return True queue = deque([root.left, root.right]) while queue: t1 = queue.popleft() t2 = queue.popleft() if not t1 and not t2: continue if not t1 or not t2: return False if t1.val != t2.val: return False queue.append(t1.left) queue.append(t2.right) queue.append(t1.right) queue.append(t2.left) return True

八、二叉树的常见类别

类别定义特点应用
满二叉树(Full Binary Tree)每个节点要么是叶子,要么有两个子节点没有度为1的节点表达式树、霍夫曼树
完全二叉树(Complete Binary Tree)除最后一层外每层满,最后一层节点靠左排列可用数组高效存储,节点编号满足 2i 和 2i+1堆(Heap)、线段树
完美二叉树(Perfect Binary Tree)所有叶子在同一层,所有内部节点度均为2节点数 = 2h+1 - 1,最对称的二叉树理论分析中的最优情况
平衡二叉树(Balanced Binary Tree)任意节点的左右子树高度差不超过1AVL树、红黑树均为平衡二叉树的变种高效查找、数据库索引
二叉搜索树(BST)左子树所有节点 < 根 < 右子树所有节点中序遍历为升序序列,支持 O(h) 的查找有序数据存储、查找表
完美二叉树(高度h=2,节点数=7): 1 / \ 2 3 / \ / \ 4 5 6 7 完全二叉树(非完美): 1 / \ 2 3 / \ 4 5 满二叉树(非完全): 1 / \ 2 3 / \ 4 5 斜树(最不平衡): 1 \ 2 \ 3

类别关系:完美二叉树一定是完全二叉树和满二叉树,但反之不成立。完全二叉树不一定是满二叉树(可以有度为1的节点,且必须靠左)。满二叉树也不一定是完全二叉树(节点可以靠右)。平衡二叉树关注的是树的高度平衡性,与节点填充方式无关。

九、核心要点总结

一、二叉树基础

二叉树是每个节点最多有两个子节点的树形结构。关键术语包括根节点、叶子节点、深度(从根向下)、高度(从叶向上)。二叉树具有丰富的数学性质,如层节点数最大值、总节点数最大值、叶子与度为2节点的关系等。

二、二叉树的表示

链式存储使用节点类(val, left, right),是通用表示法。数组存储利用完全二叉树的编号规律(左孩子2i+1,右孩子2i+2),空间效率高但仅适用于完全二叉树。三叉链表增加parent指针,便于回溯。

三、四种遍历方式

前序(根左右)、中序(左根右)、后序(左右根)、层序(BFS)。前序常用于序列化和树结构复制;中序在BST中产生有序序列;后序用于树删除和子树计算;层序用于求宽度和层级相关的问题。

四、递归与迭代

递归代码简洁但受系统栈深度限制。迭代使用显式栈模拟递归过程,更可控但代码复杂。前序迭代最简单(根先入栈,右左入栈),中序迭代需"一路向左"后再弹出,后序迭代最复杂(双栈或标记法)。

五、Morris遍历

利用叶子节点的空闲指针建立临时回溯路径,实现O(1)空间复杂度的遍历。核心操作是寻找前驱节点并建立/断开线索。中序和前序的Morris实现相对简单,后序的Morris实现较复杂。适合内存受限场景。

六、复杂度总结

所有遍历方法时间复杂度均为O(n)。递归和迭代的空间复杂度为O(h),层序为O(w),Morris为O(1)。n为节点数,h为树高,w为最大宽度。斜树下h=O(n),完全二叉树下h=O(log n)。

七、经典应用

遍历思想广泛应用于二叉树相关问题:序列化/反序列化(前序+None标记)、求最大深度(后序或层序)、判断平衡二叉树(后序+剪枝)、从前序中序重建二叉树、判断对称二叉树(递归检查镜像位置)、BST验证(中序单调递增)等。

八、二叉树类别

满二叉树(无一度节点)、完全二叉树(末层靠左排列)、完美二叉树(全满最对称)、平衡二叉树(高度平衡)、二叉搜索树(有序中序)。理解这些类别及其关系,有助于选择合适的树结构解决实际问题。

十、进一步思考与实践

掌握了二叉树的基础遍历之后,可以进一步探索以下进阶主题:

练习建议:在 LeetCode 上系统地练习二叉树题目,推荐按以下顺序:先做 144(前序)、94(中序)、145(后序)、102(层序)四道遍历基础题,然后做 104(最大深度)、110(平衡二叉树)、101(对称二叉树)、105(重建二叉树)、297(序列化),最后挑战 124(二叉树最大路径和)和 236(最近公共祖先)等进阶题目。遍历是二叉树所有问题的基础,熟练掌握遍历就等于掌握了解决二叉树问题的钥匙。