第 9 章 排序

排序:
  假設含有n個記錄的序列爲r1,r2,......,rn ,其相對應的關鍵字分別爲{k1,k2,......,kn },需確定1,2,……,n的一種排列p1,p2,......pn ,使其相對應的關鍵字滿足kp1<=kp2<=......<=kpn (非遞減或非遞增關係),即使的序列稱爲一個按關鍵字有序的序列{rp1,rp2,......,rpn ,這樣的操作就成爲排序。
  

9.1 開場白

9.2 排序的概念與分類

9.2.1 排序的穩定性

假設ki=kj(1<=i<=n,1<=j<=n,i!=j) ,且在排序前的序列中ri 領先於rj (即i < j).如果排序後ri 仍領先於rj ,則稱所用的排序方法是穩定的;反之,若可能使得排序後的序列中rj 領先ri ,則稱所用的排序方法是不穩定的

9.2.2 內排序與外排序

內排序是在排序整個過程中,待排序的所有記錄全部被放置在內存中。
外排序是由於排序的記錄個數太多,不能同時放置在內存,整個排序過程需要在內外存之間多次交換數據才能進行。
這裏主要介紹內排序,對內排序來說,排序算法的性能主要受3個方面影響:

  1. 時間性能
  2. 輔助空間
  3. 算法的複雜性
    根據排序過程中藉助的主要操作,把內排序分爲:插入排序、交換排序、選擇排序和歸併排序。
    按照算法的複雜度分爲兩類,冒泡排序、簡單選擇排序和直接插入排序屬於簡單算法,而希爾排序、堆排序、歸併排序、快速排序屬於改進算法。

9.2.3 排序用到的結構與函數

先提供一個用於排序用的的順序表結構。

#define MAXSIZE 10  
typedef struct
{
    int r[MAXSIZE];
    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;
}

9.3 冒泡排序

9.3.1 最簡單排序實現

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

//對順序表L做交換排序(冒泡排序初級版)
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);
        }
    }
}

9.3.2 冒泡排序算法

//對順序表L作冒泡排序
void BubbleSort1(SqList *L)
{
    int i, j;
    for (i = 1; i < L->length; i++)
    {
        for (j = L->length-1; j >= i; j--)      //注意j是從後往前循環
        {
            if (L->r[j] > L->r[j+1])            //若前者大於後者(注意這裏與上一算法差異)
                swap(L, j, j+1);            //交換L->r[j]與L->r[j+1]的值
        }
    }
}

9.3.3 冒泡排序優化

//對順序表L作改進冒泡算法
void BubbleSort2(SqList *L)
{
    int i, j;
    Status flag = TRUE;
    for (i = 1; i < L->length && flag; i++)     //若flag爲true則退出循環
    {
        flag = FALSE        //初始爲false
        for (j = L->length-1; j>=i; j--)
        {
            if (L->r[j] > L->r[j+1])
            {
                swap(L, j, j+1);        //交換L->r[j]與L->r[j+1]的值
                flag = TRUE;        //如果數據交換,則flag爲TRUE
            }
        }
    }
}

9.3.4 冒泡排序算法複雜度分析

總的時間複雜度爲O(n2

9.4 簡單選擇排序

選擇排序的基本思想是一趟在n-i+1個記錄中選取關鍵字最小的記錄作爲有序序列的第i個記錄。

9.4.1 簡單選擇排序算法

簡單選擇排序算法就是通過n-i次關鍵字間的比較,從n-i+1個記錄中選出關鍵字最小的記錄,並和第i(1<= i <= n)個記錄交換之。

//對順序表L作簡單選擇排序
void SelectSort(SqList *L)
{
    int i, j, min;
    for (i = 1; i < length; i++)
    {
        min = i;                //將當前下標定義爲最小值下標
        for (j = i + 1; j <= L->length; j++)        //循環之後的數據
        {
            if (L->r[min] > L->[j])     //如果有小於當前最小值的關鍵字,將此關鍵字的下標賦值給min
                min = j;
        }
        if (i != min)   //若min不等於i,說明找到最小值,交換
            swap(L, i, min);        //交換L->r[i]與L->r[min]的值
    }
}           

與冒泡排序相比較,只需交換8次就可完成,性能略有於冒泡排序。

9.4.2 簡單選擇排序複雜度分析

時間複雜度爲O(n2 ).

9.5 直接插入排序

9.5.1 直接插入排序算法

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

//對順序表L作直接插入排序
void InsertSort(SqList *L)
{
    int i,j;
    for (i = 2; i <= L->length; i++)
    {
        if (L->r[i] < L->r[i-1])        //需將L->r[i]插入有序子表
        {
            L->r[0] = L->r[i];      //設置哨兵
            for( j = i-1; L->r[j] > L->r[0]; j--0)
                L-r[j+1] = L->r[j];     //記錄後移
            L->r[j+1] = L->r[0];        //插入到正確位置
        }
    }
}

9.5.2 直接插入排序複雜度分析

複雜度爲O(n2 ).同樣的,直接插入排序算法比冒泡和簡單排序的性能要好一些。

9.6 希爾排序

9.6.1 希爾排序原理

  所謂的基本有序,就是小的關鍵字基本在前面,大的基本在後面,不大不小的基本在中間。
  採取跳躍分割的策略:將相距某個“增量”的記錄組成一個子序列,這樣才能保證在子序列內分別進行直接插入排序後得到的結果是基本有序而不是局部有序。
  

9.6.2 希爾排序算法

//對順序表L作希爾排序
void ShellSort(SqList *L)
{
    int i,j;
    int increment = L->length;
    do 
    {
        increment = increment / 3 + 1;      //增量序列
        for (i = increment + 1; i <= L->length; i++)
        {
            if (L->r[i] < L->r[i-increment])
            {//需將L->r[i]插入有序增量子表
                L->r[0] = L->r[i];      //暫存在L->r[0]
                for (j = i - increment; j > 0 && L->r[0] < L->r[j]; j -= increment)
                    L->r[j+increment] = L->r[j];        //記錄後移,查找插入位置
                L->r[j+increment] = L->r[0];        //插入
            }
        }
    }
    while (increment > 1);
}

9.6.3 希爾排序複雜度分析

  通過這段代碼的剖析,希爾排序的關鍵並不是隨便分組後各自排序,而是將相隔某個“增量”的記錄組成一個子序列,實現跳躍式的移動,使得排序的效率提高。
  增量的選取非常關鍵了,目前還是一個數學難題,沒有一個人找打一種最好的增量序列。不過大量的研究表明,當增量序列爲dlta[k]=2tk+11(0<=k<=t<=log2(n+1) 時,可以獲得不錯的效率,其時間複雜度爲O(n3/2 ,要好與直接排序的O(n2) 。需要注意的是,增量序列的最後一個增量值必須等於1才行。另外,由於記錄跳躍式的移動,希爾排序並不是一種穩定的算法
  

9.7 堆排序

堆排序就是對簡單選擇排序的一種改進。
  堆具有下列性質的完全二叉樹:每個結點的值都大於或等於其左右孩子結點的值,稱爲大頂堆;每個結點的值都小於或等於其左右孩子結點的值,稱爲小頂堆;
  根結點一定是最大或小值。
  

9.7.1 堆排序算法

  堆排序就是利用對進行排序的算法 。它的基本思想是,將待排序的序列構成一個大頂堆。此時,整個序列的最大值就是堆頂的根結點。將它移走(其實就是將其與堆數組的末尾元素交換,此時末尾元素就是最大值)然後,將剩餘的n-1個序列重新構造成一個大頂堆,這樣就會得到n個元素的次大值。如此反覆執行,便能得到一個有序的序列了。
  需要解決兩個問題:
  

  1. 如何由一個無序序列構建成一個堆
  2. 如果在輸出堆頂元素後,調整剩餘元素成爲一個新的堆。
//對順序表L進行堆排序
void HeapSort(SqList *L)
{
    int i;
    for (i = L->length/2; i > 0; i--)       //把L中的r構建成一個大頂堆
        HeapAdjust(L, i, L->length);

    for (i = L->length; i > 1; i--)
    {
        swap(L, 1, i); //將堆頂記錄和當前未經排序子序列的最後一個記錄交換
        HeapAdjust(L, 1, i-1);  //將L->r[1..i-1]重新調整成爲大頂堆。
    }
}

  所謂的將待排序的序列構建成爲一個大頂堆,其實就是從下往上、從右到左,將每個非終端結點(非葉節點)當做根結點,將其和其子樹調整成大頂堆。下面是HeapAdjust函數實現
  

//已知L->r[s..m]中記錄的關鍵字除L->r[s]之外均滿足堆的定義
//本函數調整L->r[s]的關鍵字,是L->r[s..m]成爲一個大頂堆
void HeapAdjust(SqList *L, int s, int m)
{
    int temp, j;
    temp = L->r[s];
    for (j = 2*s; j <=m; j *= 2)        //沿關鍵字較大的孩子結點向下篩選
    {
        if (j < m && L->r[j] < L->r[j+1])
            ++j;        //j爲關鍵字中較大的記錄的下標
        if (temp >= L->r[j])
            break;      //rc應插入在位置s上
        L->r[s] = L->r[j];
        s = j;
    }
    L->r[s] = temp;     //插入
}

9.7.2 堆排序複雜度分析

總的時間複雜度爲O(nlgn)。它無論是最好、最壞還是平均時間複雜度都爲O(nlgn)。不過由於記錄的比較魚交換是跳躍式進行,因此堆排序也是一種不穩定的排序算法。另外,由於初始構建堆所需的比較次數較多,因此,他並不適合待排序序列個數較少的情況。

9.8 歸併排序

9.8.1 歸併排序算法

  歸併排序的原理是假設初始序列有n個記錄,則可以看成是n個有序的子序列,每個子序列的長度爲1,讓後兩兩歸併,得到⌈n/2⌉個長度爲2或者1的有序子序列;再兩兩歸併,如此重複,直到的得到一個長度爲n的有序序列爲止。
  

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

//將SR[s..t]歸併排序爲TR1[s..t]
void MSort(int SR[], int TR1[], int s, int t)
{
    int m;
    int TR2[MAXSIZE+1];
    if (s == t)
        TR1[s] = SR[s];
    else 
    {
        m = (s + t) /2;     //將SR[s..t]平分爲SR[s..m]和SR[m+1..t]
        MSort(SR,TR2, s, m);        //遞歸將SR[s..m]歸併爲有序的TR2[s..m]
        MSort(SR, TR2, m+1, t);     //遞歸將SR[m+1..t] 歸併爲有序TR2[m+1..t]
        Merge(TR2, TR1, s, m,t);    //將TR2[s..m]和TR2[m+1..t]歸併到TR1[s..t]
    }
}
//將有序的SR[i..m]和SR[m+1..n]歸併爲有序的TR[i..n]
void Merge(int SR[], int TR[], int i, int m, int n)
{
    int j, k, 1;
    for (j = m+1, k = i; i <= m && j <= n; k++)     //將SR中記錄由小到大歸併如TR
    {
        if (SR[i] < SR[j])
            TR[k] = SR[i++];
        else 
            TR[k] = SR[j++];
    }
    if (i <= m)
    {
        for (l = 0; l <= m - i; l++)
            TR[k+1] = SR[i+1];      //將剩餘的SR[i..m]複製到TR
    }
    if (j <= n)
    {
        for (l=1; l <= n-j; l++)
        TR[k+1] = SR[j+1];      //將剩餘的SR[j..n]複製到TR
    }
}

9.8.2 歸併排序複雜度分析

總的時間複雜度爲O(nlgn),而且這是歸併排序算法中最好最壞、平均時間時間性能。空間複雜度爲O(n+logn)。但是歸併排序是穩定的排序算法。
  總之,歸併排序是一種比較佔用內存,但效率高且穩定的算法。
  

9.8.3 非遞歸實現歸併排序

//對順序表L作歸併非遞歸排序
void MergeSort2(SqList *L)
{
    int *TR= (int *)malloc(L->length * sizeof(int));
    int k = 1;
    while(k < L->length)
    {
        MergePass(L->r, TR, k, L->length);
        k = 2 * k;                  //子序列長度加倍
        MergePass(TR, L->r, k, L->length);
        k = 2 *k;                   //子序列長度加倍
    }
}

//將SR【】中相鄰長度爲s的子序列兩兩歸併到TR【】
void MergePass(int SR[], int TR[], int s, int n)
{
    int i = 1;
    int j;
    while(i <= n-2*s+1)
    {
        Merge(SR, TR, i, i+s-1, i+2*s-1);       //兩兩歸併
        i = i + 2 *s;
    }
    if (i < n-s+1)  //歸併最後兩個序列
        Merge(SR, TR, i, i+s-1, n);
    else        //若最後只剩下單個子序列
        for (j=i; j <= n; j++)
            TR[j] = SR[j];
}

使用歸併排序時,儘量考慮使用非遞歸方法

9.9 快速排序

  希爾排序相當於直接插入排序的升級,堆排序相當於簡單選擇排序的升級,而快速排序相當於冒泡排序的升級。
  

9.9.1 快速排序算法

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

//對順序表L作快速排序
void QuickSort(SqList *L)
{
    QSort(L, 1, L->length);
}

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

快速排序最爲關鍵的Partition函數實現

//交換順序表L中子表的記錄,使樞軸記錄到位,並返回其所在位置
//此時在它之前(後)的記錄均不大於它
int Partition(SqList *L, int low, int high)
{
    int pivotkey;
    pivotkey = L->r[low];       //用子表的第一個記錄作樞軸記錄
    while(low < high)       //從表的兩端交替向中間掃描
    {
        while(low < high && L->r[high] >= pivotkey)
            high--;
        swap(L,low,high);           
        while(low < high && L->r[low] <= pivotkey)
            low++;
        swap(L,low,high);       
    }
    return low;     //返回樞軸所在位置
}

9.9.2 快速排序複雜度分析

空間複雜度爲O(logn),時間複雜度爲O(nlgn)。**快速排序是一種不穩定的排序方法。
**

9.9.3 快速排序優化

  • 優化選取樞軸

三數取中,即取三個關鍵字先進行排序,將中間數作爲樞軸,一般是左端、中間、右端。
在Partition函數代碼的第3行和第4行之間增加這樣一端代碼:

//優化選取樞軸
int pivotkey;

int m = low + (high - low)
if (L->r[low] > L->r[high])
    swap(L, low, high);         //交換左端與右端數據,保證左端較小
if (L->r[m] > L->r[high])
    swap(L, high, m);           //交換中間與右端數據,保證中間較小
if (L->r[m] > L->r[low])
    swap(L, low, high);         //交換中間與左端數據,保證左端較小
//此時L.r[low]已經爲整個序列左中右三個關鍵的中間值

pivotkey = L->r[low];           //用子表的第一個記錄作樞軸記錄
  • 優化必要的交換
//優化不必要的交換
int Partition1(SqList *L, int low, int high)
{
    int pivotkey;
    //這裏省略三數取中代碼
    pivotkey = L->r[low];       //用子表的第一個記錄作樞軸記錄
    L->r[0] = pivotkey;         //將樞軸關鍵字備份到L->r[0]
    while(low < high)           //從表的兩端交替向中間掃描
    {
        while(low < hight && L->r[high] >= pivotkey)
            high--;
        L->r[low] = L->r[high];     //採用替換而不是交換的方式進行操作
        while(low < high && L->r[low] <= pivotkey)
            low++;
        L->r[high] = L->r[low];     //採用替換而不是交換的方式進行操作
    }
    L->r[low] = L->r[0];        //將樞軸數值替換會L.r[low]
    return low;     //返回樞軸所在位置
}
  • 優化小數組時的排序方案
//優化小數組時的排序方案
#define MAX_LENGTH_INSERT_SORT  7
void QSort(SqList &L, int low, int high)
{
    int pivot;
    if ((high-low) > MAX_LENGTH_INSERT_SORT)
    {//當high-low大於常數時用快速排序
        pivot = Partition(L, low, high);        //將L->r[low..high]一份爲二
                                                //算出樞軸值pivot
        QSort(L, low, pivot - 1);       //對低子表遞歸排序
        QSort(L,pivot+1, high);         //對高子表遞歸排序
    }
    else //當high-low小於等於常數時用直接插入排序
        InsertSort(L);
}
  • 優化遞歸操作
//優化遞歸操作
//對順序表L中的子序列L->r[low..high]作快速排序
void QSort1(SqList *L, int low, int high)
{
    int pivot;
    if ((high - low) > MAX_LENGTH_INSERT_SORT)
    {
        while(low < high)
        {
            pivot = Partition(L, low, high);        //將L->r[low..high]一份爲二
                                                //算出樞軸值pivot
            QSort1(L, low, pivot - 1);      //對低子表遞歸排序
            low = pivot + 1;            //尾遞歸
        }
    }
    else 
        InsertSort(L);
}
  • 了不起的排序算法
    依然時最好的排序算法

9.10 總結回顧

這裏寫圖片描述

9.11 結尾語

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