數據結構與算法--聊聊那些常見的排序算法

前言

  在開發中會經常用到排序,經常用到排序比如:冒泡排序,選擇排序,直接插入排序等。

那什麼是排序呢?這個其實都很熟悉了,其實排序還分爲內排序外排序

內排序:在排序整個過程中,待排序的所有記錄全部被放置在內存中

外排序:由於排序的記錄個數太多,不能同時放置在內存,整個排序過程需要在內外存 之間多次交換數據才能進⾏。

常用的是內排序。接下來聊聊常見的排序算法。
在排序的過程過程中進行比較,然後交換是不可避免的。

所以可以先設計一個公共的交換函數,利用哨兵思想來設計一次數據結構,第0個位置不做數據存儲,作爲哨兵或者臨時遍歷使用,具體代碼如下:

#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0

typedef int Status;

// 排序算法數據結構設計
#define MAXSIZE 10000
typedef struct
{
    // 用於存儲要排序數組,r[0]用作哨兵或臨時變量
    int r[MAXSIZE+1];
    // 用於記錄順序表的長度
    int length;
}SqList;


// 常用交換函數
// 交換L中數組r的下標爲i和j的值
void swap(SqList *L,int i,int j)
{
    int temp = L->r[i];
    L->r[i]  = L->r[j];
    L->r[j]  = temp;
}

// 打印
void print(SqList L)
{
    int i;
    for(i=1;i<L.length;i++)
        printf("%d,",L.r[i]);
    printf("%d",L.r[i]);
    printf("\n");
}

1. 冒泡排序

冒泡排序:是一種交換排序,兩兩比較相鄰記錄的關鍵字,如果反序則交換,直到沒有反序的記錄爲止。

在冒泡排序的實現時,可能會寫成下面的形式:

// 冒泡排序-(冒泡排序初級版本)
void BubbleSort0(SqList *L){
   
    int i,j;
    for (i = 1; i < L->length; i++) {
        for (j = i+1; j <= L->length; j++) {
            if(L->r[i] > L->r[j])
                swap(L, i, j);
        }
    }
    
}

其實上面的代碼嚴格的來說並不是冒泡排序,是對順序表L進行交換排序,因爲並不滿足兩兩比較,所以對其進行改進,如下:

// 冒泡排序-對順序表L作冒泡排序(正宗冒泡排序算法)
void BubbleSort(SqList *L){
    int i,j;
    for (i = 1; i < L->length; i++) {
        // ✅ j是從後面往前循環
        for (j = L->length-1; j >= i; j--) {
            
            // 若前者大於後者(注意與上一個算法區別所在)
            if(L->r[j]>L->r[j+1])
                //交換L->r[j]與L->r[j+1]的值;
                swap(L, j, j+1);
        }
    }
}

其實,還可以對冒泡排序進行優化,如果這個數據交換一次時,是有序的,那麼後面的比較是重複無意義的。我們可以用一個值來標記是否有序。

// 冒泡排序-對順序表L冒泡排序進行優化
void BubbleSort2(SqList *L){
    int i,j;
    // flag用作標記
    Status flag = TRUE;
    
    // i從[1,L->length) 遍歷;
    // 如果flag爲False退出循環. 表示已經出現過一次j從L->Length-1 到 i的過程,都沒有交換的狀態;
    for (i = 1; i < L->length && flag; i++) {
        
        // flag 每次都初始化爲FALSE
        flag = FALSE;
        for (j = L->length-1; j>=i; j--) {
            
            if(L->r[j] > L->r[j+1]){
            //交換L->r[j]和L->r[j+1]值;
            swap(L, j, j+1);
            //如果有任何數據的交換動作,則將flag改爲true;
            flag=TRUE;
            }
        }
    }
}

2. 簡單選擇排序

簡單排序算法:就是通過n-i次關鍵詞比較,從n - i +個記錄中找到關鍵字最小的記錄,並和第i(1<i<n)個記錄進行交換。


如上圖,先從下標(1-9)中找到最小記錄,i=1, min=2,然後和第一個記錄交換,得到如下:

然後,i=2, min = 9,和第二個記錄交換:

依次類推,進行比較,最終完成排序。

代碼實現:

// 選擇排序--對順序表L進行簡單選擇排序
void SelectSort(SqList *L){
    
    int i,j,min;

    for (i = 1; i < L->length; i++) {
        //✅ 1.將當前下標假設爲最小值的下標
        min = i;
        //✅ 2.循環比較i之後的所有數據
        for (j = i+1; j <= L->length; j++) {
            //✅ 3.如果有小於當前最小值的關鍵字,將此關鍵字的下標賦值給min
            if (L->r[min] > L->r[j]) {
                min = j;
            }
        }
        
        //✅ 4.如果min不等於i,說明找到了最小值,則交換2個位置下的關鍵字
        if(i!=min)
            swap(L, i, min);
    }
}

3. 直接插入排序

直接插入排序:是將一個記錄插入到已經排好序的有序表中,從而得到一個新的記錄數增加1的有序表

如上圖:

  1. 循環將i從第二個元素到最後一個元素作爲待排序元素
  2. 判斷當前待排序元素是否小於其前一個元素(i-1),小於,則參與插入排序
  3. 使用臨時遍歷變量temp,存儲待排序元素,(在本次循環中temp = 3
  4. 循環遍歷,找到第二個元素之前,能插入的位置,判斷依據是從i-1到0這個空間,滿足L->r[j] > temp, 則將L->r[j+1] = L->r[j]
  5. 找到元素5 > temp, 需要把5往前⾯面移動,覆蓋元素3


6. 此時r[0] 不大於temp 則j層循環結束. 目前 j = 0
7. 此時需要把 3覆蓋到j=1的位置,但是由於j 退出循環時等於0, 所以是r[j+1] = temp

最終完成本次循環,如下:

然後依次i++,參照上面的步驟,最終完成排序。具體實現如下:

// 直接插入排序算法
void InsertSort(SqList *L){
    int i,j;
    //L->r[0] 哨兵 可以把temp改爲L->r[0]
    int temp=0;
    
    //假設排序的序列集是{0,5,4,3,6,2};
    //i從2開始的意思是我們假設5已經放好了. 後面的牌(4,3,6,2)是插入到它的左側或者右側
    for(i=2;i<=L->length;i++)
    {
        //需將L->r[i]插入有序子表
        if (L->r[i]<L->r[i-1])
        {
            //設置哨兵 可以把temp改爲L->r[0]
            temp = L->r[i];
            for(j=i-1;L->r[j]>temp;j--)
                    //記錄後移
                    L->r[j+1]=L->r[j];
            
            //插入到正確位置 可以把temp改爲L->r[0]
            L->r[j+1]=temp;
        }
    }
}

空間複雜度: O(1)

時間複雜度: O(n2)

4. 希爾排序

希爾排序思想:在插入排序之前,將整個序列調整爲基本有序,然後再對全體序列進行一次直接插入排序。

那麼怎麼將序列調整爲基本有序呢?
希爾排序是把記錄按照下標的一定增量分組,對每組直接使用插入排序,
;隨着增量逐漸減少,每組包含的關鍵字越來越多,當增量減爲1時,整個序列被分爲1組,算法終止。

假設,有下面的一組序列,按照希爾排序的原理,對其進行分組:

初始化增量爲increment = Length / 2 = 5,每組對應不同的顏色,即分爲{8,3},{9,5},{1,4},{7,6},{2,0}五組,然後對每組進行插入排序,那麼此時3、5、6、0,這些小元素會被調整到前面。

然後縮小增量(第一次循環增量爲5),increment = increment / 2= 5/2 = 2,增量爲2,即數組被分爲兩組:{3,1,0,9,7} {5,6,8,4,2}

然後對這2個序列進行直接插⼊排序,結果爲:{0,1,3,7,9} {2,4,5,6,8},最終結果如下:

然後縮小增量(第二次循環增量爲2),increment = increment / 2= 2/2 = 1,增量爲1,即數組被分爲一組,對這個序列直接進行插入排序如下,最終完成排序。

思路(僞代碼):

1. 初始化增量爲整個序列的長度
2. 開始循環,對序列根據增量進行分組,每組進行插入排序,當增量大於1時結束循環
3. 增量序列 = 增量序列/3 + 1
4. 循環每個分組,判斷分組中,是否需要交換,需要則按照插入排序交換對應位置的元素。
// 希爾排序
void shellSort(SqList *L){
    int i,j;
    // ✅ 初始化增量爲整個序列的長度
    int increment = L->length;
    
    //0,9,1,5,8,3,7,4,6,2
    // ✅ 開始循環,當increment 爲1時,表示希爾排序結束
    do{
        // ✅ 增量序列
        increment = increment/3+1;
        // ✅ i的待插入序列數據 [increment+1 , length]
        for (i = increment+1; i <= L->length; i++) {
            // 如果r[i] 小於它的序列組元素則進行插入排序,例如3和9. 3比9小,所以需要將3與9的位置交換
            // ✅ 判斷,然後進行插入排序
            if (L->r[i] < L->r[i-increment]) {
                // 將需要插入的L->r[i]暫時存儲在L->r[0].和插入排序的temp 是一個概念;
                L->r[0] = L->r[i];
                // 記錄後移
                for (j = i-increment; j > 0 && L->r[0]<L->r[j]; j-=increment) {
                    L->r[j+increment] = L->r[j];
                }
                
                // 將L->r[0]插入到L->r[j+increment]的位置上;
                L->r[j+increment] = L->r[0];
            }
        }
    }while (increment > 1);
}

5. 堆排序

是具有一下性質的完全二叉樹

  • 每個結點的值都大於或者等於其左右孩子結點的值,稱爲大頂堆
  • 每個結點的值都小於或者等於其左右孩子結點的值,稱爲小頂堆

如果按照層尋遍歷的方式給結點從1開始編號,則結點之間滿足以下關係:

堆排序就是利用堆(假設選擇大頂堆)進行排序的算法,其基本思想如下:

  • 將待排序的序列構成一個大頂堆,此時,整個序列最大值就的堆頂的根節點,將其有堆數組的末尾元素交換,此時末尾元素爲最大
  • 然後將剩餘的n-1個序列重新構成一個堆,這樣就會得到n個元素的次大值, 如此重複執行,就能得到⼀個有序列

接下來以序列{4,6,8,5,9}爲例,詳細的分析一下:

  1. 構造初始堆,將給定⽆序列構造成一個⼤頂堆(一般升序採⽤大頂堆,降序採用小頂堆)
    A. 給的無序序列結構如下:

  B. 從最後一個非葉子結點開始(葉子結點不用調整),第一個非葉子結點2

   結點2上數據 6 大於左子樹結點數據5,小於其右子樹結點數據9,所以要將9 和 6 互換。

  C. 找到第二個非葉子結點4,從[4,9,8]中找到最大的進行交換。

  D. 因爲49的交換,導致【4,5,6】結構混亂,不符合大頂堆條件,需要繼續調整,交換46。至此,經過上面的調整,我們將無序列 調整成⼀個⼤頂堆結構。

  1. 將堆頂元素和末尾元素進行交換,使末尾元素最大,然後繼續調整堆,再將堆頂元素與末尾元素交換,得到第⼆大元素。如此反覆進行交換、重建、交換,

    A. 將堆頂元素9和末尾元素4交換,此時末尾元素9,將不參與後續排序

    B. 重新調整結構,使其繼續滿⾜堆定義[ 4, 6 , 8]中找到最大的, 48進行交換. 經過調整得到大頂堆

    C. 再將堆頂元素8與末尾元素5進行交換,得到第⼆大元素8,然後繼續上面的步驟進行調整交換,最終得到如下的有序序列

堆排序思路

  • 將無需序列構建成一個堆,根據升降序,選擇構建大頂堆或者小頂堆(升序,大頂堆,降序,小頂堆)
  • 將堆頂元素與末尾元素交換,將最⼤元素或者最小元素“沉”到數組末端
  • 重新調整使之滿足堆定義,繼續交換堆頂和當前末尾元素;反覆,直到序列有序

在構建大頂堆時,從最後一個非葉子開始,由於堆是一個完全二叉樹,其結點按層序編號,對任⼀結點i (1 ≤ i ≤ n)有:

  • 如果 i=1,則結點 i 是⼆叉樹的根. 無雙親結點。 如果i > 1,則其雙親是結點 [ i / 2 ]
  • 如果 2i > n ,則結點 i ⽆左孩子 (結點i 爲葉⼦結點), 否則左孩⼦子是結點 2i
  • 如果 2i + 1 > n ,則結點 i ⽆右孩子; 否則其右孩⼦子是結點 2i+1

接下來實現一下大頂堆調整函數:

// 大頂堆調整函數
void HeapAjust(SqList *L,int s,int m){
    
    int temp,j;
    //1. 將L->r[s] 存儲到temp ,方便後面的交換過程;
    temp = L->r[s];
    
    //2. 
    //因爲這是顆完全二叉樹,而s也是非葉子根結點. 所以它的左孩子一定是2*s,而右孩子則是2s+1
    for (j = 2 * s; j <=m; j*=2) {
        
        //3. ✅判斷j是否是最後一個結點, 並且找到左右孩子中最大的結點;
        //如果左孩子小於右孩子,那麼j++; 否則不自增1. 因爲它本身就比右孩子大;
        if(j < m && L->r[j] < L->r[j+1])
            ++j;
        
        //4. ✅比較當前的temp 是不是比較左右孩子大;如果大則表示我們已經構建成大頂堆了,跳出循環
        if(temp >= L->r[j]) {
            break;
        }
        //5. ✅小於,則將L->[j] 的值賦值給非葉子根結點
        L->r[s] = L->r[j];
        //6. ✅將s指向j; 因爲此時L.r[4] = 60, L.r[8]=60. 那我們需要記錄這8的索引信息.等退出循環時,能夠把temp值30 覆蓋到L.r[8] = 30. 這樣才實現了30與60的交換;
        s = j;
    }
    
    //7. ✅將L->r[s] = temp. 其實就是把L.r[8] = L.r[4] 進行交換;
    L->r[s] = temp;
}

堆排序實現:

// 堆排序--對順序表進行堆排序
void HeapSort(SqList *L){
    int i;
   
    //✅ 1.將現在待排序的序列構建成一個大頂堆;
    //將L構建成一個大頂堆;
    //i從length/2.因爲在對大頂堆的調整其實是對非葉子的根結點調整.
    for(i=L->length/2; i>0;i--){
        HeapAjust(L, i, L->length);
    }
    
    
    //✅ 2.逐步將每個最大的值根結點與末尾元素進行交換,並且再調整成大頂堆
    for(i = L->length; i > 1; i--){
        
        //✅ 將堆頂記錄與當前未經排序子序列的最後一個記錄進行交換;
        swap(L, 1, i);
        //✅ 將L->r[1...i-1]重新調整成大頂堆;
        HeapAjust(L, 1, i-1);
    }
}

堆排序的時間複雜度爲:O(nlogn)

堆排序是就地排序,空間複雜度爲常數:O(1)

6. 歸併排序

歸併排序是利用歸併的思想實現排序,它的原理是假設初始序列含有n個記錄,則可以看成n個有序的子序列,每個子序列的長度爲1,然後兩兩合併,得 到[n/2]個長度爲21的有序子序列。再兩兩歸併,如此重複,直到得到一個長度爲n 的有序列爲此,這種排序方法稱爲2路路歸併排序

如下圖,將序列依次拆分爲長度爲1子序列,然後在兩兩歸併,得到四個長度爲2的有序序列,然後再兩兩歸併,得到2個長度爲4的有序序列,再歸併爲一個有序序列

接下來分析一下歸併排序的執行流程:

假設對下面的一個無序序列進行歸併排序


首先low = 1,hight = 9,求得mid = (low + hight)/2 = 5,然後將原序列拆分爲下面兩個序列,


然後對[low-mid][mid+1-hight]的兩個序列遞歸拆分,最終拆分爲長度爲1的子序列。
然後開始兩兩合併。

我們來着重分析一下最後兩個子序列的合併:

  • 第一次循環,SR[i] = SR[1] = 10SR[j] = SR[6] = 20 進⾏比較。

    SR[i] < SR[j],那麼將 TR[k] = SR[i];此時 i++, k++,

    那麼如果是 SR[i] > SR[j]的話,將 SR[j] 存儲到TR[k] 這個數組。就是j++, k++
    第一次循環結束:i = 2,m = 5,j = 6,n = 9

  • 第二次循環,SR[i] = SR[2] = 30SR[j] = SR[6] = 20 進⾏比較。

    SR[i] > SR[j],那麼將 TR[k] = SR[j];此時 j++, k++,

    第二次循環結束:i = 2,m = 5,j = 7,n = 9


  • 第三次循環,SR[i] = SR[2] = 30SR[j] = SR[7] = 40 進⾏比較。

    SR[i] < SR[j],那麼將 TR[k] = SR[i];此時 i++, k++,

    第三次循環結束:i = 3,m = 5,j = 7,n = 9

  • 第四次循環,SR[i] = SR[3] = 50SR[j] = SR[7] = 40 進⾏比較。

    SR[i] > SR[j],那麼將 TR[k] = SR[j];此時 j++, k++,

    第四次循環結束:i = 3,m = 5,j = 8,n = 9

  • 第五次循環,SR[i] = SR[3] = 50SR[j] = SR[8] = 60 進⾏比較。

    SR[i] < SR[j],那麼將 TR[k] = SR[i];此時 i++, k++,

    第五次循環結束:i = 4,m = 5,j = 8,n = 9

  • 第六次循環,SR[i] = SR[4] = 70SR[j] = SR[8] = 60 進⾏比較。

    SR[i] > SR[j],那麼將 TR[k] = SR[j];此時 j++, k++,

    第六次循環結束:i = 4,m = 5,j = 9,n = 9

  • 第七次循環,SR[i] = SR[4] = 70SR[j] = SR[9] = 80 進⾏比較。

    SR[i] < SR[j],那麼將 TR[k] = SR[i];此時 i++, k++,

    第七次循環結束:i = 5,m = 5,j = 9,n = 9

  • 第八次循環,SR[i] = SR[5] = 90SR[j] = SR[9] = 80 進⾏比較。

    SR[i] > SR[j],那麼將 TR[k] = SR[j];此時 j++, k++,

    第八次循環結束:i = 5,m = 5,j = 10,n = 9

第八次循環結束後,j>n, 不滿足循環條件,結束循環。

然後判斷,將兩個子序列中剩餘的元素拼到TR後面,最終合併爲有序序列。

代碼實現如下:

//3 ✅將有序的SR[i..mid]和SR[mid+1..n]歸併爲有序的TR[i..n]
void Merge(int SR[],int TR[],int i,int m,int n)
{
    int j,k,l;
    //1.✅將SR中記錄由小到大地併入TR
    for(j=m+1,k=i;i<=m && j<=n;k++)
    {
        if (SR[i]<SR[j])
            TR[k]=SR[i++];
        else
            TR[k]=SR[j++];
    }
    
    //2.✅將剩餘的SR[i..mid]複製到TR
    if(i<=m)
    {
        for(l=0;l<=m-i;l++)
            TR[k+l]=SR[i+l];
    }
    
    //3.✅將剩餘的SR[j..mid]複製到TR
    if(j<=n)
    {
        for(l=0;l<=n-j;l++)
            TR[k+l]=SR[j+l];
    }
}


//2. ✅將SR[s...t] 歸併排序爲 TR1[s...t];
void MSort(int SR[],int TR1[],int low, int hight){
    int mid;
    int TR2[MAXSIZE+1];
    
    if(low == hight)
        TR1[low] = SR[low];
    else{
        //1.將SR[low...hight] 平分成 SR[low...mid] 和 SR[mid+1,hight];
        mid = (low + hight)/2;
        //2. 遞歸將SR[low,mid]歸併爲有序的TR2[low,mid];
        MSort(SR, TR2, low, mid);
        //3. 遞歸將SR[mid+1,hight]歸併爲有序的TR2[mid+1,hight];
        MSort(SR, TR2, mid+1, hight);
        //4. 將TR2[low,mid] 與 TR2[mid+1,hight], 歸併到TR1[low,hight]中
        Merge(TR2, TR1, low, mid, hight);
    }
}

//1. ✅對順序表L進行歸併排序
void MergeSort(SqList *L){
   
    MSort(L->r,L->r,1,L->length);
}

歸併排序的非遞歸實現:

//歸併排序(非遞歸)-->對順序表L進行非遞歸排序
//對SR數組中相鄰長度爲s的子序列進行兩兩歸併到TR[]數組中;
void MergePass(int SR[],int TR[],int s,int length){
  
    int i = 1;
    int j;
    
    //1. ✅合併數組
    //s=1 循環結束位置:8 (9-2*1+1=8)
    //s=2 循環結束位置:6 (9-2*2+1=6)
    //s=4 循環結束位置:2 (9-2*4+1=2)
    //s=8 循環結束位置:-6(9-2*8+1=-6) s = 8時,不會進入到循環;
    while (i<= length-2*s+1) {
        //兩兩歸併(合併相鄰的2段數據)
        Merge(SR, TR, i, i+s-1, i+2*s-1);
        i = i+2*s;
        
        /*
         s = 1,i = 1,Merge(SR,TR,1,1,2);
         s = 1,i = 3,Merge(SR,TR,3,3,4);
         s = 1,i = 5,Merge(SR,TR,5,5,6);
         s = 1,i = 7,Merge(SR,TR,7,7,8);
         s = 1,i = 9,退出循環;
         */
        
        /*
         s = 2,i = 1,Merge(SR,TR,1,2,4);
         s = 2,i = 5,Merge(SR,TR,5,6,8);
         s = 2,i = 9,退出循環;
         */
        
        /*
         s = 4,i = 1,Merge(SR,TR,1,4,8);
         s = 4,i = 9,退出循環;
         */
    }
    
    //2. ✅如果i<length-s+1,表示有2個長度不等的子序列. 其中一個長度爲length,另一個小於length
    // 1 < (9-8+1)(2)
    //s = 8時, 1 < (9-8+1)
    if(i < length-s+1){
        //Merge(SR,TR,1,8,9)
        Merge(SR, TR, i, i+s-1, length);
    }else{
        //③只剩下一個子序列;
        for (j = i; j <=length; j++) {
            TR[j] = SR[j];
        }
    }
}

void MergeSort2(SqList *L){
    int *TR = (int *)malloc(sizeof(int) * L->length);
    int k = 1;
    //k的拆分變換是 1,2,4,8;
    while (k < L->length) {
        //將SR數組按照s=2的長度進行拆分合並,結果存儲到TR數組中;
        //注意:此時經過第一輪的歸併排序的結果是存儲到TR數組了;
        MergePass(L->r, TR, k, L->length);
        k = 2*k;
        //將剛剛歸併排序後的TR數組,按照s = 2k的長度進行拆分合並. 結果存儲到L->r數組中;
        //注意:因爲上一輪的排序的結果是存儲到TR數組,所以這次排序的數據應該是再次對TR數組排序;
        MergePass(TR, L->r, k, L->length);
        k = 2*k;
        
    }
}

7. 快速排序

快速排序的基本思想:通過一趟排序將待排序記錄分割爲獨立的兩部分,其中一部分記錄的關鍵字均比另一部分記錄的關鍵字小,則可以分別對兩部分記錄繼續進行排序,以達到整個排序有序的目的。

快速排序思路

  • 判斷low是否小於hight
  • 求得樞軸,並將數組樞軸左邊的關鍵字都比樞軸對應的關鍵字小,右邊的關鍵字都比樞軸對應的關鍵字大
  • 將數組一份爲二,分別對低子表和高子表排序

那麼如何找一個樞軸,怎麼將樞軸變量放在合適的位置,並且使得它的左側關鍵字均⽐它⼩, 右側關鍵字均⽐比它大。

接下來,我們一下面的數組爲例,分析一下快速排序的執行流程。

首先,選擇子表中第1個記錄作爲樞軸變量,pivotkey = 50

然後,從表的兩端往中間掃描,開始循環,循環判斷,

  • 從高位開始,找到比pivokey更小的值的下標位置,即:循環判斷是否滿足low<high並且r[high] >= pivotkey,滿足,則遞減high,不滿足條件,則跳出循環
  • 然後交換比樞軸值小的記錄到低端
  • 從低位開始,找到比pivokey更大的值的下標位置。即:循環判斷是否滿足low<high並且r[low] <= pivotkey,滿足,則遞增low,不滿足條件,則跳出循環
  • 然後交換比樞軸值大的記錄到高端

對上圖的序列第一輪循環,判斷條件low < high

  1. 首先比較 L->r[high] >= pivotkey && low < high,此時 low=1,high=9,L->r[9] < 50 , 則循環退出。然後交換,將比樞軸記錄小的記錄交換到低端位置上,得到下圖:

  1. 循環判斷low < high && L->r[low] <= pivotkey,此時low=1,high=9,L->r[1] < 50,則low++,low=2

    然後繼續判斷是否滿足low < high && L->r[low] <= pivotkey,此時low=2,high=9,L->r[2] < 50,則low++,low=3

    然後繼續判斷是否滿足low < high && L->r[low] <= pivotkey,此時low=3,high=9,L->r[3] > 50,不滿足條件,跳出循環,然後將比樞軸記錄大的記錄交換到高端位置上,得到下圖:


至此第一輪循環結束,low = 3, high = 9,滿足循環條件,進入第二輪循環。

對上圖的序列第二輪循環,判斷條件low < high(此時low = 3, high = 9):

  1. 首先比較 L->r[high] >= pivotkey && low < high,此時 low=3,high=9,L->r[9] > 50 ,滿足條件,high--,得到high=8

    繼續比較 L->r[high] >= pivotkey && low < high,此時 low=3,high=8,L->r[8]=60 > 50 ,滿足條件,high--,得到high=7

    繼續比較 L->r[high] >= pivotkey && low < high,此時 low=3,high=7,L->r[7]=80 > 50 ,滿足條件,high--,得到high=6

    繼續比較 L->r[high] >= pivotkey && low < high,此時 low=3,high=6,L->r[6]=40 < 50 ,不滿足條件,跳出循環。然後交換lowhigh的值,將比樞軸記錄小的記錄交換到低端位置上,得到下圖:

  1. 循環判斷low < high && L->r[low] <= pivotkey,此時low=3,high=6,L->r[3] < 50,則low++,low=4

    循環判斷low < high && L->r[low] <= pivotkey,此時low=4,high=6,L->r[4] < 50,則low++,low=5

    循環判斷low < high && L->r[low] <= pivotkey,此時low=5,high=6,L->r[5] > 50,不滿足條件,跳出循環,然後交換lowhigh的值,將比樞軸記錄大的記錄交換到高端位置上,得到下圖:

至此第二輪循環結束,low = 5, high = 6,滿足循環條件,進入第三輪循環。

對上圖的序列第三輪循環,判斷條件low < high(此時low = 5, high = 6):

首先比較 L->r[high] >= pivotkey && low < high,此時 low=5,high=6,L->r[5] < 50 ,滿足條件,high--,得到high=5

此時low == high 退出循環! 表示這一次從兩端交替向中間的掃描已經全部完成了。此時返回low=5

接下來按照上面的邏輯,對序列的【1,5-1】和【5+1,9】子序列進行操作。最終得到一個有序的序列。

//✅3. 交換順序表L中子表的記錄,使樞軸記錄到位,並返回其所在位置
//此時在它之前(後)的記錄均不大(小)於它
int Partition(SqList *L,int low,int high){
    int pivotkey;
    //pivokey 保存子表中第1個記錄作爲樞軸記錄;
    pivotkey = L->r[low];
    //1. 從表的兩端交替地向中間掃描;
    while (low < high) {
        
        //2. 比較,從高位開始,找到比pivokey更小的值的下標位置;
        while (low < high &&  L->r[high] >= pivotkey) {
            high--;
        }
        //3. 將比樞軸值小的記錄交換到低端;
        swap(L, low, high);
        //4. 比較,從低位開始,找到比pivokey更大的值的下標位置;
        while (low < high && L->r[low] <= pivotkey) {
            low++;
        }
        //5. 將比樞軸值大的記錄交換到高端;
        swap(L, low, high);
        
    }
    
    //返回樞軸pivokey 所在位置;
    return low;
}

//✅2. 對順序表L的子序列L->r[low,high]做快速排序;
void QSort(SqList *L,int low,int high){
    int pivot;
    
    if(low < high){
        //將L->r[low,high]一分爲二,算出中樞軸值 pivot;
        pivot = Partition(L, low, high);
        printf("pivot = %d L->r[%d] = %d\n",pivot,pivot,L->r[pivot]);
        //對低子表遞歸排序;
        QSort(L, low, pivot-1);
        //對高子表遞歸排序
        QSort(L, pivot+1, high);
    }
    
}

//✅ 1. 調用快速排序(爲了保證一致的調用風格)
void QucikSort(SqList *L){
    QSort(L, 1, L->length);
}

時間複雜度:最好情況爲O(nlogn),最壞情況爲O(n2)

空間複雜度取決於遞歸造成的棧空間,最好情況爲O(logn),最壞情況爲O(n),平均情況下時間複雜度爲O(logn)

上面的算法,在求解樞軸的時候,我們比較暴力,直接取第一個元素爲樞軸,這樣可能存在一些問題,比如第一個元素在當前序列中是最大或者最小時,交換後就會出現一些問題。

那麼,我們可以對樞軸的求解進行優化,取當前序列的中間數爲樞軸,儘量避免取到最大或者最小的情況。

在比較時,要頻繁的交換高位和低位的值,我們可以對高低位進行覆蓋,在最後一次(low = high)時,用樞軸進行賦值。

比如:

  1. ⽤高位highpivotkey 進⾏比較找到⽐樞軸小的記錄. 交換到低端位置上

替換後爲:

  1. ⽤低位lowpivotkey 進⾏比較找到⽐樞軸大的記錄. 交換到高端位置上


替換後爲:


3. 這樣依次比較替換,最終得到:

然後替換L->r[low] = L->r[0],即:將低位的值替換爲樞軸的值。

優化實現:

int Partition2(SqList *L,int low,int high){
   
    int pivotkey;
    
    // ✅ 1.優化選擇樞軸
    //✅ 計算數組中間的元素的下標值;
    int m = low + (high - low)/2;
    //✅ 將數組中的L->r[low] 是整個序列中左中右3個關鍵字的中間值;
    //交換左端與右端的數據,保證左端較小;[9,1,5,8,3,7,4,6,2]
    if(L->r[low]>L->r[high])
        swap(L, low, high);
    //交換中間與右端的數據,保證中間較小; [2,1,5,8,3,7,4,6,9];
    if(L->r[m]>L->r[high])
        swap(L, high, m);
    //交換中間與左端,保證左端較小;[2,1,5,8,3,7,4,6,9]
    if(L->r[m]>L->r[low])
        swap(L, m, low);
    //交換後的序列:3,1,5,8,2,7,4,6,9
    //此時low = 3; 那麼此時一定比選擇 9,2更合適;
    
    
    // ✅ 2. 優化不必要的交換
    //pivokey 保存子表中第1個記錄作爲樞軸記錄;
    pivotkey = L->r[low];
    //將樞軸關鍵字備份到L->r[0];
    L->r[0] = pivotkey;
    
    // ✅ 3. 從表的兩端交替地向中間掃描;
    while (low < high) {
        //✅ 比較,從高位開始,找到比pivokey更小的值的下標位置;
        while (low < high &&  L->r[high] >= pivotkey)
            high--;
        
        //✅ 將比樞軸值小的記錄交換到低端;
        //swap(L, low, high);
        //✅ 用替換的方式將比樞軸值小的記錄替換到低端
        L->r[low] = L->r[high];
        
        //✅ 比較,從低位開始,找到比pivokey更大的值的下標位置;
        while (low < high && L->r[low] <= pivotkey)
            low++;
        
        //✅ 將比樞軸值大的記錄交換到高端;
        //swap(L, low, high);
        //✅ 替換的方式將比樞軸值大的記錄替換到高端
        L->r[high] = L->r[low];
    }
    //將樞軸數值替換會L->r[low]
    L->r[low] = L->r[0];
    
    //返回樞軸pivokey 所在位置;
    return low;
}

//✅2. 對順序表L的子序列L->r[low,high]做快速排序;
#define MAX_LENGTH_INSERT_SORT 7 //數組長度的閥值
void QSort2(SqList *L,int low,int high){
    int pivot;

    //✅ 當high-low 大於常數閥值是用快速排序;
    if((high-low)>MAX_LENGTH_INSERT_SORT){
        //將L->r[low,high]一分爲二,算出中樞軸值 pivot;
        pivot = Partition(L, low, high);
        printf("pivot = %d L->r[%d] = %d\n",pivot,pivot,L->r[pivot]);
        //對低子表遞歸排序;
        QSort(L, low, pivot-1);
        //對高子表遞歸排序
        QSort(L, pivot+1, high);
    }else{
        // ✅當high-low小於常數閥值是用直接插入排序
    }
}

//✅1. 快速排序優化
void QuickSort2(SqList *L)
{
    QSort2(L,1,L->length);
}

排序總結:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章