在前五篇内容中,我们学习的线性结构(数组、栈、队列、链表)和哈希表,要么以“线性排列”存储数据,要么通过“键值映射”快速定位,而今天要接触的二叉树(Binary Tree),是第一种真正意义上的“非线性结构”。它以“层级化”方式组织数据,每个节点最多包含两个子节点(左子树与右子树),这种结构天然适配层级化场景(如文件系统、组织结构图),也是后续学习二叉搜索树、堆、红黑树等高级树形结构的核心基础。
一、二叉树的核心概念:理解“层级化”结构本质
要掌握二叉树,首先需要明确其定义、核心组成与分类,这是后续实现和操作的基础,避免因概念混淆导致代码逻辑偏差。
1. 基本定义与核心组成
二叉树是由“节点(Node)”构成的树形结构,严格遵循两个规则:
- 每个节点最多拥有两个子节点,分别称为左子节点(Left Child) 和右子节点(Right Child),子节点的左右顺序不可随意交换(例如“左2右3”与“左3右2”是不同的二叉树);
- 存在唯一的根节点(Root),即树的顶层节点,没有父节点;所有非根节点都有且仅有一个父节点。
二叉树的关键术语(后续操作会频繁用到):
- 叶子节点(Leaf):没有任何子节点的节点(左、右子节点均为
None
); - 子树(Subtree):以某个节点为根的局部树结构(如根节点的左子节点及其所有后代构成“左子树”);
- 深度(Depth):从根节点到当前节点的路径长度(通常定义根节点深度为0,其子节点深度为1,以此类推);
- 高度(Height):从当前节点到最远叶子节点的路径长度(叶子节点高度为0,其父节点高度为1,以此类推);
- 节点的度(Degree):节点拥有的子节点数量(二叉树中节点的度只能是0、1或2)。
2. 常见二叉树分类(重点区分满二叉树与完全二叉树)
根据节点分布规律,二叉树可分为以下几类,其中“完全二叉树”是后续学习堆的关键前提:
- 满二叉树(Full Binary Tree):除叶子节点外,所有节点的度均为2(即每个非叶子节点都有且仅有两个子节点),例如深度为2的满二叉树共有7个节点(1个根、2个中层、4个叶子);
- 完全二叉树(Complete Binary Tree):叶子节点仅分布在最后两层,且最后一层的节点从左到右连续排列,中间不允许有空缺(适合用数组存储,堆就是典型的完全二叉树);
- 斜树(Skewed Tree):所有节点都只有左子节点(左斜树)或只有右子节点(右斜树),本质上退化为链表,失去树形结构“高效遍历”的优势;
- 平衡二叉树(Balanced Binary Tree):任意节点的左、右子树高度差的绝对值不超过1(后续二叉搜索树会详细讲解,用于避免斜树问题)。
二、二叉树的Python实现:从节点类到树形结构构建
二叉树的实现核心是“节点类”——每个节点封装“数据域”和“两个子节点引用”,再通过根节点串联起整个树结构,操作逻辑清晰易懂。
1. 节点类定义(基础组件)
class TreeNode:
def __init__(self, data):
self.data = data # 节点的数据域(存储具体值,如整数、字符串)
self.left = None # 左子节点引用(默认None,表示无左子节点)
self.right = None # 右子节点引用(默认None,表示无右子节点)
def __str__(self):
# 自定义打印格式,方便调试时查看节点数据
return f"TreeNode(data={self.data})"
2. 手动构建一棵示例二叉树
通过创建节点并关联左、右子节点,可构建任意结构的二叉树。以下是一棵符合“完全二叉树”特征的示例树,后续操作均基于此树展开:
# 构建目标二叉树结构:
# 1(根节点,深度0,高度2)
# / \
# 2 3(深度1,2的高度1,3的高度0)
# / \
# 4 5(深度2,均为叶子节点,高度0)
# 第一步:创建所有节点
root = TreeNode(1) # 根节点
node2 = TreeNode(2) # 根节点的左子节点
node3 = TreeNode(3) # 根节点的右子节点
node4 = TreeNode(4) # 节点2的左子节点
node5 = TreeNode(5) # 节点2的右子节点
# 第二步:关联子节点,形成树形结构
root.left = node2 # 根节点的左子树指向node2
root.right = node3 # 根节点的右子树指向node3
node2.left = node4 # 节点2的左子树指向node4
node2.right = node5 # 节点2的右子树指向node5
# 验证结构(通过根节点遍历子节点)
print("根节点的左子节点:", root.left) # 输出:TreeNode(data=2)
print("节点2的右子节点:", node2.right) # 输出:TreeNode(data=5)
print("节点3的左子节点(叶子节点):", node3.left) # 输出:None
三、二叉树的核心操作:遍历(前序、中序、后序、层序)
遍历是二叉树最基础也最重要的操作,指“按特定顺序访问树中所有节点,且每个节点仅访问一次”。根据访问逻辑不同,分为深度优先遍历(DFS) 和广度优先遍历(BFS) 两大类,每种遍历方式对应不同的应用场景。
1. 深度优先遍历(DFS):深入子树,回溯访问
DFS的核心思路是“优先沿一条路径深入到叶子节点,再回溯访问其他分支”,通过递归或栈实现。根据“根节点的访问时机”不同,分为前序、中序、后序三种遍历方式:
- 前序遍历(Pre-order):根节点 → 左子树 → 右子树(先访问根,再递归遍历左右子树,适合“复制二叉树”“获取树的前缀表达式”);
- 中序遍历(In-order):左子树 → 根节点 → 右子树(先递归遍历左子树,再访问根,最后递归遍历右子树,二叉搜索树的中序遍历结果为升序,适合“有序输出”);
- 后序遍历(Post-order):左子树 → 右子树 → 根节点(先递归遍历左右子树,最后访问根,适合“计算树的高度”“删除二叉树”“表达式树求值”)。
(1)递归实现(简洁直观,适合小规模树)
递归是实现DFS最简洁的方式,利用Python的函数调用栈自动处理回溯逻辑,代码可读性高:
def pre_order_recursive(node, result=None):
"""前序遍历:递归实现,返回节点数据列表"""
if result is None:
result = [] # 初始化结果列表(避免递归时重复创建)
if node is not None:
result.append(node.data) # 1. 访问根节点
pre_order_recursive(node.left, result) # 2. 递归遍历左子树
pre_order_recursive(node.right, result) # 3. 递归遍历右子树
return result
def in_order_recursive(node, result=None):
"""中序遍历:递归实现,返回节点数据列表"""
if result is None:
result = []
if node is not None:
in_order_recursive(node.left, result) # 1. 递归遍历左子树
result.append(node.data) # 2. 访问根节点
in_order_recursive(node.right, result) # 3. 递归遍历右子树
return result
def post_order_recursive(node, result=None):
"""后序遍历:递归实现,返回节点数据列表"""
if result is None:
result = []
if node is not None:
post_order_recursive(node.left, result) # 1. 递归遍历左子树
post_order_recursive(node.right, result) # 2. 递归遍历右子树
result.append(node.data) # 3. 访问根节点
return result
# 测试递归遍历(基于上文构建的二叉树)
print("前序遍历(递归):", pre_order_recursive(root)) # 输出:[1, 2, 4, 5, 3]
print("中序遍历(递归):", in_order_recursive(root)) # 输出:[4, 2, 5, 1, 3]
print("后序遍历(递归):", post_order_recursive(root)) # 输出:[4, 5, 2, 3, 1]
(2)迭代实现(手动模拟栈,避免递归深度问题)
当二叉树深度较大(如超过1000层)时,递归会触发Python的“递归深度限制”(默认约1000),导致RecursionError
。此时需用栈手动模拟递归过程,控制回溯逻辑:
def pre_order_iterative(node):
"""前序遍历:迭代实现(用栈模拟)"""
result = []
if node is None:
return result
stack = [node] # 栈初始化,先压入根节点(栈遵循LIFO,确保根节点先访问)
while stack:
current_node = stack.pop() # 弹出栈顶节点(访问根)
result.append(current_node.data)
# 关键:右子节点先压栈,左子节点后压栈(确保左子树先于右子树访问)
if current_node.right is not None:
stack.append(current_node.right)
if current_node.left is not None:
stack.append(current_node.left)
return result
def in_order_iterative(node):
"""中序遍历:迭代实现(用栈模拟)"""
result = []
stack = []
current_node = node
while current_node is not None or stack:
# 第一步:遍历到左子树最底层,所有左节点依次压栈
while current_node is not None:
stack.append(current_node)
current_node = current_node.left
# 第二步:弹出栈顶节点(左子树遍历完,访问根)
current_node = stack.pop()
result.append(current_node.data)
# 第三步:遍历右子树(重复上述过程)
current_node = current_node.right
return result
def post_order_iterative(node):
"""后序遍历:迭代实现(用栈+访问标记,避免重复处理)"""
result = []
if node is None:
return result
# 栈元素为元组:(节点, 是否已访问),False表示未访问(需先处理子树)
stack = [(node, False)]
while stack:
current_node, is_visited = stack.pop()
if is_visited:
# 已访问:直接添加到结果(后序遍历最后访问根)
result.append(current_node.data)
else:
# 未访问:按“根→右→左”压栈(弹出时顺序为“左→右→根”)
stack.append((current_node, True)) # 标记为已访问,后续弹出时处理
if current_node.right is not None:
stack.append((current_node.right, False))
if current_node.left is not None:
stack.append((current_node.left, False))
return result
# 测试迭代遍历(结果与递归一致,验证正确性)
print("前序遍历(迭代):", pre_order_iterative(root)) # 输出:[1, 2, 4, 5, 3]
print("中序遍历(迭代):", in_order_iterative(root)) # 输出:[4, 2, 5, 1, 3]
print("后序遍历(迭代):", post_order_iterative(root)) # 输出:[4, 5, 2, 3, 1]
2. 广度优先遍历(BFS):按层级访问,逐层扩散
BFS又称“层序遍历”,核心思路是“按节点深度从浅到深访问,先访问完当前层所有节点,再进入下一层”。由于需要维护“层级顺序”,需用队列(FIFO特性)实现,适合“获取树的层数”“判断是否为完全二叉树”“最短路径问题”(如无权图的最短路径)。
层序遍历实现(迭代+队列)
from collections import deque # 用deque实现队列,popleft()操作时间复杂度为O(1)
def level_order_traversal(node):
"""层序遍历:迭代实现,返回按层级分组的节点数据列表(如[[1], [2,3], [4,5]])"""
result = []
if node is None:
return result
queue = deque([node]) # 队列初始化,先压入根节点
while queue:
level_size = len(queue) # 当前层的节点数量(关键:控制每层遍历范围)
current_level = [] # 存储当前层的节点数据
# 遍历当前层所有节点
for _ in range(level_size):
current_node = queue.popleft() # 弹出队头节点(FIFO,保证层级顺序)
current_level.append(current_node.data)
# 左子节点入队(下一层节点)
if current_node.left is not None:
queue.append(current_node.left)
# 右子节点入队(下一层节点)
if current_node.right is not None:
queue.append(current_node.right)
result.append(current_level) # 将当前层结果加入总结果
return result
# 测试层序遍历(上文二叉树共3层,结果按层分组)
print("层序遍历:", level_order_traversal(root)) # 输出:[[1], [2, 3], [4, 5]]
四、二叉树的常用扩展操作(基于遍历的逻辑延伸)
除了基础遍历,二叉树还有“计算高度”“统计节点数”“判断完全二叉树”等常用操作,这些操作本质上是遍历逻辑的扩展,可直接复用遍历框架。
1. 计算二叉树的高度
二叉树的高度是“根节点到最远叶子节点的路径长度”,可通过后序遍历实现(先计算左、右子树高度,再取最大值加1):
def get_tree_height(node):
"""计算二叉树的高度(递归实现):空树高度为-1,叶子节点高度为0"""
if node is None:
return -1 # 空树高度定义为-1,确保叶子节点高度为0(1 + (-1) = 0)
# 递归计算左、右子树高度
left_height = get_tree_height(node.left)
right_height = get_tree_height(node.right)
# 当前节点高度 = 1 + 子树最大高度
return 1 + max(left_height, right_height)
# 测试树高度(上文二叉树的高度为2:根节点1→节点2→节点4/5,共3层,高度=3-1=2)
print("二叉树高度:", get_tree_height(root)) # 输出:2
2. 统计二叉树的节点总数
节点总数 = 1(当前节点) + 左子树节点数 + 右子树节点数,递归实现逻辑简单:
def count_total_nodes(node):
"""统计二叉树的节点总数(递归实现)"""
if node is None:
return 0 # 空树节点数为0
# 当前节点数(1) + 左子树节点数 + 右子树节点数
return 1 + count_total_nodes(node.left) + count_total_nodes(node.right)
# 测试节点总数(上文二叉树共5个节点:1、2、3、4、5)
print("二叉树节点总数:", count_total_nodes(root)) # 输出:5
3. 判断是否为完全二叉树
完全二叉树的核心特征是“最后一层节点从左到右连续,且前面所有层均为满二叉树”,需通过层序遍历验证:
- 按层序遍历所有节点,遇到第一个“空节点”后,后续所有节点必须都是空节点;
- 若遇到“非空节点”但前面已有空节点,则不是完全二叉树。
def is_complete_binary_tree(node):
"""判断二叉树是否为完全二叉树(层序遍历实现)"""
if node is None:
return True # 空树视为完全二叉树
queue = deque([node])
has_empty_node = False # 标记是否已遇到空节点
while queue:
current_node = queue.popleft()
if current_node is None:
has_empty_node = True # 遇到空节点,标记状态
else:
if has_empty_node:
# 已出现空节点,又遇到非空节点 → 不符合完全二叉树特征
return False
# 非空节点:将左右子节点入队(即使为None也要入队,用于后续判断)
queue.append(current_node.left)
queue.append(current_node.right)
return True # 遍历结束未发现异常,是完全二叉树
# 测试完全二叉树判断
print("是否为完全二叉树(示例树):", is_complete_binary_tree(root)) # 输出:True
# 构建一棵非完全二叉树(节点3的左子节点为空,右子节点非空)
node3.left = None
node3.right = TreeNode(6)
print("是否为完全二叉树(修改后):", is_complete_binary_tree(root)) # 输出:False
五、二叉树的经典应用场景
二叉树的“层级化”和“递归结构”特性,使其在多个领域中发挥核心作用,以下是3个典型应用:
1. 表达式树(解析与求值)
数学表达式(如(3+4)*5-6
)可转化为二叉树结构(称为“表达式树”),其中:
- 叶子节点为操作数(如3、4、5、6);
- 非叶子节点为运算符(如+、*、-);
- 通过后序遍历可直接计算表达式结果(先算左右子树,再用根节点运算符计算)。
def evaluate_expression_tree(node):
"""计算表达式树的值(后序遍历)"""
if node is None:
return 0
# 叶子节点:直接返回操作数
if node.left is None and node.right is None:
return node.data
# 非叶子节点:递归计算左右子树,再应用当前运算符
left_val = evaluate_expression_tree(node.left)
right_val = evaluate_expression_tree(node.right)
# 根据运算符计算结果
if node.data == '+':
return left_val + right_val
elif node.data == '-':
return left_val - right_val
elif node.data == '*':
return left_val * right_val
elif node.data == '/':
return left_val // right_val # 简化处理:整数除法
else:
raise ValueError(f"不支持的运算符:{node.data}")
# 构建表达式树:(3+4)*5 → 对应后序遍历:3 4 + 5 *
# *
# / \
# + 5
# / \
# 3 4
expr_root = TreeNode('*')
expr_root.left = TreeNode('+')
expr_root.right = TreeNode(5)
expr_root.left.left = TreeNode(3)
expr_root.left.right = TreeNode(4)
print("表达式树计算结果:", evaluate_expression_tree(expr_root)) # 输出:35((3+4)*5=35)
2. 二叉树的序列化与反序列化(数据存储与传输)
在网络传输或文件存储中,需将二叉树转为字符串(序列化),使用时再恢复为树形结构(反序列化),层序遍历是常用方案(保留空节点信息,确保结构唯一)。
def serialize_tree(node):
"""二叉树序列化(层序遍历):将树转为字符串,空节点用'None'表示"""
if node is None:
return "[]"
result = []
queue = deque([node])
while queue:
current_node = queue.popleft()
if current_node is not None:
result.append(str(current_node.data))
queue.append(current_node.left)
queue.append(current_node.right)
else:
result.append("None")
# 移除末尾连续的'None'(优化存储,不影响反序列化)
while result[-1] == "None":
result.pop()
return f"[{','.join(result)}]"
def deserialize_tree(data):
"""二叉树反序列化:将字符串恢复为树形结构"""
if data == "[]":
return None
# 解析字符串为数据列表
values = data[1:-1].split(',') # 去除首尾'[]',按','分割
root = TreeNode(int(values[0])) # 第一个元素为根节点
queue = deque([root])
index = 1 # 从第二个元素开始处理
while queue and index < len(values):
current_node = queue.popleft()
# 处理左子节点
if index < len(values) and values[index] != "None":
current_node.left = TreeNode(int(values[index]))
queue.append(current_node.left)
index += 1
# 处理右子节点
if index < len(values) and values[index] != "None":
current_node.right = TreeNode(int(values[index]))
queue.append(current_node.right)
index += 1
return root
# 测试序列化与反序列化
serialized = serialize_tree(root)
print("序列化结果:", serialized) # 输出:[1,2,3,4,5,6](基于修改后的树)
deserialized_root = deserialize_tree(serialized)
print("反序列化后层序遍历:", level_order_traversal(deserialized_root)) # 输出:[[1], [2,3], [4,5,6]]
3. Huffman树(数据压缩算法)
Huffman树是一种带权路径长度最短的二叉树,广泛用于数据压缩(如ZIP、PNG格式):
- 权值越大的叶子节点(如高频字符)离根节点越近,编码越短;
- 权值越小的叶子节点(如低频字符)离根节点越远,编码越长;
- 通过前缀编码(左0右1)实现数据压缩,无歧义解码。
六、二叉树的优缺点与使用建议
1. 优点
- 层级化存储:天然适配具有层级关系的数据(如组织结构、文件系统),比线性结构更直观;
- 灵活的遍历方式:支持前序、中序、后序、层序等多种遍历,可按需获取不同顺序的节点数据;
- 高效的局部操作:在平衡状态下,插入、删除、查找的平均时间复杂度为O(log n),优于线性结构(O(n));
- 递归友好:树形结构与递归逻辑高度契合,代码实现简洁(如遍历、计算高度等操作)。
2. 缺点
- 非平衡风险:若二叉树退化为斜树(如所有节点只有右子树),操作复杂度会退化为O(n),失去树形结构优势;
- 空间开销:每个节点需存储左、右子节点引用,空间开销高于数组;
- 随机访问困难:无法像数组那样通过索引直接访问节点,需通过遍历查找,不适合高频随机访问场景。
3. 使用建议
- 优先选择二叉树的场景:
- 数据具有层级关系(如组织架构、XML/HTML解析);
- 需要多种遍历方式(如表达式解析、有序输出);
- 需在O(log n)时间内完成插入、删除、查找(需配合平衡机制,如后续学习的二叉搜索树)。
- 谨慎使用二叉树的场景:
- 数据为线性结构且需高频随机访问(如用户ID与信息映射),优先选择数组或哈希表;
- 内存资源受限且数据无层级关系,优先选择数组或链表。
七、总结
二叉树是第一种非线性结构,通过“每个节点最多两个子节点”的规则构建层级化存储,核心操作是前序、中序、后序(深度优先)和层序(广度优先)遍历,这些遍历逻辑是实现其他扩展操作(如计算高度、统计节点)的基础。
二叉树的优势在于层级化存储和灵活遍历,适合处理具有层级关系的数据,但需注意“非平衡退化”风险。下一篇,我们将学习“二叉搜索树(BST)”——它在二叉树基础上增加了“左子树节点值≤根节点值≤右子树节点值”的规则,实现了O(log n)的高效查找、插入和删除,是数据库索引、有序集合等场景的核心结构。