未察凌云木本根,修枝剪叶自沉浮,终成玉宇最高层。
- 堆结构
- 堆排序
- 堆排序步骤
- 堆排序原理
- 堆排序代码实现
- 堆化(下滤)部分
- 堆排序部分
- 复杂度简要分析
- 例题
- 参考资料及推荐学习
- 总结
堆结构
- 堆的本质
完全二叉树:所有层除最后一层外都是满的,且最后一层节点靠左对齐。 - 数组存储:利用数组下标表示树结构:
父节点索引 i → 左子节点 2i+1,右子节点 2i+2
最后一个非叶子结点索引n/2-1,n为数组长度 - 大根堆:父节点值 ≥ 子节点值
- 堆的示例
假设数组 [3, 8, 5, 1, 7],对应的完全二叉树结构为:()内为数组索引,外为数组实际存储值
3(0)/ \8(1) 5(2)/ \1(3) 7(4)
构建大根堆后变为 [8,7,5,1,3],对应树结构:
8(0)/ \7(1) 5(2)/ \1(3) 3(4)
堆排序
堆排序步骤
步骤 | 操作 | 目的 |
---|---|---|
1 | 构建大根堆 | 使数组满足堆性质,根节点为最大值 |
2 | 交换堆顶与末尾元素 | 将当前最大值放到数组末尾 |
3 | 缩小堆范围并重新堆化 | 对剩余元素调整,恢复堆性质 |
4 | 重复2-3直到排序完成 | 逐步提取最大值,形成升序序列 |
- 关键操作详解
- (1) 堆化(Heapify)——下滤操作
核心思想:确保以 dad 为根的子树满足堆性质
操作流程:
比较父节点与左右子节点的值
若子节点值更大,交换父节点与最大子节点
递归调整被交换的子节点所在子树
示例:对节点0进行堆化初始树(非大根堆):
3(0)/ \8(1) 5(2)/ \
1 7
堆化过程:
比较节点0(3)、左子1(8)、右子2(5) → 最大为8
交换节点0和1 → 新根为8
递归处理节点1:比较节点1(3)、左子3(1)、右子4(7) → 最大为7
交换节点1和4 → 子树调整完成
最终大根堆:
8(0)/ \7(1) 5(2)/ \
1 3
- (2) 构建大根堆
起点选择:从最后一个非叶子节点开始(索引 n/2 - 1)
推导:完全二叉树中,最后一个非叶子节点的索引为 ⌊n/2⌋ - 1
原因:叶子节点本身已满足堆性质,无需调整
示例:数组 [3,8,5,1,7] 构建大根堆
最后一个非叶子节点索引 = 5/2 -1 = 1(节点1)
调整节点1:子树 [8,1,7] → 交换7和1 → [8,7,1]
调整节点0:子树 [3,8,5] → 交换3和8 → 递归调整节点1
- (3) 排序阶段
每次将堆顶(最大值)交换到当前范围的末尾,然后对剩余元素堆化。
示例:堆 [8,7,5,1,3] 排序过程
交换8和3 → 数组变为 [3,7,5,1,8],堆范围缩小为前4个元素
对节点0堆化 → 新堆 [7,3,5,1]
重复交换堆顶7与末尾1 → 数组 [1,3,5,7,8]
继续调整直到完成
堆排序原理
-
构建最大堆:为什么构建堆要从后往前调整非叶子节点?因为堆化操作是向下调整,必须保证子树已经是堆。从最后一个非叶子节点开始,可以确保每次调整时,子树已经满足堆性质。
-
堆化(Heapify):通过下滤操作维护堆性质。父节点若不满足堆性质,则与较大子节点交换,并递归调整子树。
-
排序:每次将最大值交换到数组末尾,逐步缩小堆范围,最终得到升序数组。
堆排序代码实现
堆化(下滤)部分
// 堆化函数(下沉操作)
void heapify(int* arr,int n,int dad)
{int max_idx = dad; //最大值索引初始为dadint left = 2*dad + 1; //左子int right = 2*dad + 2;//右子if(left < n && arr[left] > arr[max_idx]) max_idx = left;if(right < n && arr[right] > arr[max_idx]) max_idx = right;if(max_idx != dad) //如果最大值索引不是父节点说明三元组变动{swap(arr[max_idx],arr[dad]); //变动三元组heapify(arr,n,max_idx); //把变动过的子节点进行递归下滤直到三元组仅有一元素或者满足三元组不变动}
}
堆排序部分
void heap_sort(int* arr,int n)
{// 1. 构建大根堆(如果不先构建大根堆的话,每次下滤堆化操作找出来的不一定是最大值)//从最后一个非叶子结点(数学关系为n/2-1)到顶结点依次作为父节点进行三元组操作for(int i=n/2-1;i>=0;--i) heapify(arr,n,i);// 2.不断把大根堆的顶部,也就是剩余元素的最大值调转到数组末尾,再进行堆化下滤找出剩余元素次大的元素for(int i=n-1;i>0;--i) //i只用来控制末尾{swap(arr[0],arr[i]);heapify(arr,i,0); //每次都是从头下滤到数组长度i,否则每次都是滤出最大值}
}
复杂度简要分析
- 初始构建大根堆部分的时间复杂度
每个节点的高度:假设树高度为h,节点在第k层(根为第0层),则高度为 h - k
总调整次数:Σ (每个节点的高度) = Σ_{k=0}^{h} [2^k * (h - k)]
数学推导:
令 S = 2^0*(h) + 2^1*(h-1) + ... + 2^{h-1}*1
2S = 2^1*(h) + 2^2*(h-1) + ... + 2^h*1
S = 2S - S = 2^{h+1} - h - 2
由于 h ≈ logn,故 S = O(n)
初始构建大根堆的时间复杂度为 O(n)
- 排序部分的时间复杂度
每次交换后堆化:需要进行 n-1 次堆化操作,所以时间复杂度为O(n)
每次堆化时间复杂度:O(log n)(递归树的高度)
总时间复杂度:O(n log n)
- 总体复杂度
时间复杂度:O(n) + O(n log n) = O(n log n)
空间复杂度:原地排序 → O(1)
例题
模板:【模板】排序
参考资料及推荐学习
【从堆的定义到优先队列、堆排序】 10分钟看懂必考的数据结构——堆
有点抽象的堆排序,一个三分钟的动画教会你
【数据结构合集 - 堆与堆排序(算法过程, 效率分析, 稳定性分析)】
【算法讲解025【必备】堆结构和堆排序】
【【强烈推荐】算法 - 印度大神图文讲解 - UP主翻译校对 (合计84集,进行中)- - -heapsort 】
总结
堆排序的每一次堆化与交换,皆是混沌与秩序的共舞,亦是算法与诗学的共振。它教会我们:真正的秩序绝非静止的完美,而是动态的平衡——正如叶子节点在沉浮中重构层级,最大值在陨落中让渡新生。拉丁文哲人卢克莱修在《物性论》中写道:“Ex nihilo nihil fit”(无中不能生有),而堆排序却以递归为笔,在虚无的数组中书写出严谨的层次,这恰似人类理性对无序世界的温柔驯服。
若将人生视作一棵不断堆化的树,或许我们终将领悟:每一次“下沉”并非坠落,而是为了在更深的维度扎根;每一次“交换”亦非失去,而是为了在系统熵增中守护局部的最优解。愿这算法的哲思,如维吉尔指引但丁穿越地狱般,带我们于代码的迷雾中窥见永恒的结构之美——Sic transit gloria informaticae(计算之荣,如是流转)。