当前位置: 首页 > news >正文

数据结构笔记——堆排序和归并排序

一、堆排序(Heap Sort)算法

堆排序基于完全二叉树 + 大顶堆实现,属于不稳定排序,平均 / 最好 / 最坏时间复杂度均为O(n log n),空间复杂度O(1),原地排序。 整体流程分为两大阶段:构建大顶堆+循环交换堆顶并调整堆

public class HeapSort { public static void main(String[] args) { int[] arr = {3, 1, 4, 2, 7, 5, 9, 6, 8}; heapSort(arr); System.out.print("堆排序结果:"); for (int num : arr) { System.out.print(num + " "); } } /** * 堆排序入口方法 * @param arr 待排序数组 */ public static void heapSort(int[] arr) { int len = arr.length; // 第一步:构建初始大顶堆,从最后一个非叶子节点向前调整 for (int i = len / 2 - 1; i >= 0; i--) { heapify(arr, i, len); } // 第二步:循环交换堆顶与堆底,缩小堆范围并重新调整堆 for (int i = len - 1; i > 0; i--) { // 交换堆顶最大值和当前堆末尾元素 int temp = arr[0]; arr[0] = arr[i]; arr[i] = temp; // 仅对根节点调整堆,有效长度缩小为i heapify(arr, 0, i); } } /** * 堆维护方法:以i为根,调整子树满足大顶堆规则 * @param arr 原数组 * @param root 当前根节点下标 * @param heapLen 当前堆有效长度 */ public static void heapify(int[] arr, int root, int heapLen) { while (true) { // 假设当前根是最大值下标 int maxIndex = root; // 左孩子下标 int left = 2 * root + 1; // 右孩子下标 int right = 2 * root + 2; // 左孩子存在且值更大,更新最大值下标 if (left < heapLen && arr[left] > arr[maxIndex]) { maxIndex = left; } // 右孩子存在且值更大,更新最大值下标 if (right < heapLen && arr[right] > arr[maxIndex]) { maxIndex = right; } // 根已经是最大值,无需调整,退出循环 if (maxIndex == root) { break; } // 交换根与最大值节点 int temp = arr[root]; arr[root] = arr[maxIndex]; arr[maxIndex] = temp; // 向下迭代,继续调整交换后的子树 root = maxIndex; } } }

1. 核心思路与数组下标映射

(1)大顶堆规则

完全二叉树结构,任意父节点数值 ≥ 左右子节点数值,堆顶是当前区间最大值。 堆排序整体两步:

  1. 对整个数组构建初始大顶堆;
  2. 将堆顶最大值与堆末尾元素交换,缩小有效堆长度,对剩余元素重新调整大顶堆,循环直到全部有序。

(2)数组下标映射(无需新建二叉树)

数组直接存储完全二叉树,通过下标换算父子节点:

  • 父节点下标:i
  • 左孩子下标:2 * i + 1
  • 右孩子下标:2 * i + 2

(3)堆维护 heapify 统一逻辑

堆调整方法heapify作用:保证以i为根的子树满足大顶堆规则。 两种场景共用同一个维护方法:

  1. 初始建堆:从最后一个非叶子节点倒序向前,逐个调用 heapify;
  2. 交换堆顶后调整:仅需对根节点下标 0 调用一次 heapify。

(4)heapify 调整逻辑

  1. 传入数组、当前父节点下标、堆有效长度;
  2. 循环查找父、左、右三者最大值下标;
  3. 若最大值不是父节点,交换父子元素;
  4. 把最大值下标作为新父节点,继续向下循环调整,直到满足大顶堆或超出边界。

2. 时间复杂度推导

  1. 单次heapify向下调整,遍历层数为树高度,复杂度O(log n)
  2. 初始建堆:遍历所有非叶子节点,总代价O(n)
  3. 交换 + 调整阶段:共 n 次循环,每次调用一次heapify,总代价O(n log n)
  4. 整体总时间复杂度:O(n log n),无论原始数组有序 / 逆序 / 乱序均稳定。
  5. 空间复杂度:原地排序,仅常数临时变量,O(1)

二、归并排序(Merge Sort)算法

归并排序采用分治思想,稳定排序,时间复杂度稳定O(n log n),缺点是需要额外临时数组,空间复杂度O(n)

/** * 合并左右两个有序区间 * @param arr 原数组 * @param left 左区间起始下标 * @param mid 左右区间分割点 * @param right 右区间结束下标 */ public static void merge(int[] arr, int left, int mid, int right) { // 右区间起始指针 int s2 = mid + 1; // 创建临时数组,存储本次合并后的有序数据,长度=当前区间元素总数 int[] temp = new int[right - left + 1]; // temp数组写入下标 int index = 0; // 1. 双指针循环:同时遍历左右有序区间,取较小值存入temp while (s1 <= mid && s2 <= right) { if (arr[s1] < arr[s2]) { temp[index] = arr[s1]; s1++; index++; } else { temp[index] = arr[s2]; s2++; index++; } } // 2. 左区间剩余元素全部拷贝进temp while (s1 <= mid) { temp[index] = arr[s1]; s1++; index++; } // 3. 右区间剩余元素全部拷贝进temp while (s2 <= right) { temp[index] = arr[s2]; s2++; index++; } // 4. 将临时有序数组写回原数组对应区间(关键步骤,防止数据丢失) for (int j = 0; j < temp.length; j++) { arr[left + j] = temp[j]; } }

1. 分治核心逻辑

(1)递归拆分策略

递归将数组以中点切分为左、右两个区间,持续拆分直到区间只剩单个元素(天然有序),再逐层向上合并有序区间。

  • 递归出口:left >= right,区间无元素或仅一个元素,无需处理;
  • 仅传递左右边界下标划分区间,不新建子数组,节约内存。

(2)双指针合并有序序列

有两个有序子区间时,借助临时数组完成合并:

  1. 双指针 S1、S2 分别指向左右有序区间起始;
  2. 对比两个指针指向元素,将更小的值存入临时数组,对应指针后移;
  3. 某一侧区间遍历完毕后,把另一侧剩余元素全部拷贝进临时数组;
  4. 合并完成后,将临时数组整体写回原数组[left, right]对应位置,防止原数据覆盖丢失。

2. 递归内存执行流程

  1. 递归拆分时,每层方法入栈保存 left、right、mid 参数;
  2. 递归到底触发出口后,逐层出栈执行合并逻辑;
  3. 每次合并会创建临时数组存放有序结果,属于堆内存;
  4. 合并完成将临时数组数据覆盖写回原数组,上层递归继续合并更大区间。

3. 复杂度总结

  • 时间复杂度:拆分层数log n,每层合并总遍历元素 n,稳定O(n log n)
  • 空间复杂度:需要辅助临时数组,O(n)
  • 特性:稳定排序,适合外排序(海量磁盘数据排序)。

三、堆排序 vs 归并排序

维度堆排序归并排序
时间复杂度稳定 O (n log n)稳定 O (n log n)
空间复杂度O (1) 原地排序O (n) 需要临时数组
排序稳定性不稳定稳定
适用场景内存内大数据排序、空间受限场景外排序、要求稳定排序场景
实现难点完全二叉树下标换算、堆调整递归分治、双指针合并、数组回写
http://www.jsqmd.com/news/1086045/

相关文章:

  • 从数据本质到代码实践:深度解析Arduino串口通信中Serial.print()与Serial.write()的底层逻辑与格式转换陷阱
  • 人工智能通识课程知识模块2:职业场景数据处理实操
  • 【组合数学】从二项式定理到帕斯卡三角:三大递推恒等式的直观证明与应用场景
  • 2026最新整理:AI自习室和普通自习室到底有哪些核心区别
  • CogVLM深度解析:多模态大模型的深度融合架构与工程实践
  • 镜子是门艺术:镜子,你知道哪些?
  • 从均匀到优先:经验回放采样策略的演进与高效实现
  • 软考证书加分真相全曝光,92%考生不知道的3个隐藏条件与2024年6省市实证数据
  • VSCode中英等宽字体配置:从需求分析到Sarasa Mono SC实战
  • 【MySQL】深入浅出MySQL索引特性:从磁盘I/O底层数据结构到实战调优
  • 4G5G专题-109:实战 - 面向5G演进与多业务融合的室内分布式系统规划与设计
  • Vision Mamba:突破Transformer瓶颈,双向SSM重塑高分辨率视觉理解
  • 如何快速提升AMD显卡性能:免费驱动精简终极指南
  • Key 的作用与原理
  • WAF绕过实战:帆软报表SSTI漏洞利用与防御解析
  • UART电平转换实战:从电阻分压到MOS管的五种电路设计详解
  • Webpack配置错,打包慢到哭!
  • LLM爬虫适配优化实践:基于GEO-AI架构的企业AI收录提升技术方案
  • 33. 用 const、enum、inline 代替 #define
  • Web自动化测试实战:从工具选型到CI/CD集成的完整指南
  • MySql 主从复制+读写分离
  • CoppeliaSim/V-REP全版本软件安装包:从官网到国内网盘的一站式获取指南
  • ncmdumpGUI终极教程:3分钟掌握网易云音乐NCM文件转换技巧
  • 从零到一:在Windows系统上部署gprMax3.0并完成首个B-scan仿真
  • Python 推导式全景解析:从语法核心到高性能实战
  • Ubuntu 22.04 触屏干扰排查指南:精准识别与禁用输入设备
  • 终极指南:如何在Windows/Linux上轻松下载官方macOS系统镜像
  • 从CSS Hack到优雅降级:Flex Gap Polyfill如何重塑前端布局兼容性策略
  • WooCommerce商城的安全性一定要重视起来
  • 口碑好的瓷砖供应商