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

数据结构(三):从二叉树到排序算法的深度实践 一、二叉树与哈夫曼树层次化数据的高效处理一二叉树基础数据结构的核心核心概念二叉树是每个节点最多拥有两个子节点的树形结构子节点区分为左子节点和右子节点这种特性使其能高效表达层次化数据关系广泛应用于文件系统、表达式解析等场景。关键性质在二叉树的第i层最多存在2i−1个节点i≥1深度为k的二叉树节点总数最多为2k−1个对任意二叉树度为0的节点叶子节点数量比度为2的节点数量多1。存储实现顺序存储适用于完全二叉树按层序依次将节点存入数组。非完全二叉树需通过补空节点的方式转化为完全二叉树会存在一定空间浪费。链式存储通过节点结构体存储数据及左右子节点指针灵活适配任意形态的二叉树是二叉树最常用的存储方式。代码实现class TreeNode: def __init__(self, val0, leftNone, rightNone): 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)二哈夫曼树数据压缩的黄金工具核心原理哈夫曼树是一种带权路径长度最短的二叉树核心目标是实现最优数据压缩。其构建过程遵循贪心策略每次从节点集合中选取权值最小的两个节点合并为新节点新节点权值为二者之和重复此过程直至只剩一个根节点。关键指标带权路径长度WPL是哈夫曼树的核心指标计算方式为所有叶子节点的权值与该节点到根节点路径长度的乘积之和WPL越小压缩效果越好。哈夫曼编码基于哈夫曼树生成的编码方案核心规则为左子树路径编码为0、右子树路径编码为1每个字符的编码为其从根到叶子节点的路径序列。这种编码方案具有前缀编码特性即任意字符的编码都不是其他字符编码的前缀避免解码歧义广泛应用于文件压缩如ZIP、GZIP。代码实现import heapq class HuffmanNode: def __init__(self, val0, leftNone, rightNone): 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(valw) for w in weights] heapq.heapify(heap) while len(heap) 1: # 取出权值最小的两个节点 left heapq.heappop(heap) right heapq.heappop(heap) # 合并为新节点权值为二者之和 new_node HuffmanNode(valleft.val right.val, leftleft, rightright) 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)二、二叉树遍历解锁二叉树数据的核心路径二叉树遍历是访问二叉树所有节点的核心操作根据访问顺序不同分为深度优先遍历前序、中序、后序和广度优先遍历层序两类不同遍历方式适用于不同场景。一深度优先遍历递归与非递归前序遍历访问顺序为根节点→左子树→右子树常用于复制二叉树、前缀表达式生成。中序遍历访问顺序为左子树→根节点→右子树对于二叉搜索树中序遍历结果为升序序列是二叉搜索树排序的核心逻辑。后序遍历访问顺序为左子树→右子树→根节点常用于计算树的节点总数、释放二叉树内存。递归实现逻辑简洁直接遵循遍历顺序递归调用函数核心是终止条件节点为空时返回。非递归实现借助栈模拟递归过程手动控制节点访问和入栈出栈顺序避免递归的栈溢出风险适用于深度极大的二叉树。代码实现class TreeNode: def __init__(self, val0, leftNone, rightNone): 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))二广度优先遍历层序遍历核心逻辑借助队列实现按层依次访问二叉树节点先访问根节点再访问根的子节点接着访问子节点的子节点以此类推确保同一层的节点按从左到右的顺序访问。适用场景计算二叉树的深度、宽度判断二叉树是否为完全二叉树层序输出二叉树节点等。代码实现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))三、经典排序算法从基础到进阶的效率突破排序是数据处理的核心操作不同排序算法在时间复杂度、空间复杂度、稳定性上各有优劣需根据数据规模、数据特征、性能要求选择合适的算法。一冒泡排序简单直观的交换排序核心原理通过相邻元素的比较与交换将最大或最小元素逐步“冒泡”到数组的一端。每一轮遍历都会将当前未排序部分的最大元素放到正确位置重复此过程直至数组有序。时间复杂度最好情况数组已有序为O(n)可通过优化提前终止遍历实现平均和最坏情况数组逆序为O(n2)。空间复杂度O(1)原地排序无需额外空间。稳定性稳定相同元素的相对位置不会因交换而改变。代码实现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[j1]: arr[j], arr[j1] arr[j1], arr[j] swapped True # 若本轮未发生交换说明数组已有序提前终止 if not swapped: break return arr # 测试 arr [64, 34, 25, 12, 22, 11, 90] print(冒泡排序结果, bubble_sort(arr))二选择排序交换次数最少的排序核心原理每一轮从未排序部分选择最小或最大元素与未排序部分的第一个元素交换位置逐步构建有序序列。无论数据是否有序都需要完整遍历未排序部分找到极值。时间复杂度最好、平均、最坏情况均为O(n2)因为每次都需遍历未排序部分寻找极值。空间复杂度O(1)原地排序。稳定性不稳定若存在多个相同极值交换时可能改变其相对位置。代码实现def selection_sort(arr): n len(arr) for i in range(n-1): min_idx i # 寻找未排序部分的最小元素下标 for j in range(i1, 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))三插入排序适合小规模数据的有序构建核心原理将数组分为已排序和未排序两部分初始时已排序部分仅包含第一个元素。每一轮从未排序部分取出一个元素插入到已排序部分的正确位置逐步扩展有序序列如同整理扑克牌。时间复杂度最好情况数组已有序为O(n)平均和最坏情况数组逆序为O(n2)。空间复杂度O(1)原地排序。稳定性稳定插入过程中相同元素的相对位置保持不变。代码实现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[j1] arr[j] j - 1 arr[j1] key return arr # 测试 arr [64, 34, 25, 12, 22, 11, 90] print(插入排序结果, insertion_sort(arr))四希尔排序插入排序的进阶优化核心原理又称缩小增量排序是插入排序的改进版。先按设定的增量序列将数组划分为多个子序列对每个子序列进行插入排序随后逐步缩小增量重复子序列插入排序直至增量为1时对整个数组进行一次插入排序此时数组已接近有序插入排序效率极高。增量序列常用增量序列为希尔原始序列⌊n/2⌋,⌊n/4⌋,...,1增量的选择直接影响排序性能优质增量序列可显著减少比较和移动次数。时间复杂度取决于增量序列最好情况可达O(nlogn)平均情况优于O(n2)最坏情况仍为O(n2)但实际性能远优于简单插入排序。空间复杂度O(1)原地排序。稳定性不稳定不同增量的子序列排序会打乱相同元素的相对位置。代码实现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))五基数排序非比较型的高效排序核心原理基于数字的位数进行排序属于非比较型排序。分为最低位优先LSD和最高位优先MSD两种方式常用LSD。从数字的最低位开始按当前位的数字将元素分配到0-9的桶中再依次收集桶中元素接着对次低位重复此操作直至处理完最高位最终数组有序。适用场景适用于整数、字符串等可按位拆分的数据尤其适合数据范围大但位数少的场景如手机号排序、身份证号排序时间复杂度不受数据规模影响仅与位数和基数相关。时间复杂度O(d×(nk))其中d为数字的最大位数n为元素个数k为基数十进制为10。空间复杂度O(n×k)需要额外的桶空间存储元素。稳定性稳定相同位数的元素在桶内的相对顺序在收集时保持不变。代码实现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(nk))/O(d(nk))/O(d(nk))O(n×k)稳定不受数据规模影响效率极高整数、字符串等按位拆分的数据大范围数据排序五、总结本文系统解析了二叉树与哈夫曼树的结构特性、二叉树遍历的核心方法以及五大经典排序算法的原理与实现。这些知识是数据结构与算法的核心基础不仅在理论层面构建了数据处理的思维框架更在实际开发中发挥着关键作用二叉树与哈夫曼树为层次化数据存储和数据压缩提供了高效方案二叉树遍历是操作二叉树数据的基础是实现树相关算法的前提五大排序算法各有优劣需根据数据规模、数据特征、性能要求灵活选择同时它们也是理解更复杂排序算法如归并排序、快速排序、堆排序的基础。在实际开发中建议结合具体场景深入实践通过优化算法细节、对比性能差异逐步提升算法设计与优化能力为构建高效、稳定的程序奠定坚实基础。