基本思想
堆排序是基于堆这种数据结构的一种排序方法。首先将待排序的数组(或序列)构造成完全二叉树,然后利用完全二叉树中父节点和孩子节点之间的关系,每次从当前二叉树中找出最大节点并将其移出未排序部分,达到排序的目的。首先介绍一下一些相关概念:
-
完全二叉树:对于一棵深度为 h h h 的二叉树,如果除了最后一层外,其他每层的节点数都达到最大,且第 h h h 层的节点都连续集中在最左边,那么这就是一棵完全二叉树,如下图所示:
-
堆:这里完整的叫法应当是二叉堆,它是一棵堆有序的完全二叉树。堆分为最大堆和最小堆,对于最大堆来说所有的父节点均大于等于两个孩子节点的值,因此根节点应当是最大值节点;相反,对于最小堆,所有的父节点均小于等于子节点的值,因此最小堆的根节点是最小值节点。
-
堆的顺序存储结构:堆是一棵完全二叉数,因此用数组这种顺序存储结构就可以表示:
①:按层序遍历的顺序在数组中存放堆的元素,下标0表示的元素是根节点,其子节点分别为下标1和下标2……依次类推;
②:节点 i i i 如果存在左孩子,左孩子的下标为 2 i + 1 2i+1 2i+1;如果存在右孩子,右孩子的下标为 2 i + 2 2i+2 2i+2。
算法流程
这里以一个长度为 n n n 的数列arr为例
- 首先将待排序数组构建成一个堆,此时根节点(arr[0])应当为最大值,整个数组都处于无序区;
- 将堆中最大的元素移出。具体做法就是交换数组第一个元素 arr[0] 和 最后一个元素arr[n-1],交换之后 arr[n-1] 位于有序区,不再参与后面的排序,此时无序区由 {arr[0]~arr[n-2]} 组成;
- 第二步之后,无序区的元素排列是违反堆的规则的,因此要重新对无序区进行调整得到一个包含 {arr[0]~arr[n-2]} 的新堆,然后交换 arr[0] 和 最后一个元素arr[n-2], 并且将arr[n-2] 移到有序区,接下来对剩余元素重复相同的操作直至排序完成。
演示

代码实现
堆排序中,关键的一个操作就是每次调整无序区的元素使其满足堆的规则,这个操作通过adjust()函数实现:
private static void adjust(int[] arr, int i, int N){
while(2*i+1 < N){
int j = 2 * i + 1; // 当前节点左孩子结点的索引
if(j+1 < N && (arr[j] < arr[j+1])) j++; // 找到最大的孩子节点
if(arr[i] >= arr[j]) break; // 表明根节点大于等于所有的孩子节点,不用交换
int temp = arr[i]; // 交换最大孩子节点和根节点
arr[i] = arr[j];
arr[j] = temp;
i = j;
}
}
排序函数如下:
public static void heap_sort(int[] arr){
// 数组为空或者长度为1不需要排序
if(arr == null || arr.length < 2){
return;
}
int N = arr.length;
for(int i = N/2; i >= 0; i--){
adjust(arr, i, N); // 通过调整使得初始堆有序
}
// 交换根节点(保存着最大元素)和最后一个孩子节点,并从堆中删除
while(N-- > 0){
int temp = arr[N];
arr[N] = arr[0];
arr[0] = temp;
adjust(arr, 0, N); // 重新调整数组使得堆有序
}
}
说明:首先对于原数组进行调整使得初始堆有序,然后通过while循环里的语句,首先交换当前堆中第一个元素和最后一个元素,并将最后一个元素移出堆(通过N–实现),再重新调整堆使其有序。
分析
-
时间复杂度
堆排序遍历的次数就是由初始元素构成的完全二叉树的深度,其时间复杂度为 O ( n l o g ( n ) ) O(nlog(n)) O(nlog(n)),最好和最差情况下也都为 O ( n l o g ( n ) ) O(nlog(n)) O(nlog(n))。
-
空间复杂度
堆排序使用的额外空间跟数组长度无关,因此空间复杂度是 O ( 1 ) O(1) O(1)。
-
稳定性
如果存在重复元素,堆排序在交换堆顶元素和最后一个元素的时候,可能将原本靠后的元素前移,造成相同元素相对顺序的改变,因此堆排序是不稳定的。
堆排序是目前唯一能够同时最优地利用时间和空间的排序方法。
参考资料
- 一文搞定十大经典排序算法
- 《算法(第四版)》
- https://www.cnblogs.com/chengxiao/p/6129630.html