排序算法之(9)--八种常用排序算法效率对比

介绍

排序是数据处理中一种很重要也很常用的运算,一般情况下,排序操作在数据处理过程中要花费许多时间,为了提高计算机的运行效率,我们提出并不断改进各种各样的排序算法,这些算法也从不同角度展示了算法设计的重要原则和技巧。

分类

这里写图片描述

复杂度

排序方法 时间复杂度(平均) 时间复杂度(最坏) 时间复杂度(最好) 空间复杂度 稳定性
冒泡排序 O ( n 2 ) O ( n 2 ) O ( n ) O ( 1 ) 稳定
选择排序 O ( n 2 ) O ( n 2 ) O ( n 2 ) O ( 1 ) 不稳定
插入排序 O ( n 2 ) O ( n 2 ) O ( n ) O ( 1 ) 稳定
希尔排序 O ( n 1.3 ) O ( n 2 ) O ( n ) O ( 1 ) 不稳定
快速排序 O ( n l o g 2 n ) O ( n 2 ) O ( n l o g 2 n ) O ( n l o g 2 n ) 不稳定
归并排序 O ( n l o g 2 n ) O ( n l o g 2 n ) O ( n l o g 2 n ) O ( n ) 稳定
堆排序 O ( n l o g 2 n ) O ( n l o g 2 n ) O ( n l o g 2 n ) O ( 1 ) 不稳定
基数排序 O ( n k ) O ( n k ) O ( n k ) O ( n + k ) 稳定

代码

除了一个swap和test辅助函数,其余的是八种算法的函数全家桶,不方便看的话可以回到我前面的章节,不过没有讲原理,只有代码。

#include<stdlib.h>
#include<stdio.h>
#include<time.h>
#include<sys/timeb.h>

//#define MAX 250000  //这个数字栈就溢出了
#define MAX 2500000

/*
n种排序方法,都是从大到小排列
没有太参考别人的程序,大部分是根据定义琢磨的
如有疏漏,恳请批评指教
*/

//辅助函数:交换两个变量
void swap(int*a,int*p)
{
    int temp = *a;
    *a = *p;
    *p = temp;
}

//---------------自己实现的各种排序算法----------------
//冒泡排序
//两两对比,把最大的一直挪到最后面
//非常不怎样的一个算法,但确是大部分高校教的第一个算法
void bubbleSort(int* arr,int len)
{
    int i,j;
    //这个是上界
    for(i=0;i<len;i++)
    {
        //一般导数第i+1个以后都已经排好了
        for(j=1;j<len-i;j++)
        {
            //大的一直冒泡到最后面去
            if(arr[j-1]>arr[j])
            {
                swap(&arr[j-1],&arr[j]);
            }
        }
    }
}

//选择排序
//把最小的一个个放到第一第二第三个
//已放好的叫有序区,没放好的是无序区,有序区一旦放好就不会变了
void selectSort(int*arr,int len)
{
    //i大循环,j大小对比
    int i,j;
    int temp,index;//临时对照变量
    for(i=0;i<len;i++)
    {
        //先假设无序区第一个是最小
        temp = arr[i];
        index = i;
        for(j=i+1;j<len;j++)
        {
            //找到无序区真正最小的
            if(arr[j]<temp)
            {
                temp = arr[j];
                index = j;
            }
        }
        //如果第一个就是最小的,就不用进行什么交换
        if(index==i)
        continue;
        //不然就交换无序区第一个数和无序区最小的数
        swap(&arr[i],&arr[index]);
    }
}

//插入排序
//从第1个数开始,往后开始遍历,第i个数一定要放到使得前i个数都变成有序的。
void insertSort(int* arr,int len)
{
    int i,j;
    for(i=0;i<len;i++)
    {
        for(j=i;j>0;j--)
        {
            if(arr[j]<arr[j-1])
            {
                swap(&arr[j],&arr[j-1]);
            }else
            {
                //已经不比那个元素小了就提前退出
                break;
            }
        }
    }
}

//希尔排序
//插入排序的加强版,不是一次性进行插入,而是分成一拨拨来进行
//比如奇数下标的为一拨,偶数下标的为一拨,然后再对分好的两拨进行插入排序
//也就是一开始是隔一定step>1进行插入排序,最后的step=1
//这个步长的变动方式有多种,可是是 step:=step/3+1
//对大量的数据,排序效率明显比插入排序高
void shellSort(int* arr,int len)
{
    int step = len;
    //do while比较好,保证step为1还能再排一次
    do
    {
        //这句一定要放这里,不然步长为1就跳出去了,最后一次无法排序
        step = step/3 +1;
        int i,j,k;
        //分拨排序,一共有step拨
        for(i=0;i<step;i++)
        {
            for(j=i;j<len;j+=step)
            {
                for(k=j;k>i;k-=step)
                {
                    if(arr[k]<arr[k-step])
                    {
                        swap(&arr[k],&arr[k-step]);
                    }else
                    {
                        break;
                    }
                }
            }
        }
    }while(step>1);

}

//快速排序
//顾名思义,排得真的很快。用递归思想
//每次找一个基准数,以其为参考点,比它小的放左边,大的放右边(这两堆内部可能是无序的)
//再把分好的两堆各自找个基准数,按前面的步骤再来,直至数据不能再分,排序完毕
//基准先挖出来,有i,j两个指针,一开始j往左挪,如果遇到比基准小的,填到基准位置
//之后换i往后挪,遇到比基准大的,就放到j的那个坑。全部跑完后,基准丢到最后剩出来的那个坑。
void quickSort(int* arr,int start,int end)
{
    //递归最重要的就是设置退出条件,如下
    if(start>=end)
    {
        return;
    }
    int i = start;
    int j = end;
    int temp = arr[i];
    //如果右指针j一直没小于左指针i,一直跑
    while(i<j)
    {
        //先从右边找比基准小的,找到和基准交换,但要保留j值
        while(i<j)
        {
            if(arr[j]<temp)
            {
                swap(&arr[j],&arr[i]);
                break;
            }
            j--;
        }

        //右边找到一个比基准小的之后,轮到左边找比基准大的,然后和上面空出的j位置交换
        while(i<j)
        {
            if(arr[i]>temp)
            {
                swap(&arr[j],&arr[i]);
                break;
            }
            i++;
        }

    }
    //排左半区
    quickSort(arr,start,i-1);
    //排右半区
    quickSort(arr,i+1,end);
}

//归并排序
//本质上是把两个已经排好的序列合并成一个
//如果对一个随机序列的两两元素来看,那么每个元素都是排好的序列
//可以把一个数组拆分成前后两半来做这件事
//这个算法需要额外的辅助空间,用来存放归并好的结果
void mergeSort(int* arr,int start,int end)
{
    if(start>=end)
    {
        return;
    }
    int i = start;
    int mid = (start+end)/2;
    int j = mid + 1;
    mergeSort(arr,i,mid);
    mergeSort(arr,j,end);

    //合并
    //其实我觉得不用这个额外的空间也行,两个子序列再排一次能减少空间,不过速度肯定会有影响
    int* temp = (int*)malloc((end-start+1)*sizeof(int));
    int index = 0;
    //开始对比两个子序列,头部最小的那个数放到新空间
    while(i<=mid&&j<=end)
    {
        if(arr[i]<=arr[j])
        {
            temp[index++] = arr[i++];
        }else
        {
            temp[index++] = arr[j++];
        }
    }
    //总有一个序列是还没有放完的,这里再遍历一下没放完的
    while(i<=mid)
    {
        temp[index++] = arr[i++];
    }
    while(j<=end)
    {
        temp[index++] = arr[j++];
    }
    //排完再把新空间的元素放回旧空间
    int k = start;
    for(k;k<=end;k++)
    {
        //哎,temp的下标写错,排查了一个钟,真菜
        arr[k] = temp[k-start];
    }
    free(temp);
}

//堆排序
/*
这个堆是数据结构堆,不是内存malloc相关的那个堆--我曾经理解错n久
根节点比孩子节点都大的叫大顶堆(包括子树的根),比孩子节点小的叫小顶堆
从小到大排序用的是大顶堆,所以要先构造这种堆,然后把这个根的最大元素交换到最尾巴去
每次拿走一个最大元素,待排序的队列就慢慢变短
主要步骤1,初始化构造这种大顶堆,把堆顶最大的数放到最尾巴,数列长度减少1,再次构建大顶堆
2,这时候只有堆顶元素不满足大顶堆,那么其实只要从堆顶元素开始慢慢微调而已,没必要再完全重新建堆,想要也可以,不过很浪费时间
理解起来确实很难,涉及到完全二叉树
孩子节点i的爸爸是i/2,爸爸节点的儿子是2i和2i+1。
第一次初始化之后充分利用子树已经是大顶堆
*/
void adjust(int* arr,int len,int index)
{
    //调整函数,把孩子、父亲中的最大值放到父亲节点
    //index为待调整节点下标,一开始设它最大
    int max = index;
    int left = 2*index+1;//左孩子
    int right = 2*index+2;//右孩子
    if(left<len && arr[left] > arr[max])
    {
        max = left;
    }
    if(right<len && arr[right] > arr[max])
    {
        max = right;
    }
    //如果父亲节点不是最大
    if(max!=index)
    {
        //一旦上层节点影响了某个孩子节点,还要观察以这个孩子节点为父节点的子树是不是也不是大顶堆了
        swap(&arr[index],&arr[max]);
        //因为发生了交换,还要继续调整受到影响的孩子节点
        //***************************************
        adjust(arr,len,max);//这句话非常非常关键
        //***************************************
        /*
            只有父亲和孩子节点发生了交换,才有继续调整孩子的必要,如果无脑在不是这里面递归,堆排序的效果不会比冒泡好到哪去
            而如果写在了这里面,虽然还是pk不过快排,但好歹和快排的差距只缩小到个位数倍数的量级(小数据量的时候)
            堆排序一个优点是空间复杂度也不高
        */
    }
}
//主要排序部分
void heapSort(int* arr,int len)
{
    //初始化大顶堆
    //initHeap(arr,i,0);
    //从最后一个非叶子节点开始
    //第一次一定要从下至上一直排,一开始是乱序的
    int i = len/2-1;
    for(i;i>=0;i--)
    {
        adjust(arr,len,i);
    }
    swap(&arr[0],&arr[len-1]);


    //第二次之后,只需要从根节点从上到下调整,遇到没发生交换的直接可以退出循环了
    //微调得到大顶堆(因为只有堆顶不满足而已)
    int j = len -1; //去掉尾节点后的数组长度
    //把最大值交换到最后
    for(j;j>0;j--)
    {
        adjust(arr,j,0);
        swap(&arr[0],&arr[j-1]);
    }
}


//基数排序
//radix sort,说是桶排bucket sort的一种,具体没仔细查证
//对于整数,按个位数先排,再排十位,再排百位..
//需要和原数组一样的额外的空间,用来临时存那些数字
void radixSort(int* arr,int len,int max)
{
    int n = 1;//位数 1 10 100 1000 ...
    int (*bucket)[len] = (int(*)[len])malloc(10*len*sizeof(int));//int[某数位为某尾数的所有数字][某尾数]
    int count[10] = {0};//某个数位0-9
    while(n<=max)
    {
        int i = 0;
        int digit;
        for(i = 0;i<len;i++)
        {
            digit = (arr[i]/n)%10;//某个数位的数字,如n=10,arr[i]为293,则这里就是求十位,即=9
            bucket[digit][count[digit]]= arr[i];
            count[digit] += 1;//记得每次找到一个位数为digit的,数量+1

        }
        //放回原数组
        i = 0;
        for(digit = 0;digit<10;digit++)
        {
            int j = 0;
            for(j = 0;j<count[digit];j++)
            {
                arr[i++] = bucket[digit][j];
            }
            count[digit] = 0;//清空那个桶,准备下次放
        }

        n*=10;//位数10倍变化
    }
    free(bucket);
}

//---------------辅助函数等----------------
//打印数组
void printArr(int* arr,int len)
{
    int i = 0;
    for(i;i<len;i++)
    {
        printf("%d\t",*(arr++));
    }
    printf("\n");
}

//计算时间,精确到毫秒
long getTime()
{
    struct timeb tb;
    ftime(&tb);
    //前面是毫秒,后面是微秒
    return tb.time*1000+tb.millitm;
}

//小型主函数
void test()
{
    //int a[MAX],a1[MAX],a2[MAX],a3[MAX],a4[MAX],a5[MAX],a6[MAX],a7[MAX];
    int* a = (int*)malloc(MAX*sizeof(int));
    int* a1 = (int*)malloc(MAX*sizeof(int));
    int* a2 = (int*)malloc(MAX*sizeof(int));
    int* a3 = (int*)malloc(MAX*sizeof(int));
    int* a4 = (int*)malloc(MAX*sizeof(int));
    int* a5 = (int*)malloc(MAX*sizeof(int));
    int* a6 = (int*)malloc(MAX*sizeof(int));
    int* a7 = (int*)malloc(MAX*sizeof(int));
    int i = 0;
    srand(time(NULL));
    for(i;i<MAX;i++)
    {
        int temp = rand()%(MAX+1);
//        int temp = i;//升序测试
//        int temp = MAX-i-1;//降序测试

        a[i] = temp;
        a1[i] = temp; 
        a2[i] = temp; 
        a3[i] = temp; 
        a4[i] = temp; 
        a5[i] = temp; 
        a6[i] = temp; 
        a7[i] = temp;
    }

//    printArr(a,MAX);

    long t1 = getTime();


    //冒泡排序
//    bubbleSort(a,MAX);
//    printArr(a,MAX);
    long t2 = getTime();
    printf("冒泡排序排%d个随机数据耗时%ld毫秒\n",MAX,t2-t1);

    //选择排序
//    selectSort(a1,MAX);
//    printArr(a1,MAX);
    long t3 = getTime();
    printf("选择排序排%d个随机数据耗时%ld毫秒\n",MAX,t3-t2);

    //插入排序
//    insertSort(a2,MAX);
//    printArr(a2,MAX);
    long t4 = getTime();
    printf("插入排序排%d个随机数据耗时%ld毫秒\n",MAX,t4-t3);

    //希尔排序
    shellSort(a3,MAX);
//    printArr(a3,MAX);
    long t5 = getTime();
    printf("希尔排序排%d个随机数据耗时%ld毫秒\n",MAX,t5-t4);

    //快速排序
    quickSort(a4,0,MAX-1);
//    printArr(a4,MAX);
    long t6 = getTime();
    printf("快速排序排%d个随机数据耗时%ld毫秒\n",MAX,t6-t5);

    //归并排序
    mergeSort(a5,0,MAX-1);
//    printArr(a5,MAX);
    long t7 = getTime();
    printf("归并排序排%d个随机数据耗时%ld毫秒\n",MAX,t7-t6);

    //堆排序
    heapSort(a6,MAX);
//    printArr(a6,MAX);
    long t8 = getTime();
    printf("堆排序排%d个随机数据耗时%ld毫秒\n",MAX,t8-t7);

    //基数排序
    radixSort(a7,MAX,MAX);
    printArr(a7,MAX);
    long t9 = getTime();
    printf("基数序排%d个随机数据耗时%ld毫秒\n",MAX,t9-t8);

}

void main()
{
    test();
}

运行结果:

数据量较小的时候

150000个随机数:
这里写图片描述
250000个随机数:
这里写图片描述

数据量较大的时候

这里写图片描述
加入了个新成员基数排序
250w个随机数:
这里写图片描述
2500w个数据:
这里写图片描述
最后一个挂掉的是基数排序,我的内存只有2G,基数排序用了桶的原理,需要额外空间。
千万不要拿冒泡去试上面这个数据量,除非你真的很闲。

数据原本有序且是升序的时候

这里写图片描述

数据原本有序且是降序的时候

这里写图片描述


后话

注意,如果原本的序列就是有序(升序或降序)的,快排基本就是和冒泡是兄弟了,慢到让你怀疑人生。希尔、归并、堆排、基数基本影响不大。可见如果数据本来就只有微小的无序,还是不要用快排了。

猜你喜欢

转载自blog.csdn.net/hiudawn/article/details/80380572