- 今天讲解一下堆排序的原理以及实现、复杂度和稳定性分析
1 堆的定义
堆排序(英语:Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。
定义:
n个关键字序列L[1…n]称为堆,当且仅当该序列满足:
① L(i)>=L(2i) 且 L(i)>=L(2i+1)或
② L(i)<=L(2i) 且 L(i)<=L(2i+1) (1≤isln/2])
可以将该一维数组视为一棵完全二叉树
大根堆:满足条件① 的堆称为大根堆(大顶堆),大根堆的最大元素存放在根结点,且其任一非根结点的值小于等于其双亲结点值
对于堆中的元素编号,其实也是逻辑结构映射到数组的一个过程;
小根堆:满足条件2的堆称为小根堆(小顶堆),小根堆的定义刚好相反,根结点是最小元素
2 堆排序的思路
首先将存放在L[1…n]中的n个元素建成初始堆,由于堆本身的特点(以大顶堆为例),堆顶元素就是最大值。
输出堆顶元素后,通常将堆底元素送入堆顶,堆被破坏,将堆顶元素向下调整使其继续保持大顶堆的性质,再输出堆顶元素。如此重复。
- 根据数组构建一个完全二叉树
- 从最后一个非叶节点开始调整,使得子树成为堆
- 如此重复,直至满足堆的定义
值得注意的是:
大根堆排序结果为升序
小根堆排序结果为降序
这是一个常见的误区!
这是因为:堆使用的时候都是每次把堆顶的元素干掉留下堆内部的元素做成Top N
如果你要找100000中的 TOP100最大的,你用小根堆
如果你要找100000中的 TOP100最小的,你用大根堆
3 代码实现
void BuildMaxHeap(int *arr,int len)
{
// 建立初始大根堆
for(int i = (len-1)/2; i >= 0; i--)
{
HeapAdjust(arr,i,len);
}
}
// 子树调整
void HeapAdjust(int *arr,int k,int len)
{
int temp = arr[k]; // 暂存根节点
for(int i = k*2+1;i < len;i = i*2+1)
{
if (i < len-1 && arr[i] < arr[i+1])
i++;
if (temp >= arr[i])
break; // 筛选结束
else {
arr[k] = arr[i];
k = i; // 继续向下筛选
}
}
arr[k] = temp ; // 筛选的值放入最终位置
}
void HeapSort(int *arr,int len)
{
BuildMaxHeap(arr,len);
for(int i = len-1;i > 0;i--)
{
arr[0] = arr[0] ^ arr[i];
arr[i] = arr[0] ^ arr[i];
arr[0] = arr[0] ^ arr[i];
HeapAdjust(arr,0,i);
}
}
4 堆的输出(删除操作)
堆中根结点的值肯定是最值,不是最大就是最小,往往需要使用到。
即每次都删除第0个数据(根结点)
那么堆中输出根节点之后如何保证堆原有的特性呢?
删除之后,打破了原有规律,需要调整!
-
删除根结点
-
将最后一个数据的值赋给根结点
-
然后再从根结点开始进行一次从上向下的调整
5 堆的插入操作
-
对堆进行插入操作时,先将新结点放在堆的末端
-
随后再向上执行调整操作
6 堆排序的特点
堆排序适合关键字较多的情况(如n>1000)
不适合关键字较少的情况
大根堆排序结果为升序
小根堆排序结果为降序
例如,在1千万个数中选出前100个最大值?
首先使用一个大小为100的数组,读入前100个数,建立小顶堆,而后依次读入余下的数,若小于堆顶则舍弃,否则用该数取代堆顶并重新调整堆,待数据读取完毕,堆中100个数即为所求。
7 性能分析
【空间复杂度】:仅使用常数个辅助单元,空间复杂度为 O(1)
【时间复杂度】:建堆时间为 O(n),之后有n-1次向下调整操作,每次调整的时间复杂度为 O(h) h表示树高
故在最好、最坏和平均情况下,堆排序的时间复杂度为 O(nlog2n)
【稳定性】:进行筛选时,有可能把后面相同关键字的元素调整到前面,所以堆排序算法是一种不稳定的排序方法