排序算法之(9)--八種常用排序算法效率對比

介紹

排序是數據處理中一種很重要也很常用的運算,一般情況下,排序操作在數據處理過程中要花費許多時間,爲了提高計算機的運行效率,我們提出並不斷改進各種各樣的排序算法,這些算法也從不同角度展示了算法設計的重要原則和技巧。

分類

這裏寫圖片描述

複雜度

排序方法 時間複雜度(平均) 時間複雜度(最壞) 時間複雜度(最好) 空間複雜度 穩定性
冒泡排序 O(n2) O(n2) O(n) O(1) 穩定
選擇排序 O(n2) O(n2) O(n2) O(1) 不穩定
插入排序 O(n2) O(n2) O(n) O(1) 穩定
希爾排序 O(n1.3) O(n2) O(n) O(1) 不穩定
快速排序 O(nlog2n) O(n2) O(nlog2n) O(nlog2n) 不穩定
歸併排序 O(nlog2n) O(nlog2n) O(nlog2n) O(n) 穩定
堆排序 O(nlog2n) O(nlog2n) O(nlog2n) O(1) 不穩定
基數排序 O(nk) O(nk) O(nk) 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,基數排序用了桶的原理,需要額外空間。
千萬不要拿冒泡去試上面這個數據量,除非你真的很閒。

數據原本有序且是升序的時候

這裏寫圖片描述

數據原本有序且是降序的時候

這裏寫圖片描述


後話

注意,如果原本的序列就是有序(升序或降序)的,快排基本就是和冒泡是兄弟了,慢到讓你懷疑人生。希爾、歸併、堆排、基數基本影響不大。可見如果數據本來就只有微小的無序,還是不要用快排了。

發佈了96 篇原創文章 · 獲贊 100 · 訪問量 26萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章