1. 程式人生 > >排序及其應用C語言實現(資料結構複習最全筆記)(期末複習最新版)

排序及其應用C語言實現(資料結構複習最全筆記)(期末複習最新版)

排序

關於排序給兩篇不錯的部落格參考:

http://www.cnblogs.com/eniac12/p/5329396.html

https://www.cnblogs.com/eniac12/p/5332117.html

知識前提

關於內外排序

內排序:指在排序期間資料物件全部存放在記憶體的排序。
外排序:指在排序期間全部物件太多,不能同時存放在記憶體中,必須根據排序過程的要求,不斷在內,外存間移動的排序。
根據排序元素所在位置的不同,排序分: 內排序和外排序。
內排序:在排序過程中,所有元素調到記憶體中進行的排序,稱為內排序。內排序是排序的基礎。內排序效率用比較次數來衡量。按所用策略不同,內排序又可分為插入排序、選擇排序、交換排序、歸併排序及基數排序等幾大類。
外排序:在資料量大的情況下,只能分塊排序,但塊與塊間不能保證有序。外排序用讀/寫外存的次數來衡量其效率。

一.兩種簡單排序:氣泡排序與插入排序

1.氣泡排序

它重複地走訪過要排序的元素列,依次比較兩個相鄰的元素,如果他們的順序(如從大到小、首字母從A到Z)錯誤就把他們交換過來。走訪元素的工作是重複地進行直到沒有相鄰元素需要交換,也就是說該元素已經排序完成。

這個演算法的名字由來是因為越大的元素會經由交換慢慢“浮”到數列的頂端(升序或降序排列),就如同碳酸飲料中二氧化碳的氣泡最終會上浮到頂端一樣,故名“氣泡排序”。

要注意它是一種穩定的排序

此外他的比較次數為n(n-1)/2

說的嚴謹一:,對n個元素進行起泡排序,在(正序)情況下比較的次數最少,其比較次數為(n-1 )。在(反序)情況下比較次數最多,其比較次數為(n(n-1)/2)。

實現程式碼:

//氣泡排序
void Buttle_sort(ElementType A[], int N)
{
    long P,i;
    ElementType Tmp;
    for(P = N - 1; P >= 0; P--) //共N-1趟
    {
        bool flag = false;//判斷是否發生了交換
        for(i = 0; i < P; i++) //一趟排序
        {
            if(A[i] > A[i + 1])//如果相鄰兩個上面的比下面的大,就進行交換
            {
                Tmp = A[i];
                A[i] = A[i+1];
                A[i+1] = Tmp;
                flag = true;//發生了交換
            }
        }
        if(flag == false)//如果一趟下來並沒有發生交換,就代表陣列有序了,直接跳出迴圈即可
            break;
    }
}

2.插入排序

有一個已經有序的資料序列,要求在這個已經排好的資料序列中插入一個數,但要求插入後此資料序列仍然有序,這個時候就要用到一種新的排序方法——插入排序法,插入排序的基本操作就是將一個數據插入到已經排好序的有序資料中,從而得到一個新的、個數加一的有序資料,演算法適用於少量資料的排序,時間複雜度為O(n^2)。是穩定的排序方法。插入演算法把要排序的陣列分成兩部分:第一部分包含了這個陣列的所有元素,但將最後一個元素除外(讓陣列多一個空間才有插入的位置),而第二部分就只包含這一個元素(即待插入元素)。在第一部分排序完成後,再將這個最後元素插入到已排好序的第一部分中。

插入排序的基本思想是:每步將一個待排序的記錄,按其關鍵碼值的大小插入前面已經排序的檔案中適當位置上,直到全部插入完為止。

此外,這個演算法的分類也有很多種

插入排序的分類

包括:直接插入排序,二分插入排序(又稱折半插入排序),連結串列插入排序,希爾排序(又稱縮小增量排序)。屬於穩定排序的一種(通俗地講,就是兩個相等的數不會交換位置) 。

直接插入排序

直接插入排序是一種簡單的插入排序法,其基本思想是:把待排序的記錄按其關鍵碼值的大小逐個插入到一個已經排好序的有序序列中,直到所有的記錄插入完為止,得到一個新的有序序列。

折半插入排序(二分插入排序)

將直接插入排序中尋找A[i]的插入位置的方法改為採用折半比較,即可得到折半插入排序演算法。在處理A[i]時,A[0]……A[i-1]已經按關鍵碼值排好序。所謂折半比較,就是在插入A[i]時,取A[i-1/2]的關鍵碼值與A[i]的關鍵碼值進行比較,如果A[i]的關鍵碼值小於A[i-1/2]的關鍵碼值,則說明A[i]只能插入A[0]到A[i-1/2]之間,故可以在A[0]到A[i-1/2-1]之間繼續使用折半比較;否則只能插入A[i-1/2]到A[i-1]之間,故可以在A[i-1/2+1]到A[i-1]之間繼續使用折半比較。如此擔負,直到最後能夠確定插入的位置為止。一般在A[k]和A[r]之間採用折半,其中間結點為A[k+r/2],經過一次比較即可排除一半記錄,把可能插入的區間減小了一半,故稱為折半。執行折半插入排序的前提是檔案記錄必須按順序儲存。 [2] 

折半插入排序的演算法思想:

演算法的基本過程:

(1)計算 0 ~ i-1 的中間點,用 i 索引處的元素與中間值進行比較,如果 i 索引處的元素大,說明要插入的這個元素應該在中間值和剛加入i索引之間,反之,就是在剛開始的位置 到中間值的位置,這樣很簡單的完成了折半;

(2)在相應的半個範圍裡面找插入的位置時,不斷的用(1)步驟縮小範圍,不停的折半,範圍依次縮小為 1/2 1/4 1/8 .......快速的確定出第 i 個元素要插在什麼地方;

(3)確定位置之後,將整個序列後移,並將元素插入到相應位置。

3 希爾排序法(後面詳細介紹)---這個不是簡單排序

希爾排序法又稱縮小增量法。希爾排序法的基本思想是:先選定一個整數,把待排序檔案中所有記錄分成個組,所有距離為的記錄分在同一組內,並對每一組內的記錄進行排序。然後,取,重複上述分組和排序的工作。當到達=1時,所有記錄在統一組內排好序。

各組內的排序通常採用直接插入法。由於開始時s的取值較大,每組內記錄數較少,所以排序比較快。隨著不斷增大,每組內的記錄數逐步增多,但由於已經按排好序,因此排序速度也比較快。

下面以直接插入排序為例!!!

這個過程其實類似摸牌

要注意它也是一種穩定的排序

演算法實現:


//插入排序
void InsertionSort( ElementType A[], int N )
{
    /* 插入排序 */
    int P, i;
    ElementType Tmp;
    for ( P=1; P<N; P++ ) //假設第0張牌以及在手
    {
        Tmp = A[P]; /* 取出未排序序列中的第一個元素*/
        //相當於摸下一張牌
        for ( i=P; i>0 && A[i-1]>Tmp; i-- )
            A[i] = A[i-1]; //依次與已排序序列中元素比較並右移,即移出空位
        A[i] = Tmp; /* 放進合適的位置 */
        //即新牌落位
    }
}

 

 

二.希爾排序

希爾排序(Shell's Sort)是插入排序的一種又稱“縮小增量排序”(Diminishing Increment Sort),是直接插入排序演算法的一種更高效的改進版本。希爾排序是非穩定排序演算法。該方法因D.L.Shell於1959年提出而得名。

希爾排序是把記錄按下標的一定增量分組,對每組使用直接插入排序演算法排序;隨著增量逐漸減少,每組包含的關鍵詞越來越多,當增量減至1時,整個檔案恰被分成一組,演算法便終止。 

希爾排序希爾增量的實現


//希爾排序-希爾增量
void Shell_Sort( ElementType A[], int N )
{
    int P,D,i;
    ElementType Tmp;
    for(D=N/2; D > 0; D/=2)
    {
        for(P = D; P <N; P++)
        {
            Tmp = A[P]; /* 取出未排序序列中的第一個元素*/
            //相當於摸下一張牌
            for ( i=P; i>=D && A[i-D]>Tmp; i-=D )
                A[i] = A[i-D]; //依次與已排序序列中元素比較並右移,即移出空位
            A[i] = Tmp; /* 放進合適的位置 */
            //即新牌落位
        }
    }
}

這種演算法很明顯是有問題的,比如

改進方法

下面給出Sedgewick增量序列的實現方法 

// 希爾排序 - 用Sedgewick增量序列
void Shellsedgewick_Sort( ElementType A[], int N )
{
     int Si, D, P, i;
     ElementType Tmp;
     /* 這裡只列出一小部分增量 */
     int Sedgewick[] = {929, 505, 209, 109, 41, 19, 5, 1, 0};

     for ( Si=0; Sedgewick[Si]>=N; Si++ )
         ; /* 初始的增量Sedgewick[Si]不能超過待排序列長度 */

     for ( D=Sedgewick[Si]; D>0; D=Sedgewick[++Si] )
         for ( P=D; P<N; P++ ) { /* 插入排序*/
             Tmp = A[P];
             for ( i=P; i>=D && A[i-D]>Tmp; i-=D )
                 A[i] = A[i-D];
             A[i] = Tmp;
         }
}

 關於他的穩定性

由於多次插入排序,我們知道一次插入排序是穩定的,不會改變相同元素的相對順序,但在不同的插入排序過程中,相同的元素可能在各自的插入排序中移動,最後其穩定性就會被打亂,所以shell排序是不穩定的。

三.選擇排序

選擇排序(Selection sort)是一種簡單直觀的排序演算法。它的工作原理是每一次從待排序的資料元素中選出最小(或最大)的一個元素,存放在序列的起始位置,然後,再從剩餘未排序元素中繼續尋找最小(大)元素,然後放到已排序序列的末尾。以此類推,直到全部待排序的資料元素排完。 選擇排序是不穩定的排序方法。

選擇排序法 是對 定位比較交換法(也就是氣泡排序法) 的一種改進。選擇排序的基本思想是:每一趟在n-i+1(i=1,2,…n-1)個記錄中選取關鍵字最小的記錄作為有序序列中第i個記錄。基於此思想的演算法主要有簡單選擇排序、樹型選擇排序和堆排序

簡單選擇排序的基本思想:第1趟,在待排序記錄r[1]~r[n]中選出最小的記錄,將它與r[1]交換;第2趟,在待排序記錄r[2]~r[n]中選出最小的記錄,將它與r[2]交換;以此類推,第i趟在待排序記錄r[i]~r[n]中選出最小的記錄,將它與r[i]交換,使有序序列不斷增長直到全部排序完畢。

挺簡單的,直接上程式碼:


//選擇排序
void Selection_Sort(ElementType A[],int N)
{
    int i,j,min;
    for(i=0;i<N-1;i++)
    {
        //tmp = A[i];
        min=i;//查詢最小值
        for(j=i+1;j<N;j++)
        {
            if(A[min]>A[j])
            {
                min=j;
            }
        }
        if(min!=i)
        {
            int t;
            t = A[min];
            A[min] = A[i];
            A[i] = t;
            //swap(&A[min],&A[i]);
        }
    }
}

穩定性

選擇排序是給每個位置選擇當前元素最小的,比如給第一個位置選擇最小的,在剩餘元素裡面給第二個元素選擇第二小的,依次類推,直到第n-1個元素,第n個元素不用選擇了,因為只剩下它一個最大的元素了。那麼,在一趟選擇,如果一個元素比當前元素小,而該小的元素又出現在一個和當前元素相等的元素後面,那麼交換後穩定性就被破壞了。比較拗口,舉個例子,序列5 8 5 2 9,我們知道第一遍選擇第1個元素5會和2交換,那麼原序列中兩個5的相對前後順序就被破壞了,所以選擇排序是一個不穩定的排序演算法。

 

簡單選擇排序特點總結:

簡單選擇排序它最大的特點是交換移動資料次數相當少,這樣也就節約了相應的時間,無論最好最壞的情況,其比較次數都是一樣多。第 i 次排序需要進行n-i 次關鍵字的比較,此時需要比較n-1+n-2+...+1=n(n-1)/2次,時間複雜度為O(n^2)。對於交換次數而言,當最好的時候,交換為0次,最差的時候,也就初始排序,交換次數為n-1次,複雜度為O(n)。

四.堆排序

關於堆的一些基礎知識如果不懂的話可以參考這篇部落格:https://blog.csdn.net/weixin_42110638/article/details/83982381

堆排序(英語:Heapsort)是指利用這種資料結構所設計的一種排序演算法。堆是一個近似完全二叉樹的結構,並同時滿足堆積的性質:即子結點的鍵值或索引總是小於(或者大於)它的父節點。

這個演算法不穩定!!!

我們知道在選擇排序中找最小元的過程其實是很耗時間的,那有什麼辦法能更加快速的找到最小元呢?

沒錯,就是利用堆,這也就有了堆排序

先介紹一種比較笨的堆排序演算法

這個方法的大致思路是先把陣列調成最小堆,然後儲存根節點並將其彈出。

這種方法有個很明顯的問題就是最後把臨時陣列賦值給A陣列這步操作太耗時間了(本來人家已經找出來了。。。)

於是就有了演算法2,這種演算法有個很重要的特點,就是先建立的不是最小堆,而是最大堆

這裡要注意一個地方,就是正常建堆我們是從第一個位置開始建,而第0個位置是哨兵。

可是現在我們是從第一個位置開始建立的,那麼有個問題要想清楚

在堆排序中,元素下標從0開始。則對於下標為i的元素,其左、右孩子的下標分別為:

 

2i+1, 2i+2!!!

想清楚這個問題就好啦

演算法實現:

void Swap( ElementType *a, ElementType *b )
{
     ElementType t = *a; *a = *b; *b = t;
}
  
void PercDown( ElementType A[], int p, int N )
{ /* 改編程式碼4.24的PercDown( MaxHeap H, int p )    */
  /* 將N個元素的陣列中以A[p]為根的子堆調整為最大堆 */
    int Parent, Child;
    ElementType X;
 
    X = A[p]; /* 取出根結點存放的值 */
    for( Parent=p; (Parent*2+1)<N; Parent=Child ) {
        Child = Parent * 2 + 1;
        if( (Child!=N-1) && (A[Child]<A[Child+1]) )
            Child++;  /* Child指向左右子結點的較大者 */
        if( X >= A[Child] ) break; /* 找到了合適位置 */
        else  /* 下濾X */
            A[Parent] = A[Child];
    }
    A[Parent] = X;
}
 
void HeapSort( ElementType A[], int N ) 
{ /* 堆排序 */
     int i;
       
     for ( i=N/2-1; i>=0; i-- )/* 建立最大堆 */
         PercDown( A, i, N );
      
     for ( i=N-1; i>0; i-- ) {
         /* 刪除最大堆頂 */
         Swap( &A[0], &A[i] ); /* 見程式碼7.1 */
         PercDown( A, 0, i );
     }
}

有個小思考,堆排序最適合解決什麼樣的問題?

答案:

如果我們要從全球70多億人口中找出最富有的100個人,有什麼排序演算法可以保證不用完全排序就能在中途得到結果嗎?

插入排序不行:如果最大富翁最後才出現,那麼不到最後一步完成,我們都不敢保證說前面100個就是答案了。

希爾排序本質上是插入的變形,肯定也是不行。

歸併呢?因為前後兩半的元素在整個排序中不會串邊,所以只要後半部分有大富翁,就一定得等到最後一步大合併,才能確保前面排的100個是答案。

而堆排序是唯一可以只用100步就保證得到前100個大富翁的演算法!當然,在排序之前先要O(N)的時間去建立最大堆。

所以當我們的問題是要從大量的N個數據中找最大/最小的k個元素時,用堆排序是比較快的,可以在O(N+klogN)時間內得到解 —— 當然k比較小才行。對於這種問題,還有另一種方法是:先把前k個元素調整成最小堆(時間為O(k));此後每讀入一個元素,首先跟堆頂元素比較,如果沒有堆頂大,就直接扔掉了;否則把堆頂元素替換掉,做一次下濾。這樣總體最壞複雜度是O(k+Nlogk)。

五.歸併排序

歸併排序(MERGE-SORT)是建立在歸併操作上的一種有效的排序演算法,該演算法是採用分治法(Divide and Conquer)的一個非常典型的應用。將已有序的子序列合併,得到完全有序的序列;即先使每個子序列有序,再使子序列段間有序。

歸併操作

歸併操作(merge),也叫歸併演算法,指的是將兩個順序序列合併成一個順序序列的方法。

如 設有數列{6,202,100,301,38,8,1}

初始狀態:6,202,100,301,38,8,1

第一次歸併後:{6,202},{100,301},{8,38},{1},比較次數:3;

第二次歸併後:{6,100,202,301},{1,8,38},比較次數:4;

第三次歸併後:{1,6,8,38,100,202,301},比較次數:4;

總的比較次數為:3+4+4=11;

逆序數為14;

演算法描述

歸併操作的工作原理如下:

第一步:申請空間,使其大小為兩個已經排序序列之和,該空間用來存放合併後的序列

第二步:設定兩個指標,最初位置分別為兩個已經排序序列的起始位置

第三步:比較兩個指標所指向的元素,選擇相對小的元素放入到合併空間,並移動指標到下一位置

重複步驟3直到某一指標超出序列尾

將另一序列剩下的所有元素直接複製到合併序列尾

比較

歸併排序是穩定的排序.即相等的元素的順序不會改變.如輸入記錄 1(1) 3(2) 2(3) 2(4) 5(5) (括號中是記錄的關鍵字)時輸出的 1(1) 2(3) 2(4) 3(2) 5(5) 中的2 和 2 是按輸入的順序.這對要排序資料包含多個資訊而要按其中的某一個資訊排序,要求其它資訊儘量按輸入的順序排列時很重要。歸併排序的比較次數小於快速排序的比較次數,移動次數一般多於快速排序的移動次數。

用途

排序

速度僅次於快速排序,為穩定排序演算法,一般用於對總體無序,但是各子項相對有序的數列

歸併操作:

而他的排序是基於分治思想的

此外要注意這個演算法是穩定的

但是有個問題,這個MSort函式的引數好像不太友好,和我們排序演算法的統一介面(一個數組A,一個長度n)不太一樣

為此我們設計了一個統一的介面

下面給出歸併排序完整的遞迴演算法程式碼

/* 歸併排序 - 遞迴實現 */
 
/* L = 左邊起始位置, R = 右邊起始位置, RightEnd = 右邊終點位置*/
void Merge( ElementType A[], ElementType TmpA[], int L, int R, int RightEnd )
{ /* 將有序的A[L]~A[R-1]和A[R]~A[RightEnd]歸併成一個有序序列 */
     int LeftEnd, NumElements, Tmp;
     int i;
      
     LeftEnd = R - 1; /* 左邊終點位置 */
     Tmp = L;         /* 有序序列的起始位置 */
     NumElements = RightEnd - L + 1;
      
     while( L <= LeftEnd && R <= RightEnd ) {
         if ( A[L] <= A[R] )
             TmpA[Tmp++] = A[L++]; /* 將左邊元素複製到TmpA */
         else
             TmpA[Tmp++] = A[R++]; /* 將右邊元素複製到TmpA */
     }
 
     while( L <= LeftEnd )
         TmpA[Tmp++] = A[L++]; /* 直接複製左邊剩下的 */
     while( R <= RightEnd )
         TmpA[Tmp++] = A[R++]; /* 直接複製右邊剩下的 */
          
     for( i = 0; i < NumElements; i++, RightEnd -- )
         A[RightEnd] = TmpA[RightEnd]; /* 將有序的TmpA[]複製回A[] */
}
 
void Msort( ElementType A[], ElementType TmpA[], int L, int RightEnd )
{ /* 核心遞迴排序函式 */ 
     int Center;
      
     if ( L < RightEnd ) {
          Center = (L+RightEnd) / 2;
          Msort( A, TmpA, L, Center );              /* 遞迴解決左邊 */ 
          Msort( A, TmpA, Center+1, RightEnd );     /* 遞迴解決右邊 */  
          Merge( A, TmpA, L, Center+1, RightEnd );  /* 合併兩段有序序列 */ 
     }
}
 
void MergeSort( ElementType A[], int N )
{ /* 歸併排序 */
     ElementType *TmpA;
     TmpA = (ElementType *)malloc(N*sizeof(ElementType));
      
     if ( TmpA != NULL ) {
          Msort( A, TmpA, 0, N-1 );
          free( TmpA );
     }
     else printf( "空間不足" );
}

其中的malloc只執行了一次,只在陣列的某一段執行操作,那麼如果在merge中宣告臨時陣列呢? 

顯然這會造成極大的空間浪費 !!!

此外,我們知道遞迴演算法雖然好寫,但是對電腦本身其實並不友好

下面我們介紹非遞迴演算法

別被這個圖嚇到哦!

實際上只需要開一個額外的陣列,兩邊來回倒就成 

其核心步驟就是:一趟歸併

其歸併的躺數的數量級數量級是logN!!!

這個圖有個錯誤,malloc前應該有個(ElementType*)

下面給出歸併排序完整的非遞迴演算法程式碼

/* 歸併排序 - 迴圈實現 */
/* 這裡Merge函式在遞迴版本中給出 */
 
/* length = 當前有序子列的長度*/
void Merge_pass( ElementType A[], ElementType TmpA[], int N, int length )
{ /* 兩兩歸併相鄰有序子列 */
     int i, j;
       
     for ( i=0; i <= N-2*length; i += 2*length )
         Merge( A, TmpA, i, i+length, i+2*length-1 );
     if ( i+length < N ) /* 歸併最後2個子列*/
         Merge( A, TmpA, i, i+length, N-1);
     else /* 最後只剩1個子列*/
         for ( j = i; j < N; j++ ) TmpA[j] = A[j];
}
 
void Merge_Sort( ElementType A[], int N )
{ 
     int length; 
     ElementType *TmpA;
      
     length = 1; /* 初始化子序列長度*/
     TmpA = (ElementType*)malloc( N * sizeof( ElementType ) );
     if ( TmpA != NULL ) {
          while( length < N ) {
              Merge_pass( A, TmpA, N, length );
              length *= 2;
              Merge_pass( TmpA, A, N, length );
              length *= 2;
          }
          free( TmpA );
     }
     else printf( "空間不足" );
}

這個演算法好處很多,比如時間複雜度永遠是O(n log n),而且他還是穩定的

但是,就是有一個地方不好

就是他需要開闢額外的空間,並且在這兩個陣列間導來導去很費時間

所以這個演算法雖然很好,但一般只適用於外排序,很少用於內排序

六.快速排序

快速排序(Quicksort)是對氣泡排序的一種改進。

快速排序由C. A. R. Hoare在1962年提出。它的基本思想是:通過一趟排序將要排序的資料分割成獨立的兩部分,其中一部分的所有資料都比另外一部分的所有資料都要小,然後再按此方法對這兩部分資料分別進行快速排序,整個排序過程可以遞迴進行,以此達到整個資料變成有序序列

此外這個演算法不穩定!!!

演算法介紹

設要排序的陣列是A[0]……A[N-1],首先任意選取一個數據(通常選用陣列的第一個數)作為關鍵資料,然後將所有比它小的數都放到它前面,所有比它大的數都放到它後面,這個過程稱為一趟快速排序。值得注意的是,快速排序不是一種穩定的排序演算法,也就是說,多個相同的值的相對位置也許會在演算法結束時產生變動。

一趟快速排序的演算法是:

1)設定兩個變數i、j,排序開始的時候:i=0,j=N-1;

2)以第一個陣列元素作為關鍵資料,賦值給key,即key=A[0];

3)從j開始向前搜尋,即由後開始向前搜尋(j--),找到第一個小於key的值A[j],將A[j]和A[i]互換;

4)從i開始向後搜尋,即由前開始向後搜尋(i++),找到第一個大於key的A[i],將A[i]和A[j]互換;

5)重複第3、4步,直到i=j; (3,4步中,沒找到符合條件的值,即3中A[j]不小於key,4中A[i]不大於key的時候改變j、i的值,使得j=j-1,i=i+1,直至找到為止。找到符合條件的值,進行交換的時候i, j指標位置不變。另外,i==j這一過程一定正好是i+或j-完成的時候,此時令迴圈結束)。

給出偽碼描述

看著並不難對吧,就是先選個主元,然後分次連個獨立的子集,在遞迴呼叫快排函式

但是實現過程極易出錯,而且很容易讓快排變得很慢。。。

第一步:選主元

很明顯這麼選主元就慢到家了

要注意並不是排好了就完了,這裡我們把pivot藏到了右邊倒數第二個位置,為什麼要這麼做呢?

看程式碼好好理解

第二步:子集劃分

設計兩個指標,一個最左邊,一個最右邊-1(因為最右邊是pivot)

這個過程看程式碼就成,有點類似多項式的加法

遇到圖上這個問題你該怎麼辦,考慮一種極端的情況,陣列中元素全部都是一樣的

如果你選擇,停下來交換,那你想想這個過程,你會進行很多次無用的交換,比較次數差不多是一半的長度,這樣的演算法時間複雜度是O(nlogn),但這樣有個好處,就是他不停的交換,最終回來到一個靠近中間的位置,再把i和pivot,這樣還是不錯的!

而如果你選擇不理它,繼續移動指標,你會老問題解決了,終於不用老交換了。但是更大的問題來了,你的i指標會從最左邊一直移動到最右邊,j都來不及移動。什麼意思呢?就是最後你的pivot一定是在序列的某一端點上,那就回到了之前選主元時那種慢到家的方法(上面講了),時間複雜度將變成o(n2)

總上考慮,我們還是選擇停下來交換更好一些吧

所以也就得出了這個演算法的時間複雜度

時間複雜度

平均為O(nlogn),最好為O(nlogn),最差為O(n2)。

此外要注意,快速排序因為要用遞迴,所以比較適合處理規模較大的資料,處理規模較小的資料,那可能還不如簡單的排序

程式碼實現:

// 快速排序
ElementType Median3( ElementType A[], int Left, int Right )
{
    int Center = (Left+Right) / 2;
    if ( A[Left] > A[Center] )
        Swap( &A[Left], &A[Center] );
    if ( A[Left] > A[Right] )
        Swap( &A[Left], &A[Right] );
    if ( A[Center] > A[Right] )
        Swap( &A[Center], &A[Right] );
    /* 此時A[Left] <= A[Center] <= A[Right] */
    Swap( &A[Center], &A[Right-1] ); /* 將基準Pivot藏到右邊*/
    /* 只需要考慮A[Left+1] … A[Right-2] */
    return  A[Right-1];  /* 返回基準Pivot */
}

void Qsort( ElementType A[], int Left, int Right )
{ /* 核心遞迴函式 */
     int Pivot, Cutoff, Low, High;

     if ( Cutoff <= Right-Left ) { /* 如果序列元素充分多,進入快排 */
          Pivot = Median3( A, Left, Right ); /* 選基準 */
          Low = Left; High = Right-1;
          while (1) { /*將序列中比基準小的移到基準左邊,大的移到右邊*/
               while ( A[++Low] < Pivot ) ;
               while ( A[--High] > Pivot ) ;
               if ( Low < High ) Swap( &A[Low], &A[High] );
               else break;
          }
          Swap( &A[Low], &A[Right-1] );   /* 將基準換到正確的位置 */
          Qsort( A, Left, Low-1 );    /* 遞迴解決左邊 */
          Qsort( A, Low+1, Right );   /* 遞迴解決右邊 */
     }
     else InsertionSort( A+Left, Right-Left+1 ); /* 元素太少,用簡單排序 */
}

void QuickSort( ElementType A[], int N )
{ /* 統一介面 */
     Qsort( A, 0, N-1 );
}

 

穩定性

    首先大家應該都知道快速排序是一個不穩定排序演算法,那麼為什麼呢?

    舉個例子,例如(5,3A,6,3B)對這個進行排序,排序之前相同的數3A與3B,A在B的前面,經過排序之後會變成

        (3B,3A,5,6),所以說快速排序是一個不穩定的排序
七.表排序(簡單瞭解)

八.基數排序

在這裡要先介紹一下桶排序

<1>桶排序

桶排序 (Bucket sort)或所謂的箱排序,是一個排序演算法,工作的原理是將陣列分到有限數量的桶子裡。每個桶子再個別排序(有可能再使用別的排序演算法或是以遞迴方式繼續使用桶排序進行排序,桶排序使用線性時間(Θn))。但桶排序並不是 比較排序,他不受到 O(n log n) 下限的影響。

程式碼實現:

#include<iostream>
using namespace std;

// 分類 ------------- 內部非比較排序
// 資料結構 --------- 陣列
// 最差時間複雜度 ---- O(nlogn)或O(n^2),只有一個桶,取決於桶內排序方式
// 最優時間複雜度 ---- O(n),每個元素佔一個桶
// 平均時間複雜度 ---- O(n),保證各個桶內元素個數均勻即可
// 所需輔助空間 ------ O(n + bn)
// 穩定性 ----------- 穩定

/* 本程式用陣列模擬桶 */
const int bn = 5;    // 這裡排序[0,49]的元素,使用5個桶就夠了,也可以根據輸入動態確定桶的數量
int C[bn];           // 計數陣列,存放桶的邊界資訊

void InsertionSort(int A[], int left, int right)
{
    for (int i = left + 1; i <= right; i++)  // 從第二張牌開始抓,直到最後一張牌
    {
        int get = A[i];
        int j = i - 1;
        while (j >= left && A[j] > get)
        {
            A[j + 1] = A[j];
            j--;
        }
        A[j + 1] = get;
    }
}

int MapToBucket(int x)
{
    return x / 10;    // 對映函式f(x),作用相當於快排中的Partition,把大量資料分割成基本有序的資料塊
}

void CountingSort(int A[], int n)
{
    for (int i = 0; i < bn; i++)
    {
        C[i] = 0;
    }
    for (int i = 0; i < n; i++)     // 使C[i]儲存著i號桶中元素的個數
    {
        C[MapToBucket(A[i])]++;
    }
    for (int i = 1; i < bn; i++)    // 定位桶邊界:初始時,C[i]-1為i號桶最後一個元素的位置
    {
        C[i] = C[i] + C[i - 1];
    }
    int *B = (int *)malloc((n) * sizeof(int));
    for (int i = n - 1; i >= 0; i--)// 從後向前掃描保證計數排序的穩定性(重複元素相對次序不變)
    {
        int b = MapToBucket(A[i]);  // 元素A[i]位於b號桶
        B[--C[b]] = A[i];           // 把每個元素A[i]放到它在輸出陣列B中的正確位置上
                                    // 桶的邊界被更新:C[b]為b號桶第一個元素的位置
    }
    for (int i = 0; i < n; i++)
    {
        A[i] = B[i];
    }
    free(B);
}

void BucketSort(int A[], int n)
{
    CountingSort(A, n);          // 利用計數排序確定各個桶的邊界(分桶)
    for (int i = 0; i < bn; i++) // 對每一個桶中的元素應用插入排序
    {
        int left = C[i];         // C[i]為i號桶第一個元素的位置
        int right = (i == bn - 1 ? n - 1 : C[i + 1] - 1);// C[i+1]-1為i號桶最後一個元素的位置
        if (left < right)        // 對元素個數大於1的桶進行桶內插入排序
            InsertionSort(A, left, right);
    }
}

int main()
{
    int A[] = { 29, 25, 3, 49, 9, 37, 21, 43 };// 針對桶排序設計的輸入
    int n = sizeof(A) / sizeof(int);
    BucketSort(A, n);
    printf("桶排序結果:");
    for (int i = 0; i < n; i++)
    {
        printf("%d ", A[i]);
    }
    printf("\n");
    return 0;
}

 

總結:桶排序的平均時間複雜度為線性的O(N+C),其中C=N*(logN-logM)。如果相對於同樣的N,桶數量M越大,其效率越高,最好的時間複雜度達到O(N)。當然桶排序的空間複雜度為O(N+M),如果輸入資料非常龐大,而桶的數量也非常多,則空間代價無疑是昂貴的。此外,桶排序是穩定的。

<2>基數排序

基數排序(radix sort)屬於“分配式排序”(distribution sort),又稱“桶子法”(bucket sort)或bin sort,顧名思義,它是透過鍵值的部份資訊,將要排序的元素分配至某些“桶”中,藉以達到排序的作用,基數排序法是屬於穩定性的排序,其時間複雜度為O (nlog(r)m),其中r為所採取的基數,而m為堆數,在某些時候,基數排序法的效率高於其它的穩定性排序法。

效率分析

時間效率 :

設待排序列為n個記錄,d個關鍵碼(也就是進位制碼),關鍵碼的取值範圍為radix,則進行鏈式基數排序的時間複雜度為O(d(n+radix)),其中,一趟分配時間複雜度為O(n),一趟收集時間複雜度為O(radix),共進行d趟分配和收集。

空間效率:需要2*radix個指向佇列的輔助空間,以及用於靜態連結串列的n個指標

設元素個數為N,整數進製為B,LSD的趟數為P,則最壞時間複雜度是

時間複雜度就是圖中的

實現原理

基數排序的發明可以追溯到1887年赫爾曼·何樂禮在打孔卡片製表機(Tabulation Machine)上的貢獻。它是這樣實現的:將所有待比較數值(正整數)統一為同樣的數位長度,數位較短的數前面補零。然後,從最低位開始,依次進行一次排序。這樣從最低位排序一直到最高位排序完成以後, 數列就變成一個有序序列。

基數排序的方式可以採用LSD(Least significant digital)或MSD(Most significant digital),LSD的排序方式由鍵值的最右邊開始,而MSD則相反,由鍵值的最左邊開始。

實現方法

1.次位優先

最高位優先(Most Significant Digit first)法,簡稱MSD法:先按k1排序分組,同一組中記錄,關鍵碼k1相等,再對各組按k2排序分成子組,之後,對後面的關鍵碼繼續這樣的排序分組,直到按最次位關鍵碼kd對各子組排序後。再將各組連線起來,便得到一個有序序列。

程式碼實現:

/* 基數排序 - 次位優先 */
 
/* 假設元素最多有MaxDigit個關鍵字,基數全是同樣的Radix */
#define MaxDigit 4
#define Radix 10
 
/* 桶元素結點 */
typedef struct Node *PtrToNode;
struct Node {
    int key;
    PtrToNode next;
};
 
/* 桶頭結點 */
struct HeadNode {
    PtrToNode head, tail;
};
typedef struct HeadNode Bucket[Radix];
  
int GetDigit ( int X, int D )
{ /* 預設次位D=1, 主位D<=MaxDigit */
    int d, i;
     
    for (i=1; i<=D; i++) {
        d = X % Radix;
        X /= Radix;
    }
    return d;
}
 
void LSDRadixSort( ElementType A[], int N )
{ /* 基數排序 - 次位優先 */
     int D, Di, i;
     Bucket B;
     PtrToNode tmp, p, List = NULL; 
      
     for (i=0; i<Radix; i++) /* 初始化每個桶為空連結串列 */
         B[i].head = B[i].tail = NULL;
     for (i=0; i<N; i++) { /* 將原始序列逆序存入初始連結串列List */
         tmp = (PtrToNode)malloc(sizeof(struct Node));
         tmp->key = A[i];
         tmp->next = List;
         List = tmp;
     }
     /* 下面開始排序 */ 
     for (D=1; D<=MaxDigit; D++) { /* 對資料的每一位迴圈處理 */
         /* 下面是分配的過程 */
         p = List;
         while (p) {
             Di = GetDigit(p->key, D); /* 獲得當前元素的當前位數字 */
             /* 從List中摘除 */
             tmp = p; p = p->next;
             /* 插入B[Di]號桶尾 */
             tmp->next = NULL;
             if (B[Di].head == NULL)
                 B[Di].head = B[Di].tail = tmp;
             else {
                 B[Di].tail->next = tmp;
                 B[Di].tail = tmp;
             }
         }
         /* 下面是收集的過程 */
         List = NULL; 
         for (Di=Radix-1; Di>=0; Di--) { /* 將每個桶的元素順序收集入List */
             if (B[Di].head) { /* 如果桶不為空 */
                 /* 整桶插入List表頭 */
                 B[Di].tail->next = List;
                 List = B[Di].head;
                 B[Di].head = B[Di].tail = NULL; /* 清空桶 */
             }
         }
     }
     /* 將List倒入A[]並釋放空間 */
     for (i=0; i<N; i++) {
        tmp = List;
        List = List->next;
        A[i] = tmp->key;
        free(tmp);
     } 
}

 

2.主位優先

最低位優先(Least Significant Digit first)法,簡稱LSD法:先從kd開始排序,再對kd-1進行排序,依次重複,直到對k1排序後便得到一個有序序列。

關於圖中這個問題的答案:

不一定。

極端情況下,當主位可以一次性把元素都直接分開、而次位辦不到的時候,顯然MSD更好。

一般情況下,如果主位的基數比次位大(例如撲克牌如果先按面值、同一面值內部按花色排序的話),則主位更有可能把元素分開,這時候用MSD就可能比LSD快。

程式碼實現:

/* 基數排序 - 主位優先 */
 
/* 假設元素最多有MaxDigit個關鍵字,基數全是同樣的Radix */
 
#define MaxDigit 4
#define Radix 10
 
/* 桶元素結點 */
typedef struct Node *PtrToNode;
struct Node{
    int key;
    PtrToNode next;
};
 
/* 桶頭結點 */
struct HeadNode {
    PtrToNode head, tail;
};
typedef struct HeadNode Bucket[Radix];
  
int GetDigit ( int X, int D )
{ /* 預設次位D=1, 主位D<=MaxDigit */
    int d, i;
     
    for (i=1; i<=D; i++) {
        d = X%Radix;
        X /= Radix;
    }
    return d;
}
 
void MSD( ElementType A[], int L, int R, int D )
{ /* 核心遞迴函式: 對A[L]...A[R]的第D位數進行排序 */
     int Di, i, j;
     Bucket B;
     PtrToNode tmp, p, List = NULL; 
     if (D==0) return; /* 遞迴終止條件 */
      
     for (i=0; i<Radix; i++) /* 初始化每個桶為空連結串列 */
         B[i].head = B[i].tail = NULL;
     for (i=L; i<=R; i++) { /* 將原始序列逆序存入初始連結串列List */
         tmp = (PtrToNode)malloc(sizeof(struct Node));
         tmp->key = A[i];
         tmp->next = List;
         List = tmp;
     }
     /* 下面是分配的過程 */
     p = List;
     while (p) {
         Di = GetDigit(p->key, D); /* 獲得當前元素的當前位數字 */
         /* 從List中摘除 */
         tmp = p; p = p->next;
         /* 插入B[Di]號桶 */
         if (B[Di].head == NULL) B[Di].tail = tmp;
         tmp->next = B[Di].head;
         B[Di].head = tmp;
     }
     /* 下面是收集的過程 */
     i = j = L; /* i, j記錄當前要處理的A[]的左右端下標 */
     for (Di=0; Di<Radix; Di++) { /* 對於每個桶 */
         if (B[Di].head) { /* 將非空的桶整桶倒入A[], 遞迴排序 */
             p = B[Di].head;
             while (p) {
                 tmp = p;
                 p = p->next;
                 A[j++] = tmp->key;
                 free(tmp);
             }
             /* 遞迴對該桶資料排序, 位數減1 */
             MSD(A, i, j-1, D-1);
             i = j; /* 為下一個桶對應的A[]左端 */
         } 
     } 
}
 
void MSDRadixSort( ElementType A[], int N )
{ /* 統一介面 */
    MSD(A, 0, N-1, MaxDigit); 
}

九.排序大法總結