首页/新闻资讯/正文详情

堆排序图解:厘清算法堆与内存堆的区别

发布时间:2026/9/25 20:07:38 来源:云帆数科 栏目:资讯中心
堆排序图解:厘清算法堆与内存堆的区别
1. 为什么堆排序总被说“难懂”先拆掉那个“堆”的心理门槛很多人第一次看到“堆排序”三个字脑子里立刻浮现出编译器报错里那行刺眼的“java.lang.OutOfMemoryError: Java heap space”或者调试时在IDE里点开“Variables”面板却找不到某个变量——下意识觉得“堆是不是和内存堆、JVM堆、PyTorch里的小土堆是同一个东西”结果越查越乱越学越懵。其实这是典型的概念混淆陷阱算法里的“堆”Heap和内存管理中的“堆区”heap memory同名不同物就像“苹果手机”和“苹果树上的苹果”一样只是碰巧用了同一个词。前者是一种完全二叉树的逻辑结构后者是操作系统分配的一块动态内存区域。它们唯一的共同点大概就是都长得有点“堆”——上小下大、层层叠叠。我带过不少刚转算法的同学发现他们卡在第一步不是代码写不出来而是根本没建立起“堆”这个数据结构的空间直觉。教科书上画个三角形树图再标几个数字大家点头说“哦明白了”可一到手写调整过程就容易把父节点和子节点索引算反或者搞不清“下沉”sift-down到底是往哪沉。这背后其实是缺乏一个可触摸、可回溯、可暂停的视觉化路径。比如你让一个人徒手把一摞乱序的书按高度排成“金字塔形”——最上面是最高那本下面一层放次高的两本再下面一层放再次高的四本……这个过程就是堆的构建而每次把塔尖最高那本拿走再把最后一本挪上来、从上往下逐层比较调整就是堆排序的核心动作。它不神秘它就是用树形规则约束数组的一种聪明办法。所以这篇图解我们彻底抛开“完全二叉树”“满二叉树”这些术语包袱直接用数组下标箭头连线颜色分层的方式带你一帧一帧看清每一步发生了什么。你会看到为什么索引i的左孩子永远是2i1右孩子是2i2不是2i和2i1这是初学者最常踩的坑为什么建堆要从最后一个非叶子节点开始“下沉”而不是从根节点开始因为叶子节点天生满足堆性质没必要动为什么排序阶段要把堆顶元素和末尾交换而不是和开头交换交换后未排序部分长度减一已排序部分自然“沉底”。这些不是死记硬背的公式而是由数组存储特性倒推出来的必然选择。当你真正理解了“为什么必须这样”堆排序就从一道算法题变成了一种清晰、可控、甚至有点优雅的思维工具。接下来我们就用一张张手绘级示意图把整个过程摊开在你眼前。2. 堆的本质不是树是“带规则的数组”很多人学堆排序第一反应是去画一棵树。但你要记住堆在计算机里从来不是真的存成一棵树它只是一维数组。所谓“堆结构”是程序员用一套数学规则强行赋予这个数组一种“树形解读方式”。这种解读方式就是堆的灵魂。我们以一个具体例子切入数组[4, 10, 3, 5, 1, 8, 2]。现在请暂时忘掉“堆”这个词只把它看作7个数字排成一排。我们的目标是让它满足“大根堆”的条件每个节点的值都大于或等于它的两个子节点的值。注意这里说的“节点”、“子节点”不是物理存在的而是我们约定俗成的映射关系对于数组中任意位置i从0开始计数它的父节点在位置floor((i-1)/2)它的左子节点在位置2*i 1它的右子节点在位置2*i 2。这个公式不是凭空来的它源于完全二叉树的层序遍历特性。想象一下你把这7个数字从上到下、从左到右一层一层地填进一棵二叉树第一层填1个索引0第二层填2个索引1、2第三层填4个索引3、4、5、6——刚好填满。这时索引0就是根索引1和2就是它的左右孩子索引3和4是索引1的孩子索引5和6是索引2的孩子。你拿纸笔画一画就会发现索引1的左孩子是3右孩子是4 →2*113,2*124索引2的左孩子是5右孩子是6 →2*215,2*226索引0的左孩子是1右孩子是2 →2*011,2*022。所以2i1和2i2是层序编号的数学必然结果不是魔法口诀。理解了这一点你就不会再纠结“为什么不是2i和2i1”——因为那样编号就不是层序遍历了树的结构就乱了。再来看“大根堆”的约束。对[4, 10, 3, 5, 1, 8, 2]我们检查每个非叶子节点即有孩子的节点是否满足“大于等于孩子”索引0值4左孩子索引1值10→4 10不满足索引1值10左孩子索引3值5、右孩子索引4值1→10 5且10 1满足索引2值3左孩子索引5值8、右孩子索引6值2→3 8不满足。所以这个数组目前不是大根堆。问题出在索引0和索引2。但注意我们不能只修这两个点因为修索引2值3时如果把它和索引5值8交换索引2变成8但索引5变成3这时索引1值10的右孩子原索引2现为8依然小于它自己没问题可索引5新值3现在成了叶子节点不用管。但索引0的问题更复杂因为它影响整棵树的顶端。这就是为什么堆排序要分两步先建堆make heap再排序sort。建堆的目标是让整个数组满足堆性质排序的目标是利用堆顶永远是最大值这一特性一次次把最大值“摘”出来放到数组末尾。整个过程所有操作都在原数组上进行零额外空间开销——这也是堆排序被称为“原地排序”的原因。提示小根堆的规则完全对称只是把“大于等于”换成“小于等于”。实际应用中大根堆更常见因为排序升序时我们习惯把最大值先排到后面符合人类阅读顺序从小到大。3. 建堆实战从最后一个非叶子节点开始“下沉”建堆是堆排序里最容易被误解的环节。很多教程说“从根节点开始调整”结果学员照着做发现调完根节点下面的子树又乱了只好反复循环效率极低。真相是建堆必须从最后一个非叶子节点逆序向上调整。为什么因为叶子节点没有孩子天然满足堆性质无需处理而从下往上调整能保证每次调整完一个节点它和它下面的子树就构成了一个局部有效的堆。我们继续用[4, 10, 3, 5, 1, 8, 2]这个数组。首先确定最后一个非叶子节点的位置。数组长度n 7最后一个节点索引是6它的父节点就是最后一个非叶子节点。父节点索引 floor((6-1)/2) floor(5/2) 2。所以我们要从索引2开始依次处理索引2、索引1、索引0。3.1 处理索引2值为3当前状态索引: 0 1 2 3 4 5 6 值: 4 10 3 5 1 8 2索引2的值是3它的左孩子是索引5值8右孩子是索引6值2。比较三者3, 8, 2最大值是8在左孩子位置。所以把索引2和索引5的值交换索引: 0 1 2 3 4 5 6 值: 4 10 8 5 1 3 2交换后索引2变成8满足“大于等于孩子”8 3 且 8 2。但注意索引5现在是3它成了叶子节点不用再看。这一步结束。3.2 处理索引1值为10当前状态索引: 0 1 2 3 4 5 6 值: 4 10 8 5 1 3 2索引1的值是10左孩子索引3值5右孩子索引4值1。10 5且10 1已经满足大根堆性质。无需交换直接跳过。这是关键不是每个节点都要动只动那些不满足条件的。3.3 处理索引0值为4当前状态索引: 0 1 2 3 4 5 6 值: 4 10 8 5 1 3 2索引0的值是4左孩子索引1值10右孩子索引2值8。三者中最大值是10在左孩子位置。交换索引0和索引1索引: 0 1 2 3 4 5 6 值: 10 4 8 5 1 3 2现在索引0是10满足条件。但索引1变成了4而它的孩子是索引35和索引41。4 5不满足所以交换还没完要继续对索引1“下沉”。把索引1和它的较大孩子索引3值5交换索引: 0 1 2 3 4 5 6 值: 10 5 8 4 1 3 2现在索引1是5孩子是索引34和索引415 4且5 1满足。索引3变成4是叶子节点停止。至此整个数组变成索引: 0 1 2 3 4 5 6 值: 10 5 8 4 1 3 2验证一下索引010孩子15、28→10 5且10 8✓索引15孩子34、41→5 4且5 1✓索引28孩子53、62→8 3且8 2✓完美大根堆建成了。整个过程只做了3次比较、3次交换其中一次触发了二次下沉非常高效。如果你从根节点开始会发现索引0一动下面全乱得反复扫好几遍时间复杂度就失控了。注意建堆的时间复杂度是 O(n)不是直觉上的 O(n log n)。这是因为大部分节点都在底层它们的“下沉”路径很短。数学证明涉及等比数列求和但实操中你只需记住从下往上建堆是经过严格优化的最优策略。4. 排序执行交换、缩小范围、再下沉三步循环建好堆只是完成了热身。真正的排序是从这个“顶部最大”的堆里把最大值一个个“请”出来放到数组的末尾。这个过程核心就三步循环执行直到整个数组有序交换把堆顶索引0的元素和当前未排序部分的最后一个元素交换缩小未排序部分长度减一即堆的“有效长度”减一下沉对新的堆顶仍是索引0但数组范围变小了执行一次“下沉”操作恢复堆性质。关键在于为什么是和“最后一个”交换而不是第一个因为我们要升序排列。升序意味着最小的在前最大的在后。堆顶永远是当前最大值所以把它放到末尾就相当于把它“归位”了。下次再找最大值就在剩下的n-1个数里找以此类推。如果和开头交换最大值就跑到前面去了反而打乱了顺序。我们接着上面建好的堆[10, 5, 8, 4, 1, 3, 2]开始排序。此时整个数组都是未排序的有效长度heapSize 7。4.1 第一轮摘出最大值10交换索引010 ↔ 索引62[2, 5, 8, 4, 1, 3, 10]缩小heapSize 6索引0~5是新的堆索引6的10已就位下沉对新堆顶索引0值2下沉。它的孩子是索引15和索引28最大值8在索引2。交换索引0和索引2[8, 5, 2, 4, 1, 3, 10]现在索引0是8孩子索引15、索引22✓。但索引2变成2它的孩子是索引53和索引610——等等索引6现在是已排序区不算所以索引22只有左孩子索引532 3需继续下沉。交换索引2和索引5[8, 5, 3, 4, 1, 2, 10]索引2变成3孩子索引52和索引610——索引6仍不算所以只看索引523 2满足。最终[8, 5, 3, 4, 1, 2, 10]4.2 第二轮摘出次大值8交换索引08 ↔ 索引52[2, 5, 3, 4, 1, 8, 10]缩小heapSize 5索引0~4是堆索引5、6已就位下沉索引02孩子索引15、索引23最大值5在索引1。交换[5, 2, 3, 4, 1, 8, 10]索引1变成2孩子索引34、索引412 4交换索引1和索引3[5, 4, 3, 2, 1, 8, 10]索引3变成2是叶子节点停。堆恢复[5, 4, 3, 2, 1]。4.3 后续轮次与最终结果继续这个循环第三轮交换索引05↔索引41→[1, 4, 3, 2, 5, 8, 10]下沉后[4, 2, 3, 1, 5, 8, 10]第四轮交换索引04↔索引31→[1, 2, 3, 4, 5, 8, 10]下沉后[3, 2, 1, 4, 5, 8, 10]第五轮交换索引03↔索引21→[1, 2, 3, 4, 5, 8, 10]下沉后[2, 1, 3, 4, 5, 8, 10]第六轮交换索引02↔索引11→[1, 2, 3, 4, 5, 8, 10]下沉后[1, 2, 3, 4, 5, 8, 10]。最终数组变为[1, 2, 3, 4, 5, 8, 10]升序完成。整个过程没有开辟任何新数组所有操作都在原地进行空间复杂度稳定为 O(1)。而时间复杂度建堆 O(n)排序 O(n log n)总体 O(n log n)和快排、归并齐平但胜在空间无敌。实操心得在写代码实现时我习惯把“下沉”封装成一个独立函数siftDown(arr, start, end)其中start是要下沉的节点索引end是当前堆的右边界即heapSize-1。这样逻辑清晰调试时可以单独测试下沉功能避免和交换逻辑耦合。5. 手写代码与避坑指南Python实现及5个致命细节理论讲透现在落地到代码。下面是一个精简、可读、无冗余的Python实现每一行都对应前面图解的逻辑def heap_sort(arr): n len(arr) # 步骤1建堆。从最后一个非叶子节点开始逆序向上 # 最后一个非叶子节点索引 (n // 2) - 1 for i in range(n // 2 - 1, -1, -1): sift_down(arr, i, n - 1) # 步骤2排序。每次把堆顶和末尾交换然后对剩余部分下沉 for i in range(n - 1, 0, -1): arr[0], arr[i] arr[i], arr[0] # 交换 sift_down(arr, 0, i - 1) # 对新堆顶下沉范围是0到i-1 return arr def sift_down(arr, start, end): 将start位置的元素向下调整使其在arr[start:end1]范围内满足大根堆 root start while True: # 计算左孩子索引 child root * 2 1 # 如果左孩子超出范围说明root是叶子停止 if child end: break # 如果右孩子存在且比左孩子大则选右孩子 if child 1 end and arr[child] arr[child 1]: child 1 # 如果root比孩子都小则交换并继续下沉 if arr[root] arr[child]: arr[root], arr[child] arr[child], arr[root] root child # 更新root继续下沉 else: break # 满足堆性质退出这段代码看似简单但藏着5个新手必踩的坑我挨个说透5.1 坑1建堆起始索引算错——(n//2)-1不是n//2很多教程写for i in range(n//2, -1, -1)这是错的当n7时n//2 3range(3, -1, -1)会遍历3,2,1,0。但索引3是叶子节点它的孩子是7和8超出数组不该处理。正确起始是(n//2)-1 2。通用公式最后一个非叶子节点索引 floor((n-2)/2) (n//2)-1整数除法下成立。你可以用n1,2,3...代入验证只有(n//2)-1永远正确。5.2 坑2下沉时右孩子判断条件漏了child 1 end在sift_down函数里判断右孩子是否存在必须写if child 1 end。如果只写if child 1 end或if child 1 len(arr)-1在end刚好是数组末尾时会出错。end就是当前堆的右边界所以右孩子索引child1必须 end才合法。5.3 坑3交换后忘记更新root导致无限循环sift_down里一旦发生交换root必须更新为child否则while True会一直用旧的root计算陷入死循环。这是最隐蔽的bug程序会卡住不动debug时很难发现。5.4 坑4排序循环的end范围写成i而不是i-1在主循环for i in range(n-1, 0, -1)中交换后调用sift_down(arr, 0, i-1)。因为i是当前要放最大值的位置交换后新的堆范围是0到i-1。如果写成sift_down(arr, 0, i)就会把刚放好的最大值也纳入堆范围导致它又被“沉”下去排序失败。5.5 坑5误以为堆排序稳定——它其实是不稳定排序稳定性指相等元素的相对位置不变。堆排序中交换操作如堆顶和末尾交换会跨距离移动元素完全可能打乱相等元素的顺序。例如[5a, 5b, 1]a、b表示相同值但不同实例建堆后可能是[5b, 5a, 1]第一次交换后变成[1, 5a, 5b]5a和5b顺序就反了。所以如果业务要求稳定排序如数据库多字段排序堆排序不能直接用得选归并或插入。最后一个小技巧如果你想快速验证自己写的堆排序是否正确不要只测[3,1,4,1,5]这种小数组。我习惯用list(range(1000, 0, -1))逆序1000个数来测因为这是堆排序最差情况建堆工作量最大能暴露性能问题再用random.shuffle(list(range(1000)))测平均情况。跑通这两个基本就没问题了。

相关推荐

大唐杯备赛捷径:300道模拟题圈定5G核心考点
大唐杯备赛捷径:300道模拟题圈定5G核心考点

简介:《“大唐杯”全国大学生移动通信5G技术大赛模拟题库》面向该赛事的参赛学生与指导教师,覆盖第七届、第八届、第九届及历届考点,适合通信类专业备考与赛前强化。资源以判断题和选择题为主,内容涵盖eMBB、uRLLC、mMTC三大应用场… · 2026/9/25 20:07:32

学生成绩管理数据库设计:从ER图到MySQL实现全攻略
学生成绩管理数据库设计:从ER图到MySQL实现全攻略

简介:这是一份用于数据库实验大作业的学生成绩管理数据库系统设计文档,基于MySQL/SQL Server,完整描述了需求分析、系统功能框架、运行环境、用户权限与功能分解等核心内容。文档按管理员、教师和学生三类角色划分功能模块,涵盖信… · 2026/9/25 20:07:32

DeskcommCRM自建部署实战:从选型到落地的客户关系管理全指南
DeskcommCRM自建部署实战:从选型到落地的客户关系管理全指南

1. 为什么我最终选了 DeskcommCRM 这套方案做销售管理这行的人应该都有同感:客户信息散落在微信聊天、Excel 表格、笔记本甚至脑子里,每次想梳理跟进进度都像在拼图。我去年帮一家 30 人左右的贸易公司梳理销售流程时,他们最痛的点就是客户资… · 2026/9/25 20:07:25

大数据架构深度解析—Flink CDC 实战:MySQL 到数据仓库的实时同步完整方案
大数据架构深度解析—Flink CDC 实战:MySQL 到数据仓库的实时同步完整方案

一、为什么是 Flink CDC 数据同步的老三样: 方案问题定时 sqoop 全量抽取T1,时效性差,大表慢Canal Kafka 手写消费链路长,要维护 MQ,代码多双写(业务代码同时写库和数仓)侵入业务&#xff0… · 2026/9/25 20:38:42

2026年精选最值得推荐的5款降AIGC工具
2026年精选最值得推荐的5款降AIGC工具

2026 年毕业季临近,各大高校对论文 AIGC 检测的审查标准愈发严格。面对市面上种类繁多的降 AI 工具,许多同学开始困惑:到底该选哪个才靠谱?我花了两周时间,对当前市面主流的 5 款降 AI 工具进行了全面测试,… · 2026/9/25 20:38:35

CARLA多服务器仿真部署指南:架构、调度与避坑实践
CARLA多服务器仿真部署指南:架构、调度与避坑实践

简介:毕业设计聚焦多服务器环境下CARLA仿真系统的搭建与调度,适合自动驾驶、智能网联方向的学生及需要完成期末大作业或毕设项目的开发者。资源共251个文件,涵盖Python核心脚本、txt说明文档、地图与路网相关的sumocfg/xodr配置文件、XML/YAM… · 2026/9/25 20:38:35

论文AI率0%黑科技!降AIGC网站留学生亲测:Turnitin查重从“高危红”秒变“安全蓝”
论文AI率0%黑科技!降AIGC网站留学生亲测:Turnitin查重从“高危红”秒变“安全蓝”

写论文用AI确实省心又高效,特别是面对海量资料和紧迫时间的时候,简直像开挂一样。但别高兴太早,现在越来越多高校开始严查AI痕迹,Turnitin之类的检测系统越来越灵敏,一旦被发现内容是AI生成的,轻则被打回重… · 2026/9/25 20:38:29

B树如何优化磁盘IO:从原理到工程实践全解析
B树如何优化磁盘IO:从原理到工程实践全解析

前几天处理一份数据库慢查询时,系统日志里蹦出一条磁盘IO重试记录:逻辑块地址0x11d40360处重试IO操作。那条查询走的是主键索引,理论上是三层B树,最多三次磁盘IO就能拿到数据,实际却卡了快两秒。问题最后查出来出在硬件… · 2026/9/25 20:38:29

2026年10款靠谱降AI率平台推荐:论文AIGC检测轻松过关
2026年10款靠谱降AI率平台推荐:论文AIGC检测轻松过关

随着知网、维普、万方等主流学术平台对AIGC检测标准不断升级,论文通过率面临严峻挑战。选择一款高效的降AI工具已成为关键突破口。本文将实测对比10款主流工具,为读者提供精准的解决方案参考。为什么需要降 AI 率工具? 2026 年,各… · 2026/9/25 20:38:29

数值优化(Numerical Optimization)学习系列-03-共轭梯度方法(Conjugate Gradient)
数值优化(Numerical Optimization)学习系列-03-共轭梯度方法(Conjugate Gradient)

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/25 1:00:31

创维E900V22D刷机全攻略:S905L3SB芯片兼容性解析与救砖实战
创维E900V22D刷机全攻略:S905L3SB芯片兼容性解析与救砖实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/25 1:00:31

MQTT协议原理与Broker服务器搭建实战:从Mosquitto到EMQX
MQTT协议原理与Broker服务器搭建实战:从Mosquitto到EMQX

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/25 1:00:37

了解更多?预约专属演示

我们的顾问将为您一对一讲解产品与方案

企业微信二维码