当前位置: 首页 > 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.gsyq.cn/news/1597993.html

相关文章:

  • 瑞萨RA2L2开发板快速上手指南:从环境搭建到调试实战
  • 2026最新整理:AI自习室和普通自习室到底有哪些核心区别
  • 4G5G专题-109:实战 - 面向5G演进与多业务融合的室内分布式系统规划与设计
  • Vision Mamba:突破Transformer瓶颈,双向SSM重塑高分辨率视觉理解
  • VSCode中英等宽字体配置:从需求分析到Sarasa Mono SC实战
  • MySql 主从复制+读写分离
  • ncmdumpGUI终极教程:3分钟掌握网易云音乐NCM文件转换技巧
  • 33. 用 const、enum、inline 代替 #define
  • UART电平转换实战:从电阻分压到MOS管的五种电路设计详解
  • WooCommerce商城的安全性一定要重视起来
  • 【实践解析】DDRNet:面向实时道路场景解析的双分辨率网络架构与实现
  • Allegro高效设计:从零构建你的专属快捷键体系
  • Windows热键侦探:3步快速找出谁偷了你的快捷键
  • Fay数字人框架终极指南:5步实现智能代理的自主决策与主动交互
  • TVA 赋能智慧工厂的十大核心优势(4)
  • WELearn网课助手:告别熬夜刷题的3个实用技巧
  • 从特征工程到模型融合:Kaggle植物幼苗分类竞赛的机器学习实战解析
  • 【RuoYi-Vue-Plus】性能调优实践:从Druid迁移至HikariCP数据源
  • CH32V MCU IAP 进阶:利用函数指针与参数封装实现动态APP跳转
  • 模块五-生产环境中的RAG系统
  • InSAR干涉相位计算的核心:为何复数共轭相乘是唯一正解?
  • WinRAR ACE格式路径穿越漏洞CVE-2018-20250深度解析与复现
  • 抖音无水印下载神器:三分钟掌握批量视频保存的终极方案
  • ExplorerPatcher终极指南:如何彻底解决Windows资源管理器不稳定问题
  • Apache Shiro反序列化漏洞实战:从流量分析到防御加固
  • 开源开发工具生态构建:技术方案如何提升编程效率与开发体验
  • 模块四-LLM与文本生成
  • Apache APISIX高危漏洞CVE-2022-24112:从插件热加载到RCE的深度剖析与防御
  • 2026权威选型指南|主流AI编程助手深度横评,开发者精准适配方案
  • 【故障排查】浪潮服务器硬盘红灯长鸣:从RAID异常到Foreign配置导入的实战解析