为什么堆排序要比其他排序难一些呢,因为其他排序可以直接通过数组这种简单的数据结构来进行实现,比较直观。但是堆排序用到了堆这种逻辑上的结构,实现方式有很多种,可以用链表,也可以用数组。所以理解起来就比较困难了。同时其他的排序方式都是通过一个二层循环或者递归循环运行结束就可以看到已经排好的序的数组。但是堆排序你很难在整个过程中看到一个有序的数组或者链表。它整个排序过程大致可以分为两个步骤,第一,根据现有的数据构造一个堆,第二,不停的从堆顶取出元素,然后调整堆。这两个过程就可以保证你取出的元素有序。从这两个步骤可以看出,在元素入堆和出堆的时候需要修改堆本身,和普通的排序方式不同,普通的排序方式为,将元素一个一个的放入到数组,接着调用排序,接着从数组中取出元素,每一步操作比较单一,所以比较能够容易理解。下面就来实现一个堆排序的源代码:
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#define DATA_COUNT 10
static int data[DATA_COUNT];
int main(int argc,char* argv[]){
srand(time(NULL));
//准备模拟数据
printf("待处理数据为:");
for(intj=0;j<DATA_COUNT;j++){
data[j]=rand()%100+1;
printf("%d,",data[j]);
}
printf("\n\n");
/*
//经典选择排序
for(inti=0;i<DATA_COUNT;++i){
for(intj=i+1;j<DATA_COUNT;++j){
if(data[j]>data[i]){
inttmp=data[j];
data[j]=data[i];
data[i]=tmp;
}
}
}
//输出
printf("排序后:");
for(inti=0;i<DATA_COUNT;++i)
printf("%d,",data[i]);
printf("\n\n");
*/
//数据入堆
for(inti=0;i<DATA_COUNT;++i){
//调整堆
for(intidx=i;idx;){
intparent=idx/2;
if(data[idx]>=data[parent]){
inttmp=data[idx];
data[idx]=data[parent];
data[parent]=tmp;
idx=parent;
}
else
break;
}
}
printf("堆排序结果为:");
//数据出堆
for(inti=0;i<DATA_COUNT;i++){
printf("%d,",data[0]);
//调整堆
data[0]=data[DATA_COUNT-1-i];
intidx=0,left,right,best;
while(1){
left=2*idx+1;
right=2*(idx+1);
if(left>=DATA_COUNT-i)break;
if(data[left]>=data[idx])
best=left;
else
best=idx;
if(right<DATA_COUNT-i&& data[right]>=data[best])
best=right;
if(best==idx)break;
inttmp=data[idx];
data[idx]=data[best];
data[best]=tmp;
idx=best;
}
}
getchar();
}
看完堆排序,那么究竟什么是堆呢?
堆,实际上是一颗完全二叉树,其任何一个非叶结点满足性质:
key[i]<=key[2i+1]&&key[i]<=key[2i+2]或者 Key[i]>=Key[2i+1]&&key>=key[2i+2]
即任何一非叶节点的关键字不大于或者不小于其左右孩子节点的关键字。堆分为大根堆和小根堆,满足key[i]<=key[2i+1]&&key[i]<=key[2i+2]的称为小根堆。由堆的定义可以知道大根堆的根部的关键字肯定是所有关键字中最大的,小根堆根部的关键字是所有关键字中最小的。
难道堆这么强大的数据结构就只是用来排序吗,从上面的代码可以很轻松的看出,相比之下,堆排序和普通的选择排序相比要多写很多代码,估计为了一掉效率没有多少人愿意多些代码,那么堆在什么时候使用呢?来个实际的题目:
如何从文件里面的10000000万个数据中找到最大的前10个数,前100个数?
普通解法:
1.构造一个101个元素的数组,然后从文件里面读取100个数到数组里面。
2.从文件里面读取下一个数据放入到数组的最后一个元素。
3.对整个数组排序。
4.重复2,3过程直到文本文件读取结束。
5.输出数组的前100个元素。
从上面的解法我们可以发现几乎每一次读取一个数据到数组末尾的时候,都需要重新对整个数据进行排序,根据这里的情况发现前100个元素已经有序,只有最后一个元素无序,可以采用二分搜索法快速找到最后一个元素在前面100个元素中的位置ind,接着把ind之后的数据依次往后一动一个位置,进行提高一点效率,但是整体效率还是比较低。这种解法的好处是,思路比较简单,易于实现。
堆解法:采用小根堆数据结构。
1.初始化一个小根堆
2.从文件里面读取下一个数据a,
3用a和堆顶元素heap[0]比较,如果大于堆顶元素,进入4,否则转到2。
4.调整堆。
5.判断文本读取是否结束,如果结束,进入6,否则转到2。
6.从堆中以此取出元素。
从堆的解法可以看到整个过程和普通解法大同小异,但是唯独不同的是在第4步,那么这个解法的效率提升也主要是在第4步,因为讲一个数组变成有序的复杂度是大于将一个数组构成堆的复杂度,同时由于这样的操作存在于一个数量级比较大的循环中,那么最终这两个解法的时间复杂度就差距就很大。
接下来就根据前面提到的问题,进行简化,来写个代码实现,对于普通解法,就不去写代码了,主要是针对堆解法的代码进行实现。
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#define DATA_COUNT 100
#define HEAP_COUNT 10
int data[DATA_COUNT];
int heap[HEAP_COUNT];
int main(int argc,char* argv[]){
//产生模拟数据
printf("待处理数据为:");
srand(time(NULL));
for(inti=0;i<DATA_COUNT;i++){
data[i]=rand()%100+1;
printf("%d,",data[i]);
}
//初始化堆
for(inti=0;i<HEAP_COUNT;i++)
heap[i]=INT_MIN; //找到前HEAP_COUNT个较大元素,采用小根堆
for(inti=0;i<DATA_COUNT;i++){
if(data[i]>heap[0]){
heap[0]=data[i];
//调整堆
intidx=0,left,best;
while(1){
left=2*idx+1;
if(left>HEAP_COUNT-1)break;
if(heap[left]<=heap[idx])
best=left;
else
best=idx;
if(left+1<HEAP_COUNT&& heap[left+1]<=heap[best])
best=left+1;
if(idx==best)break;
inttmp=heap[idx];
heap[idx]=heap[best];
heap[best]=tmp;
idx=best;
}
}
}
//取出堆顶元素
printf("\n\n前%d大元素为:",HEAP_COUNT);
for(inti=0;i<HEAP_COUNT;++i){
printf("%d,",heap[0]);
heap[0]=heap[HEAP_COUNT-1-i];
//调整堆
intidx=0,left,best;
while(1){
left=2*idx+1;
if(left>HEAP_COUNT-1-i)break;
if(heap[left]<=heap[idx])
best=left;
else
best=idx;
if(left+1<HEAP_COUNT-i&& heap[left+1]<=heap[best])
best=left+1;
if(idx==best)break;
inttmp=heap[idx];
heap[idx]=heap[best];
heap[best]=tmp;
idx=best;
}
}
getchar();
}
整个代码写的比较丑陋,比如调整堆的代码,实际上是可以进行一个函数封装的!!!