數據結構 學習筆記 排序

一、基本概念

1.1增排序和減排序

按關鍵字從大到小或從小到大劃分。

1.2內部排序和外部排序

數據元素均在內存中即內部排序,否則則包含外部排序。

1.3穩定排序和不穩定排序

關鍵字相同的兩個元素,排序後相對位置發生變化即不穩定,否則即穩定。

1.4排序算法的評價指標

時間複雜度   和   空間複雜度。

 

二、插入排序

2.1基本思想

將待排序表看作左右兩個部分,左邊爲有序區,右邊爲無序區,整個排序過程就是將右邊無序區的元素逐個插入到有序區中,以構成有序區。主要介紹 直接插入排序和希爾排序。

2.2直接插入排序

現直接給出代碼和註釋:

void insertSort(elementType A[n+1]) {
    for(int i = 2; i <= n; i++){        //I表示待插入元素的下標
        A[0] = A[I];                    //設置監視哨保存待插入元素,以騰出A[i]的空間
        j = I - 1;                      //j表示當前空位置的前一個
        while(A[j].key > A[0].key){     //搜索插入位置並騰出空位
            A[j+1] = A[j];
            j = j - 1;
        }
        A[j+1] = A[0];                  //插入元素
    }
}

算法分析:

1.穩定性:該算法爲穩定算法。

2.空間性能:該算法僅需要一個記錄的監視哨輔助空間。

3.時間性能:整個算法循環n-1次,每次循環中的基本操作爲比較和移動元素,一般情況下爲O(N^2).

 

2.3希爾排序(Shell Sort)

基本思想:將待排序的序列劃分爲若干組別,在每組內進行直接選擇插入排序,以使得整個序列基本有序,然後再對整個序列進行直接插入排序。

這種排序的關鍵在於選組。而我們所決定的選擇是將整個序列的長度的1/2在初始選擇爲步長。後面依次遞減1/2。

僞代碼:

void ShellSort(elementType A[n+1], int dh) { //dh means the ORIGINAL FOOTSTEP
    while(dh>=1) {
        for(I = dh + 1; I <= n; I++){
            temp = A[I];
            j = I;
            while(j > d && temp.key<A[j-dh].key){
                A[j] = A[j-dh];
                j = j - dh;
            }
            A[j] = temp;    

        }
            dh = dh/2;
    }


}

算法分析:

希爾排序是分組插入排序,先按照規定將元素分組,同一組內採用直接插入排序。

對比希爾排序和直接插入排序,希爾排序除了分組循環外,其餘同插入排序幾乎完全一致,只是步長從1變爲了dh

1.該算法爲不穩定算法。

2.空間複雜度爲O(1)

3.時間複雜度爲O(nlog2N)

   與分區方法有很大關係

   性能優於直接插入排序,時間複雜度介於O(n)和O(n^2)之間,大致爲O(1.3)或O(1.5)

 

三、交換排序

兩兩比較待排序元素,發現倒序則交換。

3.1冒泡排序

逐個比較兩相鄰元素,發現倒序則交換。

典型做法是從後往前(從下往上)逐個比較相鄰2個元素,發現倒序則交換。

每次掃描一定能將當前最小/大的元素交換到最終位置,如同水泡冒出水面

僞代碼:

void bubbleSort(int A[]){
    for(int I = 1; I < n; I++){
        for(int j = n; j >= I + 1; j--){
            if(A[j].key<A[j-1].key){
                swap(A[j],A[j-1]; //SWAP in IOSTREAM
            }
        }
    }
}

改進的冒泡排序
接下來考慮一種極端情況:序列本身就是有序。

此種情況下,依然將進行O(n^2)級別的掃描。

很明顯不夠划算。

因此,我們可以設置一種含有標誌是否已經交換完成的標誌。這樣作爲每次冒泡排序完成後是否還需要繼續的標誌。

因此得到的改進算法如下:

void bubbleSort(int A[n+1]) {
I = 1;
do{
    exchanged = FALSE; // As a sign of Exchanged or not
    for(j = n; j >= I + 1; j--) {
        if(A[j].key < A[j-1].key){
            swap(A[j],A[j-1]);
            exchanged = TRUE;
            }
        }
    I++;
    }while(I<=n-1&&exchanged == TRUE);
}

算法分析:

穩定性:穩定排序

空間複雜度:O(1)的輔助空間

時間複雜度:

受到數據表初始狀態影響大。

最好情況:正序 比較n-1次,交換 0 次, 時間複雜度O(n)

最壞情況:全部逆序 比較與交換 均爲 n*(n-1)/2;

一般:O(n^2)

 

3.2快速排序

3.2.1基本思想:分治法。

選定一個元素作爲中間元素,然後將表中所有元素與之比較:

比其小的放在表的前面;

比其大的放在表的後面;

該元素放在兩部分中間做劃分,這就是其最終位置。

這樣就可以得到一個劃分(二分)

然後對左右子表再分別進行劃分。

快速排序通過一趟排序將排序序列分成左右兩部分,使得左邊任意元素均不大於/小於右邊任意元素,並將中間元素放到最終位置。

3.2.2操作方法

選擇第一個元素作爲中間元素

1.先保存該元素到其他位置,騰出該位置。

2.從後往前掃描一個比中間數小的元素,並將其放置到(1)中的空位置上,此時後面空出一個位置。

3.從前往後掃描一個比中間數大的元素,並將其放置到(2)中的空位置上,此時前面空出一個位置。

重複2、3直到兩邊掃描到的空位重合,此時將中間元素放在空位中。

 

3.3.3算法設計

分區算法

1.保存中間元素的值到臨時變量x以騰出空間,並且用low指向該元素,即x = A[low];

2.從後往前搜索比這個數字小的元素,並將其放在空位上,從而在後面騰出一個位置(high指向)

3.從前往後掃描到比這個數字大的元素,將其放置在(2)中的high上,從而使得前面空出一個位置(low指向)

重複2、3直到兩邊掃描的位置重合(low==high,即在該空位前沒有更大的元素,此後沒有更小的元素)因而可以將中間元素放在此位置,該元素歸位。

void Partition(int A[], int low,int high, int &mid) {
    //low 分區的第一個元素下標,high 作爲最後一個元素下標
    //mid爲中間元素
    A[0] = A[low];
    while(low < high) {
        //A[high] >= mid元素則不交換,high左移
        while(low < high && A[high].key >= A[0].key) high--;
        //右區間遇到第一個小於mid的元素,移動到 A[low]
        //此時A[low]的元素已經取到A[0]
        //同時A[high]已經移動,其爲空位置,可以存放其他數據
        A[low] = A[high];

        //A[low]<= mid 元素,則不交換,low右邊移動
        while(low < high && A[low].key <= A[0].key) low++;

        //左區間遇到第一個大於此中間元素的值,移動到 A[high]
        //此時A[high]空
        A[high] = A[low];
    }
    //此時low == high 爲目標的空位置
    A[low] = A[0];//將中間元素移動到目標位置
    mid = low;  //返回本次中間值的最終位置
}

快速排序即用到上述的分區算法

void QuickSort(int A[n], int low, int high){
    int mid; // mid 由Partition函數給出
    if(low <high){
        Partition(A,low,high,mid);
        QuickSort(A,low,mid-1);
        QuickSort(A,mid+1,high);
    }
}

算法分析:

1.穩定性:不穩定排序

2.空間複雜度:需要一個輔助空間

3.時間複雜度:

理想情況:每次選擇元素正好兩等份子表。整個算法複雜度爲O(nlog2N)

最壞情況:每次選擇的元素恰爲最大/最小。即需要(n-1)次劃分,掃描(n-i+1)次。整個複雜度爲O(n^2)

一般情況:O(K*nlog2^N)

分析可得:劃分中中間元素的選擇非常重要,因此改進選擇爲:比較子表第一個、最後一個、中間元素。選取中值作爲樞紐元素。

而快排目前也被認爲是內部排序最優解之一。

 

四、選擇排序

基本思想:在每次排序中選出關鍵字最小/最大的元素放在最終位置。

4.1簡單(直接)選擇排序

通過在待排序子表中完整的比較一遍以確定最值元素,並將該元素放在子表的最前/後面。

void SelectSort(int A[],int n) {
    //1~n
    for(int i = 1; i < n; i++) {
        int min = i;
        for(int j = i + 1; j < n; j++) {
            if(A[j] < A[min])
                min = j;
            if(min!=i) {
                swap(A[min],A[i]);
            }
        }
    }
}

算法分析:

穩定性:不穩定排序。

空間複雜度:需要一個額外空間。O(1)

時間複雜度:

共比較n*(n-1)/2次

最多交換n-1次,一趟最多交換1次

O(n^2)

 

4.2堆排序

4.2.1堆及其基本概念

堆實際上是一棵完全二叉樹

​​​​​·若其每個結點均不大於其左右孩子的值,稱爲小根堆(根結點的值最小)

·若其每個結點均不小於其左右孩子的值,稱爲大根堆(根結點的值最大)

可見,若某序列爲堆,其堆頂必爲序列中的最大值或最小值。

 

堆排序的基本思想:

假設要求遞增排序且已有一個大根堆

1.輸出根

2.用二叉樹的最後一個結點替代根,重新調整堆(待排序元素-1)

3.重複上述直到輸出全部結點。

可見,要解決兩個問題:

一是如何建立初始堆、二是輸出根後如何調整堆。

 

4.2.2堆的篩選(調整)

1.輸出根,用二叉樹最後一個結點代替新的根。

2.調整堆,此時,除了跟結點和其左右孩子違反條件外,其餘左右子樹仍然滿足條件。即整個序列不是堆,但其左右子樹仍然是堆。

如何調整:

1.由於其左右子樹是堆,此時左右孩子結點的值分別是兩個子樹中的最大值。因此,新的堆頂只可能從當前根點、其左右孩子中產生,故可以比較這三者得到。

2.如果當前根結點已經是最大值,即已經是堆,則無需調整;否則將左右孩子中的最大值與根對換。

但是調整之後可能違反子樹中堆的大小,因此需要在執行調換的子樹中繼續進行。

 

算法設計:

1.保存臨時根的值到一個變量(設爲x)用i標記該結點。

2.比較i結點的左右孩子和x的最大值:

2.1     i結點沒有左右孩子,即已經到達葉子結點。將x填到i結點中。

2.2     i結點的左右孩子的值小於x的值,表示搜索到了填充位置,將x填入i結點中。

2.3     否則將左右孩子中的最大填充在i結點中,從而出現新的空位,因此,同樣用i指示,並且轉2.2繼續執行。

整理可得 所需參數:

調整中,堆頂的下標不一定爲1,因此需要將堆頂的下標作爲參數---K,輸出根之後,參與運算的元素個數減一,因此,需要將當前序列的元素個數作爲參數---M,加上數組參數A[].

void sift(int A[], int k, int m) {
    //調整以K爲根的子樹序列爲堆
    //其中K爲子樹根,M爲最大元素編號
    //假設以2K和2K+1爲根的左右子樹均爲堆
    int x = A[k];   //臨時保存當前根值,空出位置
    bool finished = false;//設置未結束標誌
    int i = k;            //i指示空位,子樹根
    int j = 2*i;          //j指向k的左孩子結點
    while(j<=m && !finished) {
        //確定i結點不是葉子且未搜索結束
        if(j < m && A[j] < A[j + 1])
            j = j +1;//找出i左右孩子中的最大者,用j指向
        if(x>=A[j])
            finished = true;
                    //根值最大,無需再調整,結束標誌置真
        else {
            A[i] = A[j];    //最大值A[j]上升爲樹根
            i = j;          //跟新子樹根i爲j繼續調整j以下的子樹爲堆
            j = 2 * j;      //繼續下篩,i仍爲子樹樹根,j指向其左孩子結點
        }
    }
    A[i] = x;               //循環結束i即爲x的最終位置,使得K爲根的子樹爲大根堆
}

 

從N/2開始從右往左、自下而上逐棵子樹調整。

建立初堆:

for(int I = n/2; I>=1;i--){
    sift(A,i,n);
}

堆排序:

void HeapSort(int A[],int n){
    int i;
    //初建堆--由初始序列產生堆(此處爲大根堆)
    //從第n/2結點開始往上篩,
    //直到1號結點(根、堆頂)
    for(i = n/2; i>=1;i--) {
        sift(A,i,n);
        //每次調用此函數,
        //都將以i爲根結點的子樹調整爲堆。
    }//由堆序列產生排序序列,
    //此時整棵樹(完全二叉樹)爲堆(此處爲大根堆)
    for(i=n;i>=2;i--)
    {
        A[0]=A[i];  //完全二叉樹最後一個結點保存到A[0],
        //空出位置i輸出根A[1],即當前子樹的根(堆頂) 
        A[i]=A[1];  //輸出根,即A[1]保存到排序後的最終位置i
        A[1]=A[0];  //原第i元素暫作爲“根”。
        //又A[1]=A[0]後可能破壞了當前樹的堆屬性,
        //需要從根結點1開始重新調整爲堆
        //因爲輸出根,此時樹的結點數爲i-1。
        sift(A,1,i-1);
    }
}

算法分析:

穩定性:不穩定。

空間複雜度:需要一個輔助空間,O(1)

時間複雜度:

主要花費在建立初堆和調整堆上。

高度爲h的堆,篩選算法中所進行的關鍵比較次數最多爲2(h-1)次。

h=floor(log2n)+1;

即最多爲log2N次

堆排序共調用篩選n-1次;建立初堆共調用篩選n/2次。

總複雜度爲O(nlog2n)

 

五、歸併排序

歸併排序先設法將原序列劃分爲只含有1個元素的子表(視爲有序)

然後反覆選擇兩個有序子表進行合併直到合併後的序列長度爲n

歸併算法基於兩個基本操作:劃分和合並

劃分操作將1個未排序序列劃分成2個更短的子序列。

歸併操作將2個或者多個有序子序列合併成1個更長的有序序列。

 

歸併排序可以分爲:

·自頂向下的

·自底向上的

歸併排序同快速排序一樣,都是分治法的典型應用。

5.1歸併

(同線性表一樣的三情況分情況討論)

void merge(int A[],int B[],int C[],int la, int lb, int lc) {
    //非降序數組A,B前la,lb個元素合併到C 並且保持其次序
    int ia = 1, ib = 1, ic = 1;
    while(ia <= la && ib <= lb)
        if(A[ia]<=B[ib])
            C[ic++] = A[ia++];
        else 
            C[ic++] = B[ib++];
        while(ia <= la)
            C[ic++] = A[ia++];
        while(ib<=lb)
            C[ic++] = B[ib++];
}

算法分析:

對於A和B均是一遍掃描,整個時間複雜度爲O(|A| + |B|)

 

而歸併排序中歸併的兩個字序列要放在同一個表A中,因此要通過元素下標參數對兩個字表進行定界。

通過三個參數low、mid、high來確定2個有序子序列。

第一個子序列放在A[low~mid]

第二個子序列放在A[mid+1~high]
此外,歸併中要把歸併後的元素放在一個臨時表T中,T的大小與A相同歸併完成後,再將T中的元素複製到A中。

Merge函數需要4個參數:

A[]存放元素序列 low序列第一個元素下標 high序列最後一個元素下標  mid劃分點下標

改造後的歸併序列:

void Merge(int A[], int low, int mid, int high) {
    int T[10005];
    int i, j, k;
    //i作爲low~mid的下標  j作爲mid+1~high的下標 k作爲T的下標
    i = low;
    k = low;
    j = mid + 1;
    while(i<=mid && j<=high) {
        //A兩個子表都有元素
        if(A[i] <= A[j]) {//A[i]較小
            T[k] = A[i];
            i++;
        }else {
            T[k] = A[j];
            j++;
        }
        k++;
    }
    //處理一個表結束,另一個尚未結束的場景
    while(i<=mid) {
        T[k] = A[i];
        i++;
        k++;
    }
    while(j<=high) {
        T[k] = A[j];
        j++;
        k++;
    }
    //複製回原表
    memcpy(A,T, sizeof(T));
}

自底向上

·將原序列視爲(劃分爲)n個有序子序列,子序列長度爲1,每個子表只有一個元素;

·當子序列長度小於N的時候,循環選擇2個相鄰有序子序列,歸併

 

 

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