八大排序算法及其比較

八大排序算法及其比較

排序算法能夠分爲內部排序和外部排序,內部排序是數據記錄在內存中進行排序,而外部排序是因排序的數據很大,一次不能容納所有的排序記錄,在排序過程當中須要訪問外存。html

常見的內部排序算法有:冒泡排序、選擇排序、插入排序、希爾排序、堆排序、快速排序、歸併排序、基數排序等。git

前面三種是簡單排序,後續算法是在前面基礎上進行優化。本文將基於C語言,依次介紹上述八大排序算法。github

1 八大排序算法

1.1 冒泡排序

主要思想:依次對兩個數比較大小,較大的數冒起來,較小的數壓下來。算法

形象理解:一隊新兵N我的整齊站成一列,教官想讓他們按照身高排好隊,看起來更協調,因而從前走到後走一趟,每次遇到相鄰的兩我的身高不協調時,就讓兩人互換位置。當走完一趟時,個子最高的人就被排到了最後。教官回到前排後發現隊伍仍然不協調,因而又按照原樣走了一趟。這樣循環走了N-1趟以後,教官終於滿意了。(注意:每次走一趟時,以前排到後面的高個子就不參與此次排序了;有時候可能還沒走完N-1趟,教官就發現隊伍已經協調了,因而排序結束。)數組

特色:簡單易懂,排序穩定,但速度慢。數據結構

完整代碼 ```cpp #include #include void Swap_Two(int *p1, int *p2); //交換兩個整數 void Bubble_Sort(int A[], int N); //冒泡排序 int main() { int N, i; int *p = NULL; //將N個長整數存儲至數組中 scanf("%d", &N); //讀入個數N p = malloc(sizeof(int) * N); //申請數組 for(i=0; i 0; k--){ //跑N-1趟 flag = 0; //設置標記,若是發現某一趟沒有交換數據,說明還未排序的序列已經有序了 for(i=0; i A[i+1]){ Swap_Two(&A[i], &A[i+1]); flag = 1; } } if(flag == 0) break; } return; } ```

1.2 選擇排序

主要思想:針對冒泡排序,有一個地方能夠優化,即在跑一趟的過程當中,不必兩兩交換,能夠先記下最小值,跑完一趟後直接將最小值換到前面。函數

特色:比冒泡更快一些,但代價是跳躍性交換,排序不穩定。測試

完整代碼

#include <stdio.h>
#include <stdlib.h>

void Swap_Two(int *p1, int *p2); //交換兩個整數
void Select_Sort(int A[], int N); //選擇排序

//主函數,其他相似
int main()
{
    int N, i;
    int *p = NULL;
    //將N個整數存儲至數組中
    scanf("%d", &N); //讀入個數N
    p = (int *)malloc(sizeof(int) * N); //申請數組
    for(i=0; i<N; i++){
        scanf("%d", &p[i]);
    }
    //排序並打印
    Select_Sort(p, N);
    for(i=0; i<N; i++){
        printf("%d", p[i]);
        if(i != N-1)
            printf(" ");
    }

    free(p);
    return 0;
}

void Swap_Two(int *p1, int *p2){ //交換兩個長整型數
    int temp;
    temp = *p1;
    *p1 = *p2;
    *p2 = temp;
    return;
}

void Select_Sort(int A[], int N){ //選擇排序
    int i, j, min_idx;
    for(i=0; i<N-1; i++){
        min_idx = i;  //初始化最小值索引
        for(j=i+1; j<N; j++){
            if(A[j] < A[min_idx])  //若待排序列中有比當前最小值還小的,則更新最小值索引
                min_idx = j;
        }
        if(min_idx != i) //若更新過最小值索引
            Swap_Two(&A[i], &A[min_idx]);
    }
    return;
}

1.3 插入排序

主要思想:過程跟拿牌同樣,依次拿N張牌,每次拿到到牌後,從後往前看,遇到合適位置就插進去。最終手上的牌從小到大。大數據

特色:當數據規模較小或者數據基本有序時,效率較高。優化

完整代碼

void Swap_Two(int *p1, int *p2){ //交換兩個整數
    int temp;
    temp = *p1;
    *p1 = *p2;
    *p2 = temp;
    return;
}

void Insert_Sort(int A[], int N){ //直插排序
    int temp, i, k;
    for(i=1; i<N; i++){
        temp = A[i];
        for(k=i-1; k>=0 && temp<A[k]; k--){
            A[k+1] = A[k];
        }
        A[k+1] = temp;
    }
    return;
}

1.4 希爾排序

主要思想:設增量序列個數爲k,則進行k輪排序。每一輪中,按照某個增量將數據分割成較小的若干組,每一組內部進行插入排序;各組排序完畢後,減少增量,進行下一輪的內部排序。
特色:針對插入排序的改進,當數據規模較大或無序時也比較高效。精妙之處在於,能夠同時構造出兩個特殊的有利條件(數據量小,基本有序),一個有利條件弱時,另一個有利條件就強。(剛開始時雖然每組有序度低,但其數據量小;隨着每輪的增量逐漸壓縮,雖然各組數據量逐漸變大,但其有序度逐漸增長。)

完整代碼

void Shell_Sort(int A[], int N){ //希爾排序
    int k, i, j, p, temp;
    int t = 0;
    int D[33]; //假定增量序列不超過2^32
    //定義Hibbard增量序列
    for(k=1; k<33; k++){
        t = 2 * t + 1; //增量序列項
        if(t < N){
            D[k] = t;
        }else{
            break;
        }
    }
    //進行k-1(增量序列的個數)趟插排
    for(p=k-1; p>=1; p--){
        for(i=D[p]; i<N; i++){
            temp = A[i];
            for(j=i-D[p]; j>=0 && temp<A[j]; j-=D[p]){
                A[j+D[p]] = A[j];
            }
            A[j+D[p]] = temp;
        }
    }

    return;
}

1.5 堆排序

主要思想:將待排數組構建成一個最大堆,將堆頂最大元素換到後面,而後堆容量減1;相似進行N-1次操做便可。

完整代碼 ```cpp void Perc_Down(int A[], int N, int i){ //在由N個結點組成的徹底二叉樹中,下濾第i個結點(從第0個結點算起),暫不作有效性檢查 int parent, child, temp; temp = A[i]; for(parent=i, child=2*parent+1; child A[child]) child++; //若右兒子存在,且數值更大,則child爲右兒子 if(temp < A[child]) A[parent] = A[child]; else break; } A[parent] = temp; return; } void Heap_Sort(int A[], int N){ //堆排序 int i; //調整成最大堆 for(i=(N-2)/2; i>=0; i--){ Perc_Down(A, N, i); //在由N個結點組成的徹底二叉樹中,下濾結點i } //開始從後向前進行排序,注意每次交換後下濾時排除掉剛交換的最後一個 for(i=N-1; i>0; i--){ if(A[0] > A[i]){ Swap_Two(&A[0], &A[i]); Perc_Down(A, i, 0); } } return; } ```

1.6 快速排序

主要思想:分治思想。選一基準元素,依次將剩餘元素中小於該基準元素的值放置其左側,大於等於該基準元素的值放置其右側;而後,取基準元素的前半部分和後半部分分別進行一樣的處理;以此類推,直至各子序列剩餘一個元素時,即排序完成。

注意:對於小規模數據(n<100),快排因爲用了遞歸,其效率可能還不如插排。所以一般能夠定義一個閾值,當遞歸的數據量很小時中止遞歸,直接調用插排。

完整代碼 ```cpp void Quick_Sort(int A[], int N){ //快速排序接口 Quick_Sort_Core(A, 0, N-1); return; } void Quick_Sort_Core(int A[], int left, int right){ //快排遞歸程序 if(left >= right) return; //若待排序列不到2個元素,則直接返回 int low, high, mid, temp; //在三個點(端點和終點)中找出中間值,做爲劃分點。(也能夠直接將left做爲劃分點,但若待排序列基本有序時就容易退化成冒泡) mid = (left + right) / 2; if(A[left] > A[mid]) Swap_Two(&A[left], &A[mid]); if(A[left] > A[right]) Swap_Two(&A[left], &A[right]); if(A[mid] > A[right]) Swap_Two(&A[mid], &A[right]); if(mid == left) return; //若待排序列只有兩個元素,此時已排好序,直接返回 temp = A[mid]; //保存劃分點 //至此三個點已有序 //將空格移至left右側,開始啓動大循環 A[mid] = A[left+1]; low = left + 1; high = right - 1; while(low < high){ //跳出大循環時確定有low==high while(low < high && A[high] >= temp) high--; A[low] = A[high]; while(low < high && A[low] <= temp) low++; A[high] = A[low]; } A[low] = temp; Quick_Sort_Core(A, left, low-1); Quick_Sort_Core(A, low+1, right); return; } ```

1.7 歸併排序

主要思想:相似兩個有序鏈表的合併,每次兩兩合併相鄰的兩個有序序列,直至整個序列有序。

完整代碼 ```cpp void Merge_Sort(int A[], int N){ //歸併排序 int len = 1; //有序序列的長度 int i, start, count = 0; int *temp, *A1, *A2; temp = malloc(N * sizeof(int)); //定義一個數組,做爲A的副本,用於相互倒騰 while(len < N){ //如有序序列長度小於總長度 start = 0; //初始化起始位置 //先肯定A1和A2 if(count % 2 == 0){ A1 = A; A2 = temp; }else{ A1 = temp; A2 = A; } //根據有序序列長度,分別將A1中元素按成對列的方式合併至A2中 while(start + 2 * len <= N){ //注意start + 2 * len的實際含義是下標位置,可理解等號的緣由 Merge_Sequence(A1, A2, start, start+len, start+2*len-1); start += 2 * len; } //處理A1中剩下的尾巴 if(start + len < N) //若還有兩個序列 Merge_Sequence(A1, A2, start, start+len, N-1); else //若只剩一個序列。注意大bug:剛開始忘了這種狀況 while(start < N){ A2[start] = A1[start]; start++; } //更新參數 len = 2 * len; count++; } //若是最後數據存儲在temp中,則須要將數據複製到A中 if(count % 2 == 1){ for(i=0; i start1 && end>=start2 int end1, end2, p; end1 = start2 - 1; end2 = end; p = start1; //p指向A2中起始位置 //開始合併 while((start1 <= end1) && (start2 <= end2)){ if(A1[start1] <= A1[start2]) //注意帶等號保證穩定性 A2[p++] = A1[start1++]; else A2[p++] = A1[start2++]; } //處理尾巴 while(start1 <= end1) A2[p++] = A1[start1++]; while(start2 <= end2) A2[p++] = A1[start2++]; return; } ```

1.8 基排序

主要思想:基數排序是按照低位先排序,而後收集;再按照高位排序,而後再收集;依次類推,直到最高位。

完整代碼

#define Radix 10
#define MaxDigit 4

typedef struct ENode{ //定義元素結點
    int data;
    struct ENode *next;
}*PtrToNode;
typedef struct BucketNode{ //定義桶結點結構
    PtrToNode head;
    PtrToNode tail;
}*Bucket;

void LSDRadix_Sort(int A[], int N){ //基排序
    int i, D; //D爲位, d爲第D位上的數字(範圍爲0到9)
    //定義兩個大小爲Radix的桶,並初始化
    Bucket B1 = malloc(Radix * sizeof(struct BucketNode));
    Bucket B2 = malloc(Radix * sizeof(struct BucketNode));
    for(i=0; i<Radix; i++){
        B1[i].head = B1[i].tail = B2[i].head = B2[i].tail = NULL;   //注意大bug:B1爲桶指針,可是B1[i]爲結構體
    }
    //將數組中元素按照倒數第D位數字分別掛到桶B1上
    D = 1;
    Transfer_Array_To_Bucket(A, B1, N, D);
    D++;
    //開始相互倒騰,每倒騰一趟就往前看一位
    while(D <= MaxDigit){
        if(D % 2 == 0)
            Transfer_Bucket_To_Bucket(B1, B2, D);
        else
            Transfer_Bucket_To_Bucket(B2, B1, D);
        D++;
    }
    //倒騰結束後將桶中的元素依次倒入數組A中
    if(D % 2 == 0)
        Transfer_Bucket_To_Array(B1, A, N);
    else
        Transfer_Bucket_To_Array(B2, A, N);
    //釋放兩個桶及其元素
    Free_Bucket(B1);
    Free_Bucket(B2);
    return;
}

void Transfer_Array_To_Bucket(int A[], Bucket B, int N, int D){ //將數組A中元素按照第D位掛到桶B中
    int i, d;
    PtrToNode temp;
    for(i=0; i<N; i++){
        d = Get_Digit(A[i], D); //獲取A[i]倒數第D位數字
        temp = malloc(sizeof(struct ENode)); //建立一個新結點並初始化
        temp->data = A[i];
        temp->next = NULL;
        if(B[d].tail == NULL){ //若桶的d位置上爲空
            B[d].head = B[d].tail = temp;
        }else{
            B[d].tail->next = temp;
            B[d].tail = temp;
        }
    }
    return;
}

void Transfer_Bucket_To_Array(Bucket B, int A[], int N){ //將桶B1中元素依次倒入數組A中,最多不超過N
    int k, i = 0;
    PtrToNode p;

    for(k=0; k<Radix; k++){
        p = B[k].head;
        while(p != NULL){
            if(i < N){
                A[i++] = p->data;
                p = p->next;
            }else{
                return;
            }
        }
    }
    return;
}

void Transfer_Bucket_To_Bucket(Bucket B1, Bucket B2, int D){ //依次將桶B1中元素按照第D位掛到B2中,
    int k, d;
    PtrToNode p, temp;
    //依次從B1中取下元素結點,根據D位數字掛到B2上
    for(k=0; k<Radix; k++){
        p = B1[k].head;
        while(p != NULL){
            //從B1中取該元素,並獲取第D位數字
            temp = p;
            p = p->next;
            d = Get_Digit(temp->data, D); //獲取該結點第D位數字
            //將該元素掛到B2上
            temp->next = NULL; //確定做爲本次轉移的尾結點
            if(B2[d].head == NULL){ //若B2[d]爲空位置
                B2[d].head = B2[d].tail = temp;
            }else{ //若B2[d]不爲空位置,則將temp結點接到尾結點上
                B2[d].tail->next = temp;
                B2[d].tail = temp;
            }
        }
        //取完B1[k]上的全部結點後,設置其首尾結點指針爲空
        B1[k].head = B1[k].tail = NULL;
    }

    return;
}

int Get_Digit(int X, int D){  //獲取X倒數第D位數字
    int k, i;
    for(i=0; i<D-1; i++)
        X = X / Radix;
    k = X % Radix;
    return k;
}

2 八大排序算法比較

複雜度與穩定性的比較

算法類別 平均時間複雜度 最好狀況複雜度 最壞狀況複雜度 空間複雜度 穩定性
冒泡排序 \(O(n^2)\) \(O(n)\) \(O(n^2)\) \(O(1)\) 穩定
選擇排序 \(O(n^2)\) \(O(n^2)\) \(O(n^2)\) \(O(1)\) 不穩定
插入排序 \(O(n^2)\) \(O(n)\) \(O(n^2)\) \(O(1)\) 穩定
希爾排序 \(O(n^{1.5})\) - - \(O(1)\) 不穩定
堆排序 \(O(n log(n))\) \(O(n log(n))\) \(O(n log(n))\) \(O(1)\) 不穩定
快速排序 \(O(n log(n))\) \(O(n log(n))\) \(O(n^2)\) \(O(1)\) 不穩定
歸併排序 \(O(n log(n))\) \(O(n log(n))\) \(O(n log(n))\) \(O(n)\) 穩定
基排序 \(O(d*n)\) \(O(d*n)\) \(O(d*n)\) \(O(n)\) 穩定

如下是基於浙大數據結構課練習題測試(09-排序1 排序 (25 分))。

參考:

https://blog.csdn.net/qq_39207948/article/details/80006224

https://blog.csdn.net/qq_43152052/article/details/100078825

https://www.cnblogs.com/onepixel/articles/7674659.html

https://www.runoob.com/w3cnote/sort-algorithm-summary.html

https://github.com/francistao/LearningNotes/blob/master/Part3/Algorithm/Sort/%E9%9D%A2%E8%AF%95%E4%B8%AD%E7%9A%84%2010%20%E5%A4%A7%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95%E6%80%BB%E7%BB%93.md

相關文章
相關標籤/搜索