1. 程式人生 > >資料結構知識整理 - 排序演算法(本篇包括折半插入排序、快速排序以及堆排序)

資料結構知識整理 - 排序演算法(本篇包括折半插入排序、快速排序以及堆排序)

主要內容


 

前提

排序是計算機程式設計中的一種重要操作,在很多領域中都有著廣泛的應用。

排序的一個主要目的是便於查詢。在前兩篇關於查詢的博文中提到的折半查詢(要求有序的順序表)和樹表查詢(二叉排序樹結構)都涉及到排序演算法。

人們設計了大量的排序演算法以滿足不同的需求。

著名電腦科學家D.E.Knuth在他的鉅著《計算機程式設計藝術》第三卷《排序與查詢》中,給出了25種排序方法,然而這還只是現有排序方法中的冰山一角。(人類真是nb......)


 

初步認識排序

將“無序”的資料元素,通過一定的方法按關鍵字順序排列的過程叫做排序。

排序又分為內部排序外部排序:若整個排序過程不需要訪問外存便能完成,則稱此類排序問題為內部排序;反之,若參加排序的記錄數量很大,整個序列的排序過程不可能在記憶體中完成,則稱此類排序問題為外部排序。

內部排序是一個逐步擴大記錄的有序序列長度的過程。在排序過程中,可以將排序記錄區分為兩個:有序序列區無序序列區使有序序列區中記錄的數目增加一個或幾個的操作稱為一趟排序

 

常見的內部排序可分類為:

1)插入類:將無序子序列中的一個或幾個記錄“插入”到有序序列中。主要包括直接插入排序

折半插入排序希爾排序

2)交換類:通過“交換”無序序列中的記錄,從而得到其中關鍵字最大或最小的記錄,並將它加入有序子序列中,以此方法增加記錄的有序子序列長度。主要包括氣泡排序快速排序

3)選擇類:從記錄的無序子序列中“選擇”關鍵字最小或最大的記錄,並將它加入有序子序列中,以此方法增加記錄的有序子序列長度(沒錯,跟交換類的描述幾乎相同)。主要包括簡單選擇排序樹形選擇排序堆排序

4)歸併類:通過“歸併”兩個或兩個以上的記錄的有序子序列,逐步增加有序序列長度。常見有2-路歸併排序

5)分配類:唯一一種不需要比較關鍵字的排序方法,排序時主要利用分配和收集兩種基本操作來完成。常見有基數排序

 

排序還可以根據穩定性分為兩類;

1)穩定排序:

假設在待排序的檔案中,存在兩個或兩個以上的記錄具有相同的關鍵字,在用某種排序法排序後,若這些相同關鍵字的元素的相對次序仍然不變,則這種排序方法是穩定的。其中氣泡排序直接插入排序折半插入排序基數排序歸併排序等屬於穩定排序。

2)不穩定排序:

與穩定排序的結果相反則為不穩定排序。其中選擇類排序快速排序希爾排序屬於不穩定排序。

(本篇對於各排序演算法的時間複雜度和空間複雜度暫不作分析)


 

待排序記錄表的結構定義

(示例程式碼中的待排序記錄均以順序表為儲存方式,且關鍵字設為整型資料)

#define MAXSIZE 20    /*順序表的最大長度*/

typedef int KeyType   /*定義關鍵字型別為整型,即KeyType key等價於int key*/

/*與構造查詢表類似*/
typedef struct
{
    KeyType key;
    OtherType other;
} RecordType;         /*記錄型別*/

typedef struct
{
    RecordType rcds[MAXSIZE+1]    /*rcds[0]用作監視哨或閒置,可回顧線性表查詢*/
    int length;
} SqList;                         /*順序表型別*/

 

折半插入排序(Binary Insertion Sort)

插入排序的基本思想:每一趟排序中,將一個待排序記錄按其關鍵字大小插入到“有序”記錄的適當位置,直到所有待排序記錄全部插入為止。

直接插入排序是最簡單的排序方法,它採用順序查詢表查詢待排序記錄在有序序列上的插入位置,而“查詢”操作可利用“折半查詢”來實現,以“折半查詢法”查詢插入位置的排序則稱為折半插入排序

 

<思路>

1)rcds[0]作監視哨或閒置,r[1]只有一個記錄,不需要排序,所以排序從rcds[2]開始;

2)比較rcds[1]和rcds[2]的關鍵字大小,若rcds[1]的關鍵字大於rcds[2],則將rcds[1]後移一位,將首元素的位置讓給rcds[2];反之,若rcds[2]的關鍵字更大,則保持rcds[2]原來的位置;

3)同(2)可以推理得出,若帶插入記錄rcds[i]的關鍵字大於rcds[j],小於rcds[j+1],則將rcds[j+1]到rcds[i-1]的元素後移一位,讓出rcds[j+1]的位置給rcds[i];反之,若rcds[i]的關鍵字大於有序序列中的所有記錄,則保持rcds[i]原來的位置;

4)由(3)可知,rcds[0]不僅可用作監視哨,還可以暫存待排序記錄的資訊。

 

程式碼如下(可回顧“折半查詢”):

void Bin_Insert_Sort(SqList &L)
{
    for(int i = 2; i <= L.length; i++)    /*從rcds[2]開始插入排序*/
    {
        L.rcds[0] = L.rcds[i];            /*設定監視哨並暫存待排序記錄的資訊*/

        int low = 1, high = i - 1;        /*折半查詢的範圍是從1到i-1*/

        while(low <= high)                /*折半查詢rcds[i]的插入位置*/
        {
            int mid = (low + high) / 2;

            if(L.rcds[0].key < L.rcds[mid].key) high = mid - 1;
            else low = mid + 1;
        }

        /*經過折半排序可將rcds[i]的位置確定在rcds[high+1],在草稿紙上推理一下*/

        for(int j = i-1; j >= high+1; j--) /*關鍵字更大的記錄後移一位*/
            L.rcds[j+1] = L.rcds[j]; 

        L.rcds[high+1] = L.rcds[0];        /*插入*/
}

 

快速排序(Quick Sort)

交換排序的基本思想:兩兩比較待排序記錄的關鍵字,一旦發現兩個記錄不滿足次序要求時則進行交換,直到整個序列全部滿足要求為止。

氣泡排序(Bubble Sort)是一種最簡單的交換排序方法,它通過兩兩比較相鄰記錄的關鍵字,逆序則交換,從而使關鍵字較小的記錄如氣泡一般逐漸往上“漂浮”(左移),或者使關鍵字較大的記錄如石塊一般“沉落”(右移)。從右往左進行交換更接近“冒泡”的含義,而從左往右交換更像是“沉石”的過程。(可回顧“這裡”)

在氣泡排序中,需要安排一個變數flag表示排序迴圈結束的標誌。flag = 0表示在本趟排序中沒有發生“交換”,即排序已經完成;反之,若flag = 1,繼續進行排序。

快速排序由氣泡排序改進得到。氣泡排序中只能比較相鄰的記錄,所以每次“交換”只能消除一個逆序。而快速排序能夠比較兩個不相鄰的記錄,通過一次“交換”消除多個逆序,從而大大加快排序的速度。

 

<邏輯思路>

1)在待排序的n個記錄中選擇任意一個記錄(通常選擇第一個記錄)作為樞軸(支點),設其關鍵字為pivotkey。經過一趟排序後,所有關鍵字小於pivotkey的記錄“交換”到序列前面,所有關鍵字大於pivotkey的記錄交換到序列後面,最後將樞軸記錄插在中間;

2)不斷重複上述過程,直至每一部分只包含一個記錄。

 

<實現思路>

1)在待排序序列首尾設定指標low、high。初始化時,指標low指向rcds[1],指標high指向rcds[L.length];

2)將rcds[1]的資訊暫存在rcds[0],此時rcds[1]包含的記錄作為樞軸,rcds[1]的位置作為空位

3)指標low從左往右查詢關鍵字大於pivotkey的記錄,指標high從右往左查詢關鍵字小於pivotkey的記錄;

4)因為空位在前半部分,所以指標high首先開始查詢關鍵字小於pivotkey的記錄。假設找到的是rcds[n]上的記錄,找到後將其記錄存放在空位,儲存後空位便由rcds[1]改為rcds[n],即空位出現在後半部分;

5)指標low開始查詢關鍵字大於pivotkey的記錄,並將記錄存放在後半部分的空位,同時“生成”新的空位;

6)不斷重複(4)(5)的過程,直至low == high,而且此時rcds[low](或rcds[high])正是樞軸記錄的存放位置。

7)對前後部分分別重複上述操作,直至所有部分都只包含一個記錄(遞迴)。

/*--------在一趟排序中切分前後部分,並返回樞軸位置--------*/

int Slice_n_Pivot(SqList &L, int low, int high)
{
    L.rcds[0] = L.rcds[1];            /*暫存樞軸記錄*/
    int pivotkey = L.rcds[1].key;     /*初始化pivotkey*/

    while(low < high)                 /*當low未與high重合時*/
    {
        /*指標high從右往左移動,找到(第一個)關鍵字小於pivotkey的記錄*/
        while((low < high) && (L.rcds[high].key >= pivotkey)) high--;    
        
        L.rcds[low] = L.rcds[high];   /*將找到的記錄存放在前半部分的空位,同時得到新的空位*/

        /*指標low從右往左移動,找到(第一個)關鍵字大於pivotkey的記錄*/
        while((low < high) && (L.rcds[low].key <= pivotkey)) low++;

        L.rcds[high] = L.rcds[low];
    }

    L.rcds[low] = L.rcds[0];          /*或L.rcds[high] = L.rcds[0];*/

    return low;               /*返回樞軸的位置,作為前半部分指標high和後半部分指標low的依據*/
}

       
/*----------在前後部分中遞迴-----------*/

void Recursive(SqList &L, int low, int high)
{
    if(low < high)
    {
        pivot = Slice_n_Pivot(L, low, high);

        /*因為在每次遞迴中low與high的值都會發生相應的變化,所以不需要再初始化實參*/       
        Recursive(L, low, pivot-1);    /*在前半部分遞迴排序*/

        Recursive(L, pivot+1, high);   /*在後半部分遞迴排序*/
    }
}

/*-----------總結快速排序------------*/

void Quick_Sort(SqList &L)
{
    Recursive(L, 1, L.length);
}

 

堆排序(Heap Sort)

選擇排序的基本思想:每一趟從待排序記錄中選出關鍵字最小的記錄,按順序放在已排序的記錄序列最後,直到全部記錄排序好為止。

堆排序是一種樹形選擇排序,在排序過程中,將儲存待排序記錄的順序表看成是一棵完全二叉樹(區別滿二叉樹)的順序儲存結構。利用完全二叉樹中父結點與子結點的內在關係,在當前無序的序列中選擇關鍵字最小(或最大)的記錄。

 

<條件>

1)堆的定義:

以任意結點為根結點,根結點的關鍵字都大於(或小於)左、右子樹的根結點。

若堆頂記錄的關鍵字最大,則稱堆為大根堆,反之稱小根堆

2)完全二叉樹的定義:

所有序號大於[n/2](n為結點數)的結點都是葉結點。

因此只需要依次將以[n/2]、[n/2-1]、...、[1]的結點作為根結點的子樹調整為堆即可。

 

<思路>

(以大根堆為例)

1)調整一個小堆:

設根結點為rcds[s],左、右子樹根結點分別為rcds[2s]、rcds[2s+1]。

先比較左、右子樹根結點的關鍵字,若較大值為右子樹根結點,則右子樹根結點與根結點比較。若根結點的關鍵字更大,則該小堆滿足要求;若左、右子樹根結點中較大的關鍵字更大,則交換兩個結點。

但是,交換結點後,可能會出現(原)根結點的關鍵字小於(原)右子樹根結點的左、右子樹根結點(rcds[2(2s+1)]、rcds[2(2s+1)+1]))的情況,這時候需要繼續進行調整,即需要迴圈或遞迴。

2)利用完全二叉樹的特性,從[n/2]結點開始往前調整小堆;

3)經過上面兩步只能使一個無序序列剛好構成堆的條件,還未能稱為有序序列。但我們可以利用堆的特性,將每次調整完得到的堆頂記錄(關鍵字最大)放到最後(依次由[L.length]、[L.length-1]、...),這樣就能得到一個從左往右,關鍵字從小到大的有序序列。

 

/*------------調整一個小堆-------------*/

void Adjust_Small_Heap(SqList &L, int s, int m)    /*s為調整根結點,m為序號最大的結點*/
{
    L.rcds[0] = L.rcds[s];        /*暫存根結點的記錄*/

    for(int i = 2 * s; i <= m; i *= 2)     /*定位結點的左子樹根結點*/
    {
        if((j < m) && (L.rcds[i] < L.rcds[i+1])) i++;    /*如果左小於右,用i表示右*/

        if(L.rcds[0] > L.rcds[i]) break;   /*如果根大於較大的結點,滿足堆的條件,跳出迴圈*/

        L.rcds[s] = L.rcds[i]; s = i;      /*如果沒有在上一步跳出迴圈,交換結點後繼續判斷是否滿足堆的條件*/

    }

    L.rcds[s] = L.rcds[0];    /*經過上面的迴圈後,s表示的位置可能已經改變,需要重新插入(原)根結點的記錄*/

}


/*----------利用完全二叉樹的特性------------*/

void UseCBT(SqList &L)            /*CBT->Completed Binary Tree,完全二叉樹*/
{
    int m = L.length;

    for(int s = n/2; s >=1; i--)
        Adjust_Small_Heap(L, s, m);
}


/*-------------將大根堆有序化--------------*/

void Heap_Sort(SqList &L)
{
    UseCBT(L);                    /*將無序序列轉化成大根堆*/

    for(int i = L.length; i > 1; i--)
    {
        int t = L.rcds[1];        /*將堆頂記錄往後存放*/
        L.rcds[1] = L.rcds[i];
        L.rcds[i] = t;

        Adjust_Small_Heap(L, 1, i-1);    /*交換結點後需要調整以rcds[1]為根結點的堆*/
    }
}

 

路過的圈毛君:“......”