堆排序(Heap Sort)
堆排序是一种就地排序算法,是简单选择排序的一种改进。它将排序序列分为已排序序列和未排序序列两部分,通过不断寻找未排序序列中的最大元素放入已排序序列从而迭代减少未排序序列的元素数量。比简单选择排序优化的地方是采用了数据结构中的二叉堆结构从而代替线性的搜索时间。堆排序的工作原理是通过不断维护堆结构,每次选择出最大元素交换到堆尾。堆常常放在一个完整的二叉树布局的线性表中(a[1]为首地址),对于k位置元素,左孩子即2*k,右孩子即2*k+1。
堆排序的一般步骤:
1.将原始序列初始化为一个大顶堆。
2.堆顶元素与堆尾元素互换,忽略堆尾,将剩余n-1个元素重新调整为大顶堆结构(调整建堆)。
3.重复第二步操作n-1次。
堆排序过程图:
![](https://i-blog.csdnimg.cn/blog_migrate/ac2b2cc7d929094a42ef835225914994.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/e02790f5d484f423f57f222f637f2d4d.gif)
思路:
可以暂且跳过步骤一,假设当前大顶堆的初始化工作已经完成,则对上右图分解来看:
①交换8和1并从堆中删除8。
![](https://i-blog.csdnimg.cn/blog_migrate/1a214590772c5dc2d36d0a01476e8000.png)
![](https://i-blog.csdnimg.cn/blog_migrate/ca45c9f6e7694f609ce785a78f1d29cc.png)
②除堆顶元素外,左右子树都为堆结构。备份堆顶,比较堆顶与其两个孩子,若堆顶不最大则将两孩子中的大者“上移”。如此重复
![](https://i-blog.csdnimg.cn/blog_migrate/a33992281e85e23c134f9694f390440c.png)
![](https://i-blog.csdnimg.cn/blog_migrate/b18b0572da2008f8dd1c9b73163db274.png)
![](https://i-blog.csdnimg.cn/blog_migrate/32c0afe41869faf92f390b8ba3850faa.png)
③这n-1个元素调整成了一个新的大顶堆结构。如此重复n-1次,即可使得数组有序。
附上一次调整的函数:
//维护上顶堆结构函数
void Heapify(int arr[], int s, int m) { //s是堆顶元素下标,m为元素个数
int rc = arr[s];//临时变量记录堆顶元素的值
for(int j = 2 * s; j <= m; j *= 2) {//j为s左孩子,j+1为s右孩子
if(j < m && arr[j] < arr[j+1]) j++;//使得j始终指向最大的孩子
if(rc > arr[j]) break; //符合堆结构
else {//上移,并令s指向待填位置
arr[s] = arr[j];
s = j;
}
}//循环结束后,s指向待填位置
arr[s] = rc;
} //T(N) = logN
然后重复n - 1次:
for(int i = n; i > 1; i--) { //i指向当前堆尾
swap(arr[1],arr[i]);//头尾元素互换
Heapify(arr, 1, i - 1);
}
最后就是将原序列初始化为上顶堆序列的问题了,其实我们可以想想第二步操作对第一步操作有没有什么启发。
我们可以从最后一个非叶子结点(下标m/2)开始逆序进行操作。因为对于以最后一个非叶子节点为堆顶的堆来说,左子树肯定为堆结构(就一个节点),同样右子树也为堆结构(一个或者没有节点),所以就可以运用Heapify函数对这一个小堆进行调整。这样逆序遍历非叶子节点,就又转化成了除了堆顶左右都为堆结构的问题。
初始化的代码很容易就出来了:
for(int i = m / 2; i >= 1; i--) { // 最后一个非叶子结点到根
Heapify(arr, i, m); //到m是因为为了简化代码,细想就会发现其实不影响排序的
} //T(N) = N*logN
这样堆排序算法就完成了。然后进行一些算法性能的分析.
时间复杂度:
内部调整的复杂度为n*logn,外部调取函数遍历也是n*logn,所以整体平均时间复杂度和最坏情况都为O(n*logn)
空间复杂度:
O(1)只用一个临时变量
稳定性:
要进行堆尾和堆顶的互换,如果不同小堆之间存在相同值,则不稳定.
与其他排序算法的比较:
显然,堆排序最大的敌人还是排序界的王者:快速排序.
堆排序优于快速排序的地方是:
首先,堆排序的最坏时间复杂度也是次平方阶的,而快排的最坏是平方阶的。
其次,堆排序的空间复杂度是常数阶的,只用一个临时变量,而快排用到了递归,递归工作栈需要lgn的空间。
但是,一般情况下,快排的性能都略好于堆排序,是因为快排常数因子项要比堆排序小。并且快排到达最坏的复杂度的情况是非常罕见的。所以在很多语言的模板库内,排序不是直接用的快排,而是用的内省排序。它的排序原理是先进行快速排序,如果递归工作栈深到一定程度时,就采用堆排序(Linux内核为堆排序,基于实时性约束和安全性考虑)。
然后比较堆排序和归并排序,他们最坏复杂度都是次平方阶的。
首先归并排序是稳定的。
堆排序优于归并排序的地方是空间复杂度,堆排序常数而归并排序是线性阶的。所以一些小型缓存机器上更先考虑堆排序。但是堆排序是跳跃访问整个堆,不如归并连续访问对数据缓存友好。而且归并排序具有并行化特点,并且是一种外部排序。