希尔排序与堆排序超详解(附理解性实验与指导+完整C代码)

希尔排序、堆排序算法总结

/*
先送一个漂亮的排序算法,给有缘读到本文的读者
这个算法是研究希尔排序的增量序列时偶然发现的,
与希尔排序的原理类似,但发生了质变。
就叫“七上八下排序”吧。。
*/
void sort(int* arr,int length){
    int gap=length,temp;
    while(gap){//间距为零退出
        for(int i=0;i+gap<length;++i) {//真就一层循环,O(n)
            if(arr[i]>arr[i+gap]) {
                temp=arr[i];
                arr[i]=arr[i+gap];
                arr[i+gap]=temp;
            }
        }
        gap*=7;//按照比例缩小比较间距,O(logn)
        gap/=8;//这两个数是该算法名字的由来
    }
}
/*
时间复杂度nlogn,排1亿数据才花不到30秒
稍加改进就可以排10亿级别的数据!
排1亿个int数据qsort都要7秒多,
算法正确性由超暴力实验保证。
如果非说可能有排序不完全的危险性,
那么对于100000以下的数据这种情况发生的概率应当小于百万分之一。
*/

摘要

本文先讲解希尔排序与堆排序,然后以C语言stdlib中的qsort函数作为标准进行算法性能测试。有Win10+clodeblocks环境的读者可完全复现本文的数据。
本文中的排序都为从小到大排序。

希尔排序

引导:
希尔排序是对简单插入排序的一种优化。
怎么优化的?
数组排序,说白了就是消灭数组中的逆序数。
数组中的逆序数最大是对少?
比如5 4 3 2 1:
4+3+2+1;
不可能再大了。
显然,由等差数列的性质,一个数组可能拥有的逆序数的最大值与数组长度的平方成正比。
因为随机数组的逆序数的期望与数组的最大逆序数成正比,
所以冒泡排序、插入排序等每次交换只能使逆序数减1的算法,平均时间复杂度一定不会小于O(n2),用这样的算法要排上亿的数据是不现实的。
正文:
希尔排序对性能的优化可能来自很多方面,我们就从简单的说起(难的你以为我能会吗?)。如果我们把选择排序的比较间距拉大而不只是比较相邻的两个元素(比较的间距也就是交换的间距),那么每次交换时消灭的逆序数会大于1。希尔排序就是先用大间距快速消灭逆序数,然后再用小间距收尾,以此获取排序效率的极大提高。

//一种希尔排序的算法实现
void shellSort(int *arr,int l){//希尔排序
    int temp,left,d=l/2;
    while(d){//增量为0就排好了
        for(int right=d;right<l;++right){//以大间距进行比较
            temp=arr[right];//这里就是插入排序的东西了
            left=right-d;//大间距下相邻元素下标差d
            while(left>=0&&arr[left]>temp){//(前面有序后面乱,把小元素往前(小下标方向)推直到遇到更小的)
                print(arr,last,N);
                arr[left+d]=arr[left];
                left=left-d;
            }
            arr[left+d]=temp;
        }
        d/=2;//间距更新
    }
}

显然选择不同的增量序列(间距序列)希尔排序的性能是有差别的。
上面的算法实现在效率上与"七上八下算法"基本一样(也别小看"七上八下",进行优化后它比下面的堆排序还快(10亿级的数据测试))。
希尔排序的增量序列选择是个数学难题,这里不多介绍下面给出一个更高效的希尔排序算法实现。

int shellList[14]={//希尔排序增量序列
1, 9, 34, 182, 836, 4025, 19001, 
90358, 428481, 2034035, 9651787, 
45806244, 217378076, 1031612713
};
void shellSort(int *arr,int l){//希尔排序
        int temp,left,di=13,d=shellList[13];
        while(d>=l){
                --di;
                d=shellList[di];
        }
        while(d){
                for(int right=d;right<l;++right){
                        temp=arr[right];
                        left=right-d;
                        while(left>=0&&arr[left]>temp){
                                print(arr,last,N);
                                arr[left+d]=arr[left];
                                left=left-d;
                        }
                        print(arr,last,N);
                        arr[left+d]=temp;
                }
                --di;
                d=shellList[di];
        }
}

那一串吓人的序列不是乱写的(可以自己百度),就这个函数排1亿int数据也就十几秒的事。
现在说下"七上八下算法"到底是怎么发现的
某一刻我突发奇想,如果希尔排序里面不是完整的插入排序,而只是按照指定间隔跑一趟还能消掉多少逆序数?
换句话谁这样的不完整希尔排序跑多少遍才能真正将数组排好?

//检验函数很容易写
int check(int* arr,unsigned long int n){
        for(unsigned long int i=0;i<n-1;++i){
                if(arr[i]>arr[i+1]){
                        return 1;
                }
        }
        return 0;
}

我把增量序列从(1/2)n,(2/3)n,(3/4)n,(4/5)n,(5/6)n,(6/7)n进行尝试(实验指到会说怎么看)。
到了(6/7)n新算法就诞生了。
在这个(6/7)n的增量序列中,99.9%的数组被一次排好之前序列的排序次数会随数组规模增加而按照增量序列呈现”分形“规律变化(如下图)
(2/3)^n:
请添加图片描述
(3/4)n:
请添加图片描述
(4/5)n:
请添加图片描述
(5/6)n:
请添加图片描述
而这次,排序次数曲线是一条直线上面带有极少量的噪点。我把增量序列改成(7/8)n后我知道质变已经发生了(如图)。
(7/8)n:
请添加图片描述

随后进行的巨量的实验验证中,数组规模从1到50000(后来还单独在100000附近进行了验证),每个长度上排几百次随机数组。全都一遍排好,没有例外就算还可能会有意外发生,就看O(nlogn)的效率,排完之后再检查一遍也值得啊。(优化后的算法就是用(6/7)n排完再检查:-)。
如果有大神给出"七上八下"算法的数学证明就放在评论区吧,我真的希望这个算法是真正正确的,它实在太简洁了(即使是那个慢到离谱的完美排序简洁程度上也没发与它相比啊),舍不得放弃(可以认为我发这篇文章就为了有大神看到这个算法并给出严格证明)。

堆排序

引导:
堆排序,归并排序,快速排序等算法有比消灭逆序数更直观的原理。
对于数组排序来说简直就是降维打击,保证了O(nlogn)的时间复杂度使得亿级的排序变得现实,因此这里不扯逆序数了。
大顶堆是什么?
父节点比子节点大的完全二叉树,只有最后一行可能不是2(n-1),且都是连续存放的。如下图
二叉树在数组中的存储(数字代表数字下标)
-----------------------0
---------1---------------------------2
—3----------4-------------5--------------6
7----8----9----10----11----12-----13……
对于任意一个节点n
左子树在2n+1
右子树在2n+2
父节点在(n-1)/2 (根节点没有父节点)
这样我们就可以轻易获取任意节点的子树位置于父节点。
正文
堆排序分两步:
1、建堆
2、排序
建堆时将整个序列当作二叉树进行调整,排序时堆不断缩小,剩下的空间存放排好序的元素。
我们怎么把一个混乱的数组变成大顶堆?
或者说怎么把二叉树调整成堆?
一、 最简单的,就三个元素,把最大的找出来和根节点一换就完工了。
如图(数字代表值)
----3
8------9
成为
----9
8------3
二、 现在想象两个叶节点是两个堆,
如果是
-----------7
----8------------9
4------5-----4------5
成为
-----------9
----8------------7
4------5-----4------5
则一切顺利,但是如果如下
-----------3
----8------------9
4------5-----4------5
这样调整后,两个堆的堆顶可能会被较小的元素替换,从而也需要替换
-----------9
----8------------3
4------5-----4------5
->
-----------9
----8------------5
4------5-----4------3
这种替换操作会一直传递到最下层,然后堆调整结束。

//堆调整算法实现
void heapify(int *arr, int n, int i) {//调整大小n的堆的第i个节点
    while(1) {//无限循环,一直向下传到最底层
        int maxPos,temp,ld,rd;
        maxPos=i;//最大元素下表
        ld=i*2+1;//左节点
        rd=i*2+2;//右节点
        if(ld<n&&arr[maxPos]<arr[ld]){//找根左右最大的节点当根节点
                maxPos = ld;
        }
        if (rd<n&&arr[maxPos]<arr[rd]){
                maxPos = rd;
        }
        if (maxPos==i) break;//父节点最大该节点说明无需调整
        //把最大节点与父节点交换
        print(arr,last,N);
        temp=arr[i];
        arr[i]=arr[maxPos];
        arr[maxPos]=temp;
        i = maxPos;//因为原根节点变小了所以要再进行判断
    }
}

三、 但是我们要怎样才能调整整棵树?
刚才的算法可以调整只有叶片的子树,进而可以把所有子树都变成堆,不断向上,直到堆建成,代码如下,应该不难理解。想象二叉树最底层先成为堆,然后倒数第二层,倒数第三层,直到根节点。

void buildHeap(int *arr, int n) {//建堆
        for (int i=(n-1)/2;i>=0; --i) {//堆从后往前调整
                heapify(arr, n, i);//堆调整
        }
}

最后排序就容易理解了
交换树的根节点与树的最后一片叶,然后把树的大小减一,再调用树调整算法,如此往复,直到树只有根节点,排序完毕。

void heapSort(int *arr, int n) {//堆排序
        int temp;
        buildHeap(arr,n);//建堆
        while (--n) {//堆顶与树的最后一片叶交换然后,堆的规模减小
                print(arr,last,N);
                temp=arr[0];
                arr[0]=arr[n];//(n不必-1,因为树已经减小了,现在的n就是原来的n-1)
                arr[n]=temp;
                heapify(arr,n,0);//堆调整
       }
}

是不是感觉堆排序很慢?被堆调整的复杂表象骗了,建堆的时候每个节点元素的调整时间复杂度O(logn),排序时也一样的,每个元素过一遍就是O(nlogn),效率也是很高的。
不想自己试试的可以直接看对比动画(b站能搜到一堆类似的)。请添加图片描述
先是qsort函数
然后是优化的希尔排序
然后是"七上八下优化版(简排)"排序
最后是堆排序,各有风格
引文windows的画图函数效率堪忧,我就用长度300的数组来演示。
之所以能检测qsort是因为它的比较函数需要自己编写并传入,我在那里做了手脚:-)。
1万数据下的性能对比:
请添加图片描述
一千万:请添加图片描述

1亿数据:请添加图片描述

10亿:请添加图片描述
综上:
qsort:yyds
希尔排序:简洁高效
七上八下:
堆排序:看着舒服

实验指导

如果你用的不是codeblocks,先别急,能引入windows.h头文件的应该都可以。(用Linux的大神不会在乎这些细节)
一、在环境中添加连接文件用来绘图
codeblocks上方Settings
请添加图片描述
然后Compiler
在这里插入图片描述
Linker settings
在这里插入图片描述
点击Add按照我标蓝色的目录找到文件加进去(不过你需要直到Codeblocks按在哪里)。
在这里插入图片描述
然后复制下面的代码就不会报错了。
(这是我初学C语言时梦寐以求的功能。。当时为了写个贪吃蛇,搜了好久才会的,没想到竟然还能用于数据可视化。)

/*
这是测试10亿数据的代码(我电脑8G的,如果内存小(<4G)就别10亿了)
要想看动图就把N改到3000以下,推荐300
删除#define print(arr,last,N);
取消void print(int* arr,int * last,int n)的注释
取消cmp中print(arr,last,N);的注释
*/
#include<windows.h>
#include<stdio.h>
#include<stdlib.h>
#include<time.h>
#define N 10000000
#define print(arr,last,N);
HDC dc;
int *arr;
int *last;
/*void print(int* arr,int * last,int n){
        for(int i=0;i<n;++i){
                if(arr[i]-last[i]){
                        for(int h=0;h<600;++h){
                                SetPixel(dc,i*1200/n,600-h,0x000000);
                        }
                        for(int h=0;h<arr[i];++h){
                                SetPixel(dc,i*1200/n,600-h,0x00ff00);
                        }
                        last[i]=arr[i];
                }
        }
}*/
int cmp(const void* a,const void* b){//快速排序用的判断函数
        //print(arr,last,N);
        return *(int*)a-*(int*)b;
}
int check(int* arr,unsigned long int n){//用于优化版本简排序的检测函数
        for(unsigned long int i=0;i<n-1;++i){
                if(arr[i]>arr[i+1]){
                        return 1;
                }
        }
        return 0;
}
void heapify(int *arr, int n, int i) {//调整大小n的堆的第i个节点
    while(1) {//无限循环
        int maxPos,temp,ld,rd;
        maxPos=i;//最大元素下表
        ld=i*2+1;//左节点
        rd=i*2+2;//右节点
        if(ld<n&&arr[maxPos]<arr[ld]){//找根左右最大的节点当根节点
                maxPos = ld;
        }
        if (rd<n&&arr[maxPos]<arr[rd]){
                maxPos = rd;
        }
        if (maxPos==i) break;//父节点最大该节点说明无需调整
        //把最大节点与父节点交换
        print(arr,last,N);
        temp=arr[i];
        arr[i]=arr[maxPos];
        arr[maxPos]=temp;
        i = maxPos;//因为原根节点变小了所以要再进行判断
    }
}
void buildHeap(int *arr, int n) {//建堆
        for (int i=(n-1)/2;i>=0; --i) {//堆从后往前调整
                heapify(arr, n, i);//堆调整
        }
}
int shellList[14]={1, 9, 34, 182, 836, 4025, 19001, 90358, 428481, 2034035, 9651787, 45806244, 217378076, 1031612713};//希尔排序增量序列
void sort(int* arr,unsigned int length){//简排序(优化版)
        unsigned int temp,gap;
        do{
                gap = length;
                while(gap){
                        for(unsigned int i=0;i+gap<length;++i) {
                                if(arr[i]>arr[i+gap]) {
                                        print(arr,last,N);
                                        temp=arr[i];
                                        arr[i]=arr[i+gap];
                                        arr[i+gap]=temp;
                                }
                        }
                        if(gap<(1<<28)){
                                gap*=6;
                                gap/=7;
                        }else{
                                gap/=7;
                                gap*=6;
                        }
                }
        }while(check(arr,length));
}
void shellSort(int *arr,int l){//希尔排序
        int temp,left,di=13,d=shellList[13];
        while(d>=l){
                --di;
                d=shellList[di];
        }
        while(d){
                for(int right=d;right<l;++right){
                        temp=arr[right];
                        left=right-d;
                        while(left>=0&&arr[left]>temp){
                                print(arr,last,N);
                                arr[left+d]=arr[left];
                                left=left-d;
                        }
                        print(arr,last,N);
                        arr[left+d]=temp;
                }
                --di;
                d=shellList[di];
        }
}
void heapSort(int *arr, int n) {//堆排序
        int temp;
        buildHeap(arr,n);//建堆
        while (--n) {//堆的规模减小堆顶与队列尾交换然后
                print(arr,last,N);
                temp=arr[0];
                arr[0]=arr[n];
                arr[n]=temp;
                heapify(arr,n,0);//堆调整
       }
}
int main(){
        int init=time(NULL);
        //int *arr,*last;
        arr=(int*)malloc(N*sizeof(int));
        last=(int*)malloc(N*sizeof(int));
        clock_t t;
        HWND con = GetConsoleWindow();
        dc = GetDC(con);
//----------------------------------------------------------------------
        srand(init);
        for(int i=0;i<N;++i){
                arr[i]=rand()%600;
        }
        print(arr,last,N);
        printf("组合排:");
        t=clock();
        qsort(arr,N,sizeof(int),cmp);
        printf("%ums,%d错误\n",clock()-t,check(arr,N));
//----------------------------------------------------------------------------
        srand(init);
        for(int i=0;i<N;++i){
                arr[i]=rand()%600;
        }
        print(arr,last,N);
        printf("希尔排:");
        t=clock();
        shellSort(arr,N);
        printf("%ums,%d错误\n",clock()-t,check(arr,N));
//----------------------------------------------------------------------------
        srand(init);
        for(int i=0;i<N;++i){
                arr[i]=rand()%600;
        }
        print(arr,last,N);
        printf("简排:");
        t=clock();
        sort(arr,N);
        printf("%ums,%d错误\n",clock()-t,check(arr,N));
//----------------------------------------------------------------------------
        srand(init);
        for(int i=0;i<N;++i){
                arr[i]=rand()%600;
        }
        print(arr,last,N);
        printf("堆排:");
        t=clock();
        heapSort(arr,N);
        printf("%ums,%d错误\n",clock()-t,check(arr,N));
//----------------------------------------------------------------------------
        free(arr);
        free(last);
        ReleaseDC(con,dc);
        return 0;
}

如果想研究一下"七上八下排序"
就把sort换成文章开始处的代码,修改比例序列
用下面的代码实验。

int main(){
        srand(time(NULL));
        int n = 100120;
        int arr[n];
        double cnt;
        HWND con = GetConsoleWindow();
        HDC dc = GetDC(con);
        for(int i=0;i<=1200;++i){
                SetPixel(dc,i,400,0xffffff);
        }
        for(int l=100000;l<=n;++l){
                cnt=0;
                for(int t=0;t<100;++t){
                        for(int i=0;i<l;++i){
                                arr[i]=rand()%1000;
                        }
                        while(check(arr,l)){
                                sort(arr,l);
                                //qsort(arr,l,sizeof(arr[0]),cmp);
                                cnt++;
                        }
                }
                SetPixel(dc,l%1200,400-cnt,0xffffff);
        }
        ReleaseDC(con,dc);
        return 0;
}

EOF

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值