数据结构(三):从二叉树到排序算法的深度实践

一、二叉树与哈夫曼树:层次化数据的高效处理

(一)二叉树:基础数据结构的核心
  1. 核心概念:二叉树是每个节点最多拥有两个子节点的树形结构,子节点区分为左子节点和右子节点,这种特性使其能高效表达层次化数据关系,广泛应用于文件系统、表达式解析等场景。

  2. 关键性质

    • 在二叉树的第i层,最多存在2i−1个节点(i≥1);
    • 深度为k的二叉树,节点总数最多为2k−1个;
    • 对任意二叉树,度为0的节点(叶子节点)数量比度为2的节点数量多1。
  3. 存储实现

    • 顺序存储:适用于完全二叉树,按层序依次将节点存入数组。非完全二叉树需通过补空节点的方式转化为完全二叉树,会存在一定空间浪费。
    • 链式存储:通过节点结构体存储数据及左右子节点指针,灵活适配任意形态的二叉树,是二叉树最常用的存储方式。
  4. 代码实现

    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])

    arr = [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.left = TreeNode(6)

(二)哈夫曼树:数据压缩的黄金工具
  1. 核心原理:哈夫曼树是一种带权路径长度最短的二叉树,核心目标是实现最优数据压缩。其构建过程遵循贪心策略,每次从节点集合中选取权值最小的两个节点,合并为新节点,新节点权值为二者之和,重复此过程直至只剩一个根节点。

  2. 关键指标:带权路径长度(WPL)是哈夫曼树的核心指标,计算方式为所有叶子节点的权值与该节点到根节点路径长度的乘积之和,WPL越小,压缩效果越好。

  3. 哈夫曼编码:基于哈夫曼树生成的编码方案,核心规则为左子树路径编码为0、右子树路径编码为1,每个字符的编码为其从根到叶子节点的路径序列。这种编码方案具有前缀编码特性,即任意字符的编码都不是其他字符编码的前缀,避免解码歧义,广泛应用于文件压缩(如ZIP、GZIP)。

  4. 代码实现

    import heapq

    class HuffmanNode:
    def init(self, val=0, left=None, right=None):
    self.val = val
    self.left = left
    self.right = right

    复制代码
     # 重写比较方法,用于优先队列排序
     def __lt__(self, other):
         return self.val < other.val

    def build_huffman_tree(weights):
    """构建哈夫曼树"""
    if not weights:
    return None
    # 创建优先队列(小顶堆)
    heap = [HuffmanNode(val=w) for w in weights]
    heapq.heapify(heap)

    复制代码
     while len(heap) > 1:
         # 取出权值最小的两个节点
         left = heapq.heappop(heap)
         right = heapq.heappop(heap)
         # 合并为新节点,权值为二者之和
         new_node = HuffmanNode(val=left.val + right.val, left=left, right=right)
         heapq.heappush(heap, new_node)
     
     return heap[0]

    def generate_huffman_codes(root):
    """生成哈夫曼编码"""
    codes = {}
    def dfs(node, code):
    if not node.left and not node.right:
    codes[node.val] = code
    return
    if node.left:
    dfs(node.left, code + "0")
    if node.right:
    dfs(node.right, code + "1")
    dfs(root, "")
    return codes

    示例:字符权值集合

    weights = [5, 9, 12, 13, 20]
    huffman_tree = build_huffman_tree(weights)
    huffman_codes = generate_huffman_codes(huffman_tree)
    print("哈夫曼编码:", huffman_codes)

二、二叉树遍历:解锁二叉树数据的核心路径

二叉树遍历是访问二叉树所有节点的核心操作,根据访问顺序不同,分为深度优先遍历(前序、中序、后序)和广度优先遍历(层序)两类,不同遍历方式适用于不同场景。

(一)深度优先遍历(递归与非递归)
  1. 前序遍历:访问顺序为根节点→左子树→右子树,常用于复制二叉树、前缀表达式生成。

  2. 中序遍历:访问顺序为左子树→根节点→右子树,对于二叉搜索树,中序遍历结果为升序序列,是二叉搜索树排序的核心逻辑。

  3. 后序遍历:访问顺序为左子树→右子树→根节点,常用于计算树的节点总数、释放二叉树内存。

  4. 递归实现:逻辑简洁,直接遵循遍历顺序递归调用函数,核心是终止条件(节点为空时返回)。

  5. 非递归实现:借助栈模拟递归过程,手动控制节点访问和入栈出栈顺序,避免递归的栈溢出风险,适用于深度极大的二叉树。

  6. 代码实现

    复制代码
    class TreeNode:
        def __init__(self, val=0, left=None, right=None):
            self.val = val
            self.left = left
            self.right = right
    
    # 递归遍历
    def preorder_recursive(root):
        """前序遍历(递归)"""
        if not root:
            return []
        return [root.val] + preorder_recursive(root.left) + preorder_recursive(root.right)
    
    def inorder_recursive(root):
        """中序遍历(递归)"""
        if not root:
            return []
        return inorder_recursive(root.left) + [root.val] + inorder_recursive(root.right)
    
    def postorder_recursive(root):
        """后序遍历(递归)"""
        if not root:
            return []
        return postorder_recursive(root.left) + postorder_recursive(root.right) + [root.val]
    
    # 非递归遍历
    def preorder_non_recursive(root):
        """前序遍历(非递归)"""
        if not root:
            return []
        stack = [root]
        result = []
        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
    
    def inorder_non_recursive(root):
        """中序遍历(非递归)"""
        stack = []
        result = []
        node = root
        while stack or node:
            # 遍历左子树,将节点入栈
            while node:
                stack.append(node)
                node = node.left
            # 弹出栈顶节点,访问后转向右子树
            node = stack.pop()
            result.append(node.val)
            node = node.right
        return result
    
    # 测试
    root = TreeNode(1)
    root.left = TreeNode(2)
    root.right = TreeNode(3)
    root.left.left = TreeNode(4)
    root.left.right = TreeNode(5)
    
    print("前序遍历(递归):", preorder_recursive(root))
    print("前序遍历(非递归):", preorder_non_recursive(root))
    print("中序遍历(递归):", inorder_recursive(root))
    print("中序遍历(非递归):", inorder_non_recursive(root))
(二)广度优先遍历(层序遍历)
  1. 核心逻辑:借助队列实现,按层依次访问二叉树节点,先访问根节点,再访问根的子节点,接着访问子节点的子节点,以此类推,确保同一层的节点按从左到右的顺序访问。

  2. 适用场景:计算二叉树的深度、宽度,判断二叉树是否为完全二叉树,层序输出二叉树节点等。

  3. 代码实现

    from collections import deque

    def levelorder_traversal(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

    测试

    print("层序遍历:", levelorder_traversal(root))

三、经典排序算法:从基础到进阶的效率突破

排序是数据处理的核心操作,不同排序算法在时间复杂度、空间复杂度、稳定性上各有优劣,需根据数据规模、数据特征、性能要求选择合适的算法。

(一)冒泡排序:简单直观的交换排序
  1. 核心原理:通过相邻元素的比较与交换,将最大(或最小)元素逐步"冒泡"到数组的一端。每一轮遍历都会将当前未排序部分的最大元素放到正确位置,重复此过程直至数组有序。

  2. 时间复杂度:最好情况(数组已有序)为O(n)(可通过优化提前终止遍历实现);平均和最坏情况(数组逆序)为O(n2)。

  3. 空间复杂度:O(1),原地排序,无需额外空间。

  4. 稳定性:稳定,相同元素的相对位置不会因交换而改变。

  5. 代码实现

    def bubble_sort(arr):
    n = len(arr)
    for i in range(n-1):
    swapped = False
    # 每一轮将最大元素冒泡到末尾,未排序部分长度为n-i-1
    for j in range(n-1-i):
    if arr[j] > arr[j+1]:
    arr[j], arr[j+1] = arr[j+1], arr[j]
    swapped = True
    # 若本轮未发生交换,说明数组已有序,提前终止
    if not swapped:
    break
    return arr

    测试

    arr = [64, 34, 25, 12, 22, 11, 90]
    print("冒泡排序结果:", bubble_sort(arr))

(二)选择排序:交换次数最少的排序
  1. 核心原理:每一轮从未排序部分选择最小(或最大)元素,与未排序部分的第一个元素交换位置,逐步构建有序序列。无论数据是否有序,都需要完整遍历未排序部分找到极值。

  2. 时间复杂度:最好、平均、最坏情况均为O(n2),因为每次都需遍历未排序部分寻找极值。

  3. 空间复杂度:O(1),原地排序。

  4. 稳定性:不稳定,若存在多个相同极值,交换时可能改变其相对位置。

  5. 代码实现

    复制代码
    def selection_sort(arr):
        n = len(arr)
        for i in range(n-1):
            min_idx = i
            # 寻找未排序部分的最小元素下标
            for j in range(i+1, n):
                if arr[j] < arr[min_idx]:
                    min_idx = j
            # 将最小元素与未排序部分的第一个元素交换
            if min_idx != i:
                arr[i], arr[min_idx] = arr[min_idx], arr[i]
        return arr
    
    # 测试
    arr = [64, 34, 25, 12, 22, 11, 90]
    print("选择排序结果:", selection_sort(arr))
(三)插入排序:适合小规模数据的有序构建
  1. 核心原理:将数组分为已排序和未排序两部分,初始时已排序部分仅包含第一个元素。每一轮从未排序部分取出一个元素,插入到已排序部分的正确位置,逐步扩展有序序列,如同整理扑克牌。

  2. 时间复杂度:最好情况(数组已有序)为O(n);平均和最坏情况(数组逆序)为O(n2)。

  3. 空间复杂度:O(1),原地排序。

  4. 稳定性:稳定,插入过程中相同元素的相对位置保持不变。

  5. 代码实现

    def insertion_sort(arr):
    n = len(arr)
    for i in range(1, n):
    key = arr[i]
    j = i - 1
    # 将大于key的元素后移,为key腾出插入位置
    while j >= 0 and arr[j] > key:
    arr[j+1] = arr[j]
    j -= 1
    arr[j+1] = key
    return arr

    测试

    arr = [64, 34, 25, 12, 22, 11, 90]
    print("插入排序结果:", insertion_sort(arr))

(四)希尔排序:插入排序的进阶优化
  1. 核心原理:又称缩小增量排序,是插入排序的改进版。先按设定的增量序列将数组划分为多个子序列,对每个子序列进行插入排序;随后逐步缩小增量,重复子序列插入排序,直至增量为1时,对整个数组进行一次插入排序,此时数组已接近有序,插入排序效率极高。

  2. 增量序列:常用增量序列为希尔原始序列(⌊n/2⌋,⌊n/4⌋,...,1),增量的选择直接影响排序性能,优质增量序列可显著减少比较和移动次数。

  3. 时间复杂度:取决于增量序列,最好情况可达O(nlogn),平均情况优于O(n2),最坏情况仍为O(n2),但实际性能远优于简单插入排序。

  4. 空间复杂度:O(1),原地排序。

  5. 稳定性:不稳定,不同增量的子序列排序会打乱相同元素的相对位置。

  6. 代码实现

    def shell_sort(arr):
    n = len(arr)
    gap = n // 2
    # 不断缩小增量,直至增量为1
    while gap > 0:
    # 对每个子序列进行插入排序
    for i in range(gap, n):
    temp = arr[i]
    j = i
    # 子序列内的元素后移,找到temp的插入位置
    while j >= gap and arr[j - gap] > temp:
    arr[j] = arr[j - gap]
    j -= gap
    arr[j] = temp
    gap //= 2
    return arr

    测试

    arr = [64, 34, 25, 12, 22, 11, 90]
    print("希尔排序结果:", shell_sort(arr))

(五)基数排序:非比较型的高效排序
  1. 核心原理:基于数字的位数进行排序,属于非比较型排序。分为最低位优先(LSD)和最高位优先(MSD)两种方式,常用LSD。从数字的最低位开始,按当前位的数字将元素分配到0-9的桶中,再依次收集桶中元素;接着对次低位重复此操作,直至处理完最高位,最终数组有序。

  2. 适用场景:适用于整数、字符串等可按位拆分的数据,尤其适合数据范围大但位数少的场景(如手机号排序、身份证号排序),时间复杂度不受数据规模影响,仅与位数和基数相关。

  3. 时间复杂度:O(d×(n+k)),其中d为数字的最大位数,n为元素个数,k为基数(十进制为10)。

  4. 空间复杂度:O(n×k),需要额外的桶空间存储元素。

  5. 稳定性:稳定,相同位数的元素在桶内的相对顺序在收集时保持不变。

  6. 代码实现

    复制代码
    def radix_sort(arr):
        # 获取数组中最大元素的位数
        max_num = max(arr)
        digits = 0
        while max_num > 0:
            digits += 1
            max_num //= 10
        
        # 从最低位到最高位依次排序
        for digit in range(digits):
            # 创建10个桶(0-9)
            buckets = [[] for _ in range(10)]
            # 按当前位的数字分配元素到桶中
            for num in arr:
                # 计算当前位的数字
                current_digit = (num // (10 ** digit)) % 10
                buckets[current_digit].append(num)
            # 收集桶中元素,恢复数组
            arr = []
            for bucket in buckets:
                arr.extend(bucket)
        return arr
    
    # 测试
    arr = [170, 45, 75, 90, 802, 24, 2, 66]
    print("基数排序结果:", radix_sort(arr))

四、算法对比与场景选择

算法 时间复杂度(最好/平均/最坏) 空间复杂度 稳定性 核心优势 适用场景
冒泡排序 O(n)/O(n²)/O(n²) O(1) 稳定 逻辑简单,代码易实现 小规模数据、教学演示
选择排序 O(n²)/O(n²)/O(n²) O(1) 不稳定 交换次数少,适合写操作受限场景 小规模数据、交换成本高的场景
插入排序 O(n)/O(n²)/O(n²) O(1) 稳定 对小规模、部分有序数据高效 小规模数据、近乎有序的数据
希尔排序 O(n log n)/O(n^1.3)/O(n²) O(1) 不稳定 比简单插入排序高效,性能优 中等规模数据、对性能有要求的场景
基数排序 O(d(n+k))/O(d(n+k))/O(d(n+k)) O(n×k) 稳定 不受数据规模影响,效率极高 整数、字符串等按位拆分的数据,大范围数据排序

五、总结

本文系统解析了二叉树与哈夫曼树的结构特性、二叉树遍历的核心方法,以及五大经典排序算法的原理与实现。这些知识是数据结构与算法的核心基础,不仅在理论层面构建了数据处理的思维框架,更在实际开发中发挥着关键作用:

  • 二叉树与哈夫曼树为层次化数据存储和数据压缩提供了高效方案;
  • 二叉树遍历是操作二叉树数据的基础,是实现树相关算法的前提;
  • 五大排序算法各有优劣,需根据数据规模、数据特征、性能要求灵活选择,同时它们也是理解更复杂排序算法(如归并排序、快速排序、堆排序)的基础。

在实际开发中,建议结合具体场景深入实践,通过优化算法细节、对比性能差异,逐步提升算法设计与优化能力,为构建高效、稳定的程序奠定坚实基础。