【Conyrol】排序算法

很水而且還很鴿的學習筆記,不定期更新

無限猴子定理
一隻猴子隨機敲打打字機鍵盤,如果時間足夠長,總是能打出特定的文本,比如莎士比亞全集

最近更新:2018年12月6號,更新三向快速排序原理代碼

下列均以排成升序序列爲目標的代碼,若想做降序請自行修改XD

目錄


選擇排序:

簡單粗暴,最爲超時

  • 選數組裏最小的數,把它放在第零位
  • 從第一位開始選數組裏最小的數,把他放在第一位
  • …直到最後一位
void SelectionSort(){
    for(int i=0; i<n; i++){
        int jShu = i;
        for(int j=i+1; j<n; j++) if(A[j]<A[jShu]) jShu = j;
        swap(A[i], A[jShu]);
    }
}

swap(a, b) 函數用來交換參數裏面的兩個數 a 和 b ,後面經常用

冒泡排序:

雖然很慢,但是很有用

  • 從數組第零位開始向後枚舉判斷,如果這位比下一位要大,則交換
    ( 這樣其實就是變相的找最大值,如果你嘗試模擬一下10個數的排序,你會發現一邊下來以後,最大的數浮到了最後面 )
  • 接着數組最後一位變成了最大的數,接下來在從第零位開始向後枚舉直到最後一位的前面,然後第二大的數就到了倒數第二位
    ( 最後一位就不用去判斷了,反正都最大了,這樣後面會節省點時間 )
  • 接着枚舉,直到都排好序
void BubbleSort(){
    for(int i=n-1; i>=0; i--)
        for(int j=0; j<i; j++)
            if(A[j]>A[j+1]) swap(A[j],A[j+1]);
}

插入排序:

雖然也很慢,但是更有用,很多時候在小數據排序的情況下,冒泡排序是很方便很穩妥的排序方案

  • 從第一位開始向前檢索,把這一位插入到前面是比這位小,後面是比這位大的位置
    ( 之所以不從第零位開始就是因爲前面沒有任何數,那豈不是我插我自己? )
  • 從第二位開始向前檢索,依舊按照上面的方法插入,你會發現這麼插入後形成的都是升序列
    ( 似乎是廢話XD )
  • 直到最後一位,向前再檢索一邊,Over,從頭到尾全是升序列了

那麼怎麼找到前面是比這位小,後面是比這位大的位置呢?
實際上我們可以看出來我每次插入後,這個數組都是升序的,也就是說我只要不斷向前比較,如果比較到前面一個數比我要插入的數還小,就直接插入到這個數後面就可以了

兩種代碼實現:

//原版
void InsertionSort1(){
    int bet, jShu;
    for(int i=1; i<n; i++){
        bet = A[i];
        jShu = i;
        for(int j=i-1; j>=0&&bet<A[j]; j--){
           jShu = j;
           A[j+1] = A[j];
        }
        A[jShu] = bet;
    }
}
//簡化版
void InsertionSort2(){
    for(int i=1; i<n; i++) for(int j=i-1; j>=0&&A[j+1]<A[j]; j--) swap(A[j+1],A[j]);
}

雞尾酒排序

冒泡排序的升級版本 —— 左右搖擺!

  • 從數組第零位開始向後枚舉判斷,如果這位比下一位要大,則交換
  • 接着不回到第零位,而是從倒數第二位開始向前冒泡,把小的數浮到前面去
    ( 就是左右搖擺版的冒泡排序 )
    這種方式比普通冒泡快了那麼一點點,雖然耗時基本沒有區別,但是雞尾酒排序可以做出很騷的操作,比如說改一改代碼就能排出一個以中間爲 最大值(最小值),左右 漸小(大) 的數列
void CocktailSort(){
    int left=0, right=n-1;
    while(left<right){
        for(int i=left; i<right; i++) if(A[i]>A[i+1]) swap(A[i],A[i+1]);
        right--;
        for(int i=right; i>left; i--) if(A[i]<A[i-1]) swap(A[i],A[i-1]);
        //若改爲if(A[i]>A[i-1])可以做成以中心爲最小值向兩邊擴散的數列排序! 
        left++;
    }
}

如下以中心爲最小值向兩邊擴散的數列
雞尾酒排序

希爾排序

非常常用的排序,插入排序的升級版本 —— 跳位檢索,最爲致命!
利用希爾增量序列的希爾排序時間複雜度最壞是O(n2)O (n^2),平均時間複雜度是O(n1.3)O (n^{1.3}),屬於不穩定排序

前言:

首先我們看一下插入排序,之所以它的效率緩慢,就是因爲某個數在插入的過程中,必須一位位檢索,如果目標點很遠,往往需要遍歷的很遠才能找到指定位置,那麼我們是不是可以跳位來檢索呢?
說到快速檢索指定位置,可能大部分人第一想到的就是二分查找,的確,我們可以通過二分查找來快速找到目標點 ( 因爲對於當前你要插入的這個數來說,它前面的數據是有序的,可以利用二分查找 )
但是當我們辛辛苦苦用O(log2n)O (\log_2 n)找到了指定位置
結果問題來了,我該怎麼插入這個數據呢?
與一位位檢索不同,二分查找只能找到位置,而一位位檢索則在檢索過程中把數據一位位往後移,使得找到後可以直接插入,所以說二分查找實際上並沒有簡化整體時間複雜度,因爲你最後還是要一位位轉移數據,空出空來插入,它只是在一定程度上簡化了查找的時間

那麼這個時候希爾排序就出現了!
該排序實質上是一種分組插入方法

希爾排序:

竟然我二分查找不行,那就另換思路!
怎麼樣讓數據不一步步蹭到指定位置,又可以準確的找到指定位置呢,把數組分組怎麼樣?
假如說一個 10 數據的亂序數組,爲了使數據大跨步,我先把數組分爲 5組 ,每組 2個元素
如下圖 ( 實在太懶摘用一下百度圖XD )
在這裏插入圖片描述
592和72一組
401和911一組

然後給每組數據爲一個單位,做插入排序,如,給 592和72 做一次插入排序 ( 下標增量是5 ) 變成了 72和592這樣我們實際上就是用1步做到了移動5步
PS:直接用增量5的下標差換一下位置,並不用一個個蹭過去
給剩下幾組做同樣的事情,我們會發現小的數據僅用了幾步,就基本上移到了前面

可是這樣還不夠呀!
我需要更精確的排序
接下來分爲 2組 ,每組 5個元素
再給每組來一遍插入排序 (中跨步微調)
這個時候每次增量爲2,效率還是稍微高一點的
在這裏插入圖片描述
最後
整體數據已經比較有序了,用正常的插入過一遍整個數組就行,這時候很省時,因爲整體很有序
在這裏插入圖片描述
(可能這時候有人不太理解了,你最後也是一邊完整的插入排序,這時間複雜度不是更高了麼,實際上你可以這麼想,我只不過是把插入排序拆分開了,在前面無論是間隔4,還是間隔1,間隔0的排序,合起來就是原來插入排序的整體過程,但是在我間隔4和間隔2的時候,我跨越了很多數據,使得整體遍歷的時間變短了,而最後一邊間隔0的排序,實際上只動了那麼幾個元素,總體上,時間複雜度小很多 )

增量序列:

值得注意的是,我上面的選擇了5組,2組,1組這種方式,實際上就是元素個數10不斷除2的結果
這時候我們稱這個叫 希爾增量序列,是 { N/2,N/4,...,1{N/2 ,N/4 , ... ,1} }
但實際上這種序列並不是很有效,很多時候我分組時跨的步數過大,使得後續排序效率變低
那麼有沒有更高效的方式來遍歷,有!
Hibbard增量序列:{1,3,...,2k11, 3, ..., 2^k-1}
Sedgewick增量序列:{1,5,19,41,109,...1, 5, 19, 41, 109,...}
其中數據由 9×4i9×2i+19 \times 4^i - 9 \times 2^i + 12i+2×(2i+23)+12^{i+2}\times(2^{i+2}-3)+1 兩個算式生成,i 取正整數
這些經過實踐,要比純粹的希爾增量序列快一些,特別是Sedgewick增量序列

代碼:

  • 首先做一個循環,用來枚舉不用的增量det,通過增量來分組,如5,2,1這種分組
  • 然後再嵌套一個循環來枚舉每個組
  • 對每個組做一次插入排序,這裏跟插入排序唯一的差別就是插入是一個個向前檢索,但是這個插入是以增量det爲間隔向前檢索

使用希爾增量序列的代碼

void ShellSort_Shell(){
    for(int det=n/2; det>=1; det/=2)
        for(int i=0; i<det; i++)
            for(int j=i+det; j<n; j+=det)
                for(int x=j; x-det>=0&&A[x]<A[x-det]; x-=det) swap(A[x-det],A[x]);
}

使用Sedgewick增量序列的代碼

void ShellSort_Hibbard(){
    int jShu = 1;
    jZhi[0] = 1;
    for(int i=1; jZhi[jShu-1]<n; i++){
        jZhi[jShu++] = pow(2,i+2)*(pow(2,i+2)-3)+1;
        if(jZhi[jShu-1]>=n) break;
        jZhi[jShu++] = 9*pow(4,i)-9*pow(2,i)+1;
    }
    //上面那個就是構造Sedgewick增量序列的過程,你也可以手工打表
    //pow(a,b)函數,用來算a^b
    for(int i=0; i<jShu; i++){
        int det=jZhi[jShu-1-i];
        for(int j=0; j<det; j++)
           for(int x=j+det; x<n; x+=det)
               for(int y=x; y-det>=0&&A[y]<A[y-det]; y-=det) swap(A[y-det],A[y]);
    }
}

可以看到優化後還是快一點的
數據量1000000

快速排序:

冒泡排序的改進版本,排序算法中的dalao,據說是內部適用性最好的排序
平均時間複雜度 O(nlog2n)O (n\cdot{log_2n})最壞時間複雜度 O(n2)O (n^2)

大體思路:

快速排序利用二分的思想,先找一個基準值,通過基準值把數組不斷的二分,再在之後的小數組上重複步驟
比如說

4 9 5 0 7 2 1 3 8 6

假如說我總以最後一個元素爲基準值 ( 常被稱作Key,在代碼中我寫作Bet ) ,這個數組是6
這個時候我把比6小的放在6的前面,把比6大的放在後面
最後可以得到

4 5 0 2 1 3 6 9 7 8

可以看到這個時候我的6本身是到達指定位置了的,那麼這時候就以6爲邊界分開左右數組

4 5 0 2 1 3 邊界 9 7 8
邊界

兩個小數組,在進行同樣的操作 以最後一個元素爲基準值,這兩個數組分別是38 處理過後有

0 2 1 3 4 5 邊界 7 8 9
邊界

再以38爲邊界左右分開這兩個數組。。。
直到分得的數組長度<=1,就不再分
最後整體就是有序的了

聽起來很簡單,但是那該怎麼實現呢?

實現方法:

左右指針式寫法快排:

  • 首先我們需要不斷地以區間爲分界分開數組,這時候就要用到遞歸,怎麼在同一個數組上處理成不同的小數組?顯然我們需要知道每個小數組的首元素下標和尾元素下標 (在下段代碼中是beginend),以這個爲依據來分開數組

  • 之後我們需要取一個基準值來分開數組,這個值我們通常取每個數組的首元素尾元素或者中間元素
    ( 下面代碼我們先以取中間元素爲例 )

  • 然後設置兩個左右指針RightLeft,一個向左遍歷,一個向右遍歷,直到它們相遇 每當Left指針找到一個比基準值大的,Right指針找到一個比基準值小的,則交換這兩個數,一邊下來以後,整個數組就以基準值爲邊界分開了左右兩邊,而且LeftRight指針也一定會在基準值的位置相遇

取中值爲基準值版

void QuickSortA(int begin, int end){
    int Left = begin, Right = end;
    int Bet = A[(begin+end)/2];
    while(Left <= Right){
        while(A[Left] < Bet) Left++;
        while(A[Right] > Bet) Right--;
        if(Left <= Right) swap(A[Left++], A[Right--]);
    }
    if(begin < Right) QuickSortA(begin, Right);
    if(Left < end) QuickSortA(Left, end);
}

三值優化版
但是實際上無論是以中值爲基準值還是以首元素,尾元素爲基準值都有一定弊端
具體表現就是當我以首元素爲基準值,如果這個序列本身就是一個升序列的類似序列,此時的快排就會退化成冒泡排序,變成奇慢無比的O(n2)O (n^2)
( 每次二分只能分出一個數組,因爲首元素就是最小值,所以只有一邊 )
同樣的以尾元素和中間元素也會面臨同樣的問題,他們也會遇到一種相似的序列,使得其退化成O(n2)O (n^2)
( 其實你怎麼取基準值總會有一種序列使你的快排退化成O(n2)O (n^2),只不過首元素,尾元素,中間元素這種退化序列容易被構造出來)

所以說爲了滿足絕大部分條件,最好的方法是每次都取一個隨機位置的基準值,而不是固定基準值,這樣的話遇到一個使其退化的序列簡直是萬年一遇,特別是大量數據的情況

然而生成隨機數需要額外的時間複雜度,有些得不償失
所以三值優化法是最常用的快排取基準值方法
原理很簡單,就是在首元素,尾元素,中間元素中取中間大小的那個值作爲基準值,簡單粗暴,滿足絕大部分要求

#define MAX(x,y) ((A[x] > A[y])?x : y)
#define MIN(x,y) ((A[x] < A[y])?x : y)
#define MID(x,y,z) x+y+z-MAX(x,MAX(y,z))-MIN(x,MIN(y,z))

void QuickSortA(int begin, int end){
    int Left = begin, Right = end;
    int Bet = A[MID(begin, end, (begin+end)/2)];
    while(Left <= Right){
        while(A[Left] < Bet) Left++;
        while(A[Right] > Bet) Right--;
        if(Left <= Right) swap(A[Left++], A[Right--]);
    }
    if(begin < Right) QuickSortA(begin, Right);
    if(Left < end) QuickSortA(Left, end);
}

前後指針式寫法快排:

前後指針法比較特殊,跟挖坑法不同,它更適合於遍歷鏈表

整體流程:
稍微有些難以理解XD,建議手動嘗試一邊

首先我們要聲明兩個指針( 這裏我們用數組下標代替 ),一個是 Pre ,一個是 Cur
首先,對於每個數組,我們先以末尾元素爲基準值,把 Cur 初始值設置爲第一個元素的數組下標,Pre 的初始值則是 Cur-1
然後通過 Cur 指針來遍歷數組

  • 如果 Cur 指向的元素比基準值要大,則繼續遍歷;
  • 如果 Cur 指向的元素比基準值要小,則先將 Pre 的值+1,再判斷一下Cur 指向的元素和 Pre 指向的元素是否相等,如果不相等就交換,如果相等就繼續遍歷

最後交換 Pre 指針+1指向的元素和左右一個末尾元素,Pre 指針的指向的位置就是分開數組的中間值

關於前後指針法的解釋:

爲何這種方式可以做到把比基準值小的和比基準值大的分開在基準值兩邊?
我們可以這麼思考,假如說現在我把重點放在如何找到一個位置,使得其能分開左右兩邊(一次快排之後,小的都在左面,大的都在右面)
那麼Pre指針就是做這個事情
比如說

4 5 9 0 7 2 1 3 8 6
0 1 2 3 4 5 6 7 8 9

Cur指針先遍歷第一個元素4,我們知道這個是元素比基準值6小,所以說以目前的情況來看,Pre指向的位置至少是0位置的後面

4 pre 5 9 0 7 2 1 3 8 6
0 pre 1 2 3 4 5 6 7 8 9

Cur指針遍歷第二個元素5,依然比基準值小,這個時候我知道Pre指向的位置至少是1位置的後面

4 5 pre 9 0 7 2 1 3 8 6
0 1 pre 2 3 4 5 6 7 8 9

PS:這兩步爲了好理解沒有加入交換步驟,不過可以看到如果都是比基準值小的,即使加入交換步驟也不會有什麼問題
Cur指針遍歷第三個元素9,比基準值大了,那我們要保證它在基準值的右面,那麼這個時候Pre指針不動,但我們現在知道它的位置需要隔開5和9

4 5 0 pre 9 7 2 1 3 8 6
0 1 2 pre 3 4 5 6 7 8 9

Cur指針遍歷第四個元素0,又比基準值小,那我們要保證它在基準值左面,但是我們知道前面有個9,這時候交換0和9,再讓指針指向2的後面隔開 4 5 0 和 9

4 5 0 pre 9 7 2 1 3 8 6
0 1 2 pre 3 4 5 6 7 8 9

Cur指針遍歷第五個元素7,比基準值大,要保證它在基準右面,所以pre指針不動
Cur指針遍歷第六個元素2,比基準值小,保證其在基準值左面,所以交換 2 和 9 ,再把pre指針指向 2 後面

4 5 0 2 pre 7 9 1 3 8 6
0 1 2 3 pre 4 5 6 7 8 9


下面幾個元素同理,可以見到最後一步只需要把末尾元素(基準值) 放到pre指向的位置即可,這樣就構造出了合適的形式,所以說我們可以總結出兩條

  • 如果 Cur 指向的元素比基準值要大,則繼續遍歷;
  • 如果 Cur 指向的元素比基準值要小,則先將 Pre 的值+1,再判斷一下Cur 指向的元素和 Pre 指向的元素是否相等,如果不相等就交換,如果相等就繼續遍歷

常用版本,以末尾爲基準值

void QuickSortB(int begin, int end){
    int Pre = begin-1, Cur = begin;
    int Bet = A[end];
    while(Cur < end){
        if(A[Cur]<Bet && A[Cur]!=A[++Pre]) swap(A[Pre], A[Cur]);
        Cur++;
    }
    swap(A[++Pre], A[end]);
    if(begin < Pre-1) QuickSortB(begin, Pre-1);
    if(end > Pre+1) QuickSortB(Pre+1, end);
}

三值優化版

#define MAX(x,y) ((A[x] > A[y])?x : y)
#define MIN(x,y) ((A[x] < A[y])?x : y)
#define MID(x,y,z) x+y+z-MAX(x,MAX(y,z))-MIN(x,MIN(y,z))

void QuickSortB(int begin, int end){
    int Pre = begin-1, Cur = begin;
    int Shu = MID(begin, end, (begin+end)/2);
    int Bet = A[Shu];
    swap(A[Shu], A[end]); //把合適的基準值和尾元素交換,然後其他代碼照搬XD
    while(Cur < end){
        if(A[Cur]<Bet && A[Cur]!=A[++Pre]) swap(A[Pre], A[Cur]);
        Cur++;
    }
    swap(A[++Pre], A[end]);
    if(begin < Pre-1) QuickSortB(begin, Pre-1);
    if(end > Pre+1) QuickSortB(Pre+1, end);
}

但這還遠遠不夠!—— 三向快排優化

現在我們解決了選取基準值的問題,讓大部分數據不會退化成 O(n2)O (n^2) ,也給出了適合鏈表的快排方式,但我們是不是忘記了些什麼?
沒錯,如果一組數據有大量重複數據怎麼辦,或者說更極端一些,一個數組中幾乎全是重複數據,此時用 O(nlog2n)O (n\cdot{log_2n}) 來排序豈不是有點虧!

這時候就要讓我們的 三向快排 出場了 (其實這部分內容性價比不高,如果不是特別需要可以略過)

void QuickSortC(int begin, int end){
    if(begin>=end) return;
    int Mid_l = begin, Mid_r = end, i = Mid_l+1;
    swap(A[MID(begin,end,(begin+end)/2)], A[begin]);
    while(i <= Mid_r){
        if(A[Mid_l]>A[i]) swap(A[Mid_l++], A[i++]);
        else if(A[Mid_l]<A[i]) swap(A[Mid_r--], A[i]);
        else i++;
    }
    QuickSortC(begin, Mid_l-1);
    QuickSortC(Mid_r+1, end);
}

歸併排序

  • 待更
void Merge(int Left, int Bet, int Right){
    int *temp = new int[Right-Left+1];
    int jShu = 0, jShuA = Left, jShuB = Bet+1;
    while(jShuA <= Bet&&jShuB <= Right) temp[jShu++] = A[jShuA]<=A[jShuB] ? A[jShuA++] : A[jShuB++];
    if(jShuA <= Bet) for(int i=jShuA; i<=Bet; i++) temp[jShu++] = A[i];
    if(jShuB <= Right) for(int i=jShuB; i<=Right; i++) temp[jShu++] = A[i];
    for(int i=0; i<jShu; i++) A[Left++] = temp[i];
}
//遞歸版
void MergeSort1(int begin, int end){
    if(begin<end){
        int bet = (begin+end)/2;
        MergeSort1(begin, bet);
        MergeSort1(bet+1, end);
        Merge(begin, bet, end);
    }
}
//循環版
void MergeSort2(int Len){
    for(int i=1; i<Len; i*=2){
        int Left = 0;
        while(Left+i<Len){
            int Bet = Left+i-1;
            int Right = Bet+i>Len ? Len-1 : Bet+i;
            Merge(Left, Bet, Right);
            Left = Right+1;
        }   
    }
}

堆排序

  • 待更

計數排序

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