Python数据结构 (6):二叉树(Binary Tree)

在前五篇内容中,我们学习的线性结构(数组、栈、队列、链表)和哈希表,要么以“线性排列”存储数据,要么通过“键值映射”快速定位,而今天要接触的二叉树(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. 判断是否为完全二叉树

完全二叉树的核心特征是“最后一层节点从左到右连续,且前面所有层均为满二叉树”,需通过层序遍历验证:

  1. 按层序遍历所有节点,遇到第一个“空节点”后,后续所有节点必须都是空节点;
  2. 若遇到“非空节点”但前面已有空节点,则不是完全二叉树。
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. 使用建议

  • 优先选择二叉树的场景
    1. 数据具有层级关系(如组织架构、XML/HTML解析);
    2. 需要多种遍历方式(如表达式解析、有序输出);
    3. 需在O(log n)时间内完成插入、删除、查找(需配合平衡机制,如后续学习的二叉搜索树)。
  • 谨慎使用二叉树的场景
    1. 数据为线性结构且需高频随机访问(如用户ID与信息映射),优先选择数组或哈希表;
    2. 内存资源受限且数据无层级关系,优先选择数组或链表。

七、总结

二叉树是第一种非线性结构,通过“每个节点最多两个子节点”的规则构建层级化存储,核心操作是前序、中序、后序(深度优先)和层序(广度优先)遍历,这些遍历逻辑是实现其他扩展操作(如计算高度、统计节点)的基础。

二叉树的优势在于层级化存储和灵活遍历,适合处理具有层级关系的数据,但需注意“非平衡退化”风险。下一篇,我们将学习“二叉搜索树(BST)”——它在二叉树基础上增加了“左子树节点值≤根节点值≤右子树节点值”的规则,实现了O(log n)的高效查找、插入和删除,是数据库索引、有序集合等场景的核心结构。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值