經典排序演算法PHP實現

Sinkiang發表於2018-08-21

所謂排序,就是使一串記錄,按照其中的某個或某些關鍵字的大小,遞增或遞減的排列起來的操作。排序演算法,就是如何使得記錄按照要求排列的方法。排序演算法在很多領域得到相當地重視,尤其是在大量資料的處理方面。一個優秀的演算法可以節省大量的資源。在各個領域中考慮到資料的各種限制和規範,要得到一個符合實際的優秀演算法,得經過大量的推理和分析。

演算法複雜度分為時間複雜度和空間複雜度。其作用:時間複雜度是指執行演算法所需要的計算工作量;而空間複雜度是指執行這個演算法所需要的記憶體空間。

時間頻度

一個演算法執行所耗費的時間,從理論上是不能算出來的,必須上機執行測試才能知道。但我們不可能也沒有必要對每個演算法都上機測試,只需知道哪個演算法花費的時間多,哪個演算法花費的時間少就可以了。並且每個演算法花費的時間與演算法中語句的執行次數成正比,哪個演算法中語句執行次數多,它花費時間就多。一個演算法中的語句執行次數稱為語句頻度或時間頻度,記為T(n)。

時間複雜度

在時間頻度中,n稱為問題的規模,當n不斷變化時,時間頻度T(n)也會不斷變化。但有時我們想知道它變化時呈現什麼規律,為此,我們引入時間複雜度概念。一般情況下,演算法中基本操作重複執行的次數是問題規模n的某個函式,用T(n)表示,若有某個輔助函式f(n),使得當n趨近於無窮大時,T(n) / f(n) 的極限值為不等於零的常數,則稱f(n)是T(n)的同數量級函式。記作T(n) = O( f(n) ),稱O( f(n) )為演算法的漸進時間複雜度,簡稱時間複雜度。

在計算時間複雜度的時候,先找出演算法的基本操作,然後根據相應的各語句確定它的執行次數,再找出T(n)的同數量級(它的同數量級有以下:1,log2n, n, nlog2n, n2,n3,2n,n!),找出後,f(n) = 該數量級,若 T(n) / f(n)求極限可得到一常數c,則時間複雜度T(n) = O( f(n) )。

空間複雜度

一個程式的空間複雜度是指執行完一個程式所需記憶體的大小。利用程式的空間複雜度,可以對程式的執行所需要的記憶體多少有個預先估計。一個程式執行時除了需要儲存空間和儲存本身所使用的指令、常數、變數和輸入資料外,還需要一些對資料進行操作的工作單元和儲存一些為現實計算所需資訊的輔助空間。程式執行時所需儲存空間包括以下兩部分:

a )、固定部分。這部分空間的大小與輸入/輸出的資料的個數多少、數值無關。主要包括指令空間(即程式碼空間)、資料空間(常量、簡單變數)等所佔的空間。這部分屬於靜態空間。b )、可變空間。這部分空間主要包括動態分配的空間,以及遞迴棧所需的空間等。這部分的空間大小與演算法有關。一個演算法所需的儲存空間用f(n)表示。S(n) = O( f(n) ) 其中n為問題的規模,S(n) 表示空間複雜度。

分類

排序演算法可以分為內部排序和外部排序,內部排序是資料記錄在記憶體中進行排序,而外部排序是指在排序期間全部物件太多,不能同時存放在記憶體中,必須根據排序過程的要求,不斷在內,外存間移動的排序。常見的內部排序演算法有:插入排序、希爾排序、選擇排序、氣泡排序、歸併排序、快速排序、堆排序、基數排序等。用一張圖概括:

經典排序演算法PHP實現

(一)、氣泡排序演算法

氣泡排序是一種簡單直觀的排序演算法,它重複地走訪過要排序的數列,一次比較兩個元素,如果它們的順序錯誤就把它們交換過來。走訪數列的工作是重複地進行直到沒有再需要交換,也就是說該數列已經排序完成。這個演算法的名字由來是因為越小的元素會經由交換慢慢“浮”到數列的頂端。

作為最簡單的排序演算法之一,氣泡排序給我的感覺就像 Abandon 在單詞書裡出現的感覺一樣,每次都在第一頁第一位,所以最熟悉。氣泡排序還有一種優化演算法,就是立一個 flag,當在一趟序列遍歷中元素沒有發生交換,則證明該序列已經有序。但這種改進對於提升效能來說並沒有什麼太大作用。

演算法步驟

  1. 比較相鄰的元素。如果第一個比第二個大,就交換他們兩個。

  2. 對每一對相鄰元素作同樣的工作,從開始第一對到結尾的最後一對。在這一點,最後的元素應該會是最大的數。

  3. 針對所有的元素重複以上的步驟,除了最後一個。

  4. 持續每次對越來越少的元素重複上面的步驟,直到沒有任何一對數字需要比較。

效率分析

若檔案的初始狀態是正序的,一趟掃描即可完成排序。所需的關鍵字比較次數C和記錄移動次數M均達到最小值:Cmin = n - 1, Mmin = 0,所以,氣泡排序最好的時間複雜度為O(n)。

若初始檔案是反序的,需要進行n-1趟排序。每趟排序要進行 n-i 次關鍵字的比較(1 ≤ i ≤ n-1),且每次比較都必須移動記錄三次來達到交換記錄位置。在這種情況下,比較和移動次數均達到最大值:

Cmax = n(n-1)/2 = O(n2)

Mmax = 3n(n-1)/2 = O(n2)

氣泡排序的最壞時間複雜度為O(n2),綜上,因此氣泡排序總的平均時間複雜度為O(n2)。

演算法穩定性

氣泡排序就是把小的元素往前調或者把大的元素往後調。比較是相鄰的兩個元素比較,交換也發生在這兩個元素之間。所以,如果兩個元素相等,我想你是不會再無聊地把他們交換一下的;如果兩個相等的元素沒有相鄰,那麼即使通過前面的兩兩交換把兩個相鄰起來,這時候也不會交換,所以相同元素的前後順序並沒有改變,所以氣泡排序是一種穩定排序演算法。

程式碼實現

<?php

function bubbleSort(array $numbers = array())
{
    $count = count($numbers);
    if ($count <= 1) return $numbers;
    for ($i = 0; $i < $count-1; ++$i) {
        for ($j = 0; $j < $count-$i-1; ++$j) {
            if($numbers[$j] > $numbers[$j+1]) {
                $temp = $numbers[$j];
                $numbers[$j] = $numbers[$j+1];
                $numbers[$j+1] = $temp;
            }
        }
    }
    return $numbers;
}
複製程式碼

(二)、選擇排序演算法

選擇排序是一種簡單直接的排序演算法。它的工作原理如下,首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然後再從剩餘未排序元素中繼續尋找最小(大)元素,然後放到已排序序列的末尾。以此類推,直到所有元素均排序完畢。

選擇排序的主要優點與資料移動有關。如果某個元素位於正確的最終位置上,則它不會被移動。選擇排序每次交換一對元素,它們當中至少有一個將被移動到其最終位置上,因此對n個元素進行排序總共進行至多n-1次交換。在所有的完全依靠交換去移動元素的排序方法中,選擇排序屬於好的一種。

演算法步驟

  1. 首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置

  2. 再從剩餘未排序元素中繼續尋找最小(大)元素,然後放到已排序序列的末尾。

  3. 重複第二步,直到所有元素均排序完畢。

效率分析

在選擇排序中,共需要進行n-1次選擇和交換,每次選擇需要進行 n-i 次比較(1 <= i <= n-1),而每次交換最多需要3次移動,因此,總的比較次數 C = n (n - 1) / 2。

交換次數為O(n),最好情況是,已經有序,交換0次;最壞情況是,逆序,交換n-1次。交換次數比氣泡排序較少,由於交換所需CPU時間比氣泡排序所需的CPU時間多,n值較小時,選擇排序比氣泡排序快。

原地操作幾乎是選擇排序的唯一優點,當空間複雜度要求較高時,可以考慮選擇排序;實際適用的場合非常罕見。

演算法穩定性

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

程式碼實現

<?php

function selectSort(array $numbers = array())
{
    $count = count($numbers);
    if ($count <= 1) return $numbers;
    for ($i = 0; $i < $count-1; ++$i) {
        $min = $i;
        for ($j = $i+1; $j < $count; ++$j) {
            if($numbers[$min] > $numbers[$j]) {
                $min = $j;
            }
        }
        if($min != $i) {
            $temp = $numbers[$min];
            $numbers[$min] = $numbers[$i];
            $numbers[$i] = $temp;
        }
    }
    return $numbers;
}
複製程式碼

(三)、快速排序演算法

快速排序,又稱劃分交換排序,一種排序演算法,最早由東尼·霍爾提出。在平均狀況下,排序n個專案要O(n log n)次比較。在最壞狀況下則需要O(n2)次比較,但這種狀況並不常見。事實上,快速排序通常明顯比其他O(n log n)演算法更快,因為它的內部迴圈(inner loop)可以在大部分的架構上很有效率地被實現出來。

演算法步驟

快速排序使用分治法策略來把一個序列分為兩個子序列,步驟:

  1. 從數列中挑出一個元素,稱為"基準"。

  2. 重新排序數列,所有元素比基準值小的擺放在基準前面,所有元素比基準值大的擺在基準的後面(相同的數可以到任一邊)。在這個分割槽結束之後,該基準就處於數列的中間位置。這個稱為分割槽操作。

  3. 遞迴地把小於基準值元素的子數列和大於基準值元素的子數列排序。

遞迴的最底部情形,是數列的大小是零或一,也就是永遠都已經被排序好了。雖然一直遞迴下去,但是這個演算法總會結束,因為在每次的迭代中,它至少會把一個元素擺到它最後的位置去。

效率分析

從一開始快速排序平均需要花費O(n log n)時間的描述並不明顯。但是不難觀察到的是分割槽運算,陣列的元素都會在每次迴圈中走訪過一次,使用O(n)的時間。在使用結合的版本中,這項運算也是O(n)。

在最好的情況,每次我們執行一次分割槽,我們會把一個數列分為兩個幾近相等的片段。這個意思就是每次遞迴呼叫處理一半大小的數列。因此,在到達大小為一的數列前,我們只要作log n次巢狀的呼叫。這個意思就是呼叫樹的深度是O(log n)。但是在同一層次結構的兩個程式呼叫中,不會處理到原來數列的相同部分;因此,程式呼叫的每一層次結構總共全部僅需要O(n)的時間(每個呼叫有某些共同的額外耗費,但是因為在每一層次結構僅僅只有O(n)個呼叫,這些被歸納在O(n)係數中)。結果是這個演算法僅需使用O(n log n)時間。

另外一個方法是為T(n)設立一個遞迴關係式,也就是需要排序大小為n的數列所需要的時間。在最好的情況下,因為一個單獨的快速排序呼叫牽涉了O(n)的工作,加上對n/2大小之數列的兩個遞迴呼叫,這個關係式可以是:

T(n) = O(n) + 2T(n/2)

解決這種關係式型別的標準數學歸納法技巧告訴我們T(n) = O(n log n)。

事實上,並不需要把數列如此精確地分割槽;即使如果每個基準值將元素分開為99%在一邊和1%在另一邊,呼叫的深度仍然限制在100logn,所以全部執行時間依然是O(n log n)。

然而,在最壞的情況是,兩子數列擁有大各為1和n-1,且呼叫樹變成為一個n個巢狀呼叫的線性連串。第i次呼叫作了O(n-i)的工作量,且 ∑i = 0n(n - i) = O(n2)遞迴關係式為:

T(n) = O(n) + T(1) + T(n - 1) = O(n) + T(n - 1)

這與插入排序和選擇排序有相同的關係式,以及它被解為T(n) = O(n2)。

這一段來自維基百度,讀起來有點難懂,不過沒有關係,直接看例子就好了。

演算法穩定性

快速排序有兩個方向,左邊的i下標一直往右走,當a[i] <= a[center_index],其中center_index是中樞元素的陣列下標,一般取為陣列第0個元素。而右邊的j下標一直往左走,當a[j] > a[center_index]。如果i和j都走不動了,i <= j, 交換a[i]和a[j],重複上面的過程,直到i>j。 交換a[j]和a[center_index],完成一趟快速排序。在中樞元素和a[j]交換的時候,很有可能把前面的元素的穩定性打亂,比如序列為 5 3 3 4 3 8 9 10 11, 現在中樞元素5和3(第5個元素,下標從1開始計)交換就會把元素3的穩定性打亂,所以快速排序是一個不穩定的排序演算法,不穩定發生在中樞元素和a[j] 交換的時刻。

程式碼實現

<?php

function quickSort(array $numbers = array())
{
   $count = count($numbers);
   if ($count <= 1) return $numbers;
   $left = $right = array();
   $mid_value = $numbers[0];
   for ($i = 1; $i < $count; ++$i) {
           if($numbers[$i] < $mid_value) {
               $left[] = $numbers[$i];
           } else {
               $right[] = $numbers[$i];
           }
   }
   return array_merge(quickSort($left),(array)$mid_value,quickSort($right));
}
複製程式碼

(四)、插入排序演算法

插入排序是一種簡單直觀的排序演算法。它的工作原理是通過構建有序序列,對於未排序資料,在已排序序列中從後向前掃描,找到相應位置插入。插入排序在實現上,通常採用in-place排序(即只需要用到O(1)的額外空間的排序),因而在從後向前掃描過程中,需要反覆把已排序元素逐步向後挪位,為最新元素提供插入空間。

演算法步驟

  1. 從第一個元素開始,該元素可以認為已經被排序

  2. 取出下一個元素,在已經排序的元素序列中從後向前掃描

  3. 如果該元素(已排序大於新元素),將該元素移動到下一位置

  4. 重複步驟3,直到找到已排序的元素小於或者等於新元素的位置

  5. 將新元素插入到該位置後

  6. 重複步驟2~5

效率分析

如果目標是把n個元素的序列升級排序,那麼採用插入排序存在最好情況和最壞情況。最好情況就是,序列已經是升序排列了,在這種情況下,需要進行的比較操作需(n-1)次即可。最壞的情況就是,序列是降序排列,那麼此時需要進行的比較共有1/2 * n(n-1)次。插入排序的賦值操作是比較操作的次數加上(n-1)次。平均來說插入排序演算法複雜度為O(n2)。因而,插入排序不適合對於資料量比較大的排序應用。但是,如果需要排序的資料量很小,例如,量級小於千,那麼插入排序還是一個不錯的選擇。插入排序在工業級庫中也有著廣泛的應用,在STL的sort演算法和stdlib的qsort演算法中,都將插入排序作為快速排序的補充,用於少量元素的排序。

演算法穩定性

插入排序是在一個已經有序的小序列的基礎上,一次插入一個元素。當然,剛開始這個有序的小序列只有1個元素,就是第一個元素。比較是從有序序列的末尾開始,也就是想要插入的元素和已經有序的最大者開始比起,如果比它大則直接插入在其後面,否則一直往前找直到找到它該插入的位置。如果碰見一個和插入元素相等的,那麼插入元素把想插入的元素放在相等元素的後面。所以,相等元素的前後順序沒有改變,從原無序序列出去的順序就是排好序後的順序,所以插入排序是穩定的。

程式碼實現

<?php

function insertSort(array $numbers = array())
{
    $count = count($numbers);
    if ($count <= 1) return $numbers;
    for ($i = 1; $i < $count; ++$i) {
         $temp = $numbers[$i];
         for ($j = $i-1; $j >= 0 && $numbers[$j] > $temp; --$j) {
             $numbers[$j+1] = $numbers[$j];
         }
         $numbers[$j+1] = $temp;
    }
    return $numbers;
}
複製程式碼

(五)、希爾排序演算法

希爾排序是插入排序的一種,也稱縮小增加排序,是直接插入排序演算法的一種更高效的改進版本。希爾排序是非穩定排序演算法。該方法因DL.Shell於1959年提出而得名。

該方法的基本思想是:先將整個待排元素序列分割成若干個子序列(由相隔某個“增量”的元素組成的)分別進行直接插入排序,然後依次縮減增量再進行排序,待整個序列中的元素基本有序(增量足夠小)時,再對全體元素進行一次直接插入排序。因為直接插入排序在元素基本有序的情況下(接近最好情況),效率是很高的,因此希爾排序在時間效率上比前兩種方法有較大提高。

演算法步驟

  1. 取增量,一般取陣列長度 / 2;

  2. 按增量取得一個子數列,對子數列按插入排序的方式處理;

  3. 將增量遞減,重複1,2步驟;

  4. 直至增量均為0,數列已經排好序;

效率分析

希爾排序是按照不同步長對元素進行插入排序,當剛開始元素很無序的時候,步長最大,所以插入排序的元素個數很少,速度很快;當元素基本有序了,步長很小,插入排序對於有序的序列效率很高。所以,希爾排序的時間複雜度會比O(n2)好一些。

演算法穩定性

希爾排序是按照不同步長對元素進行插入排序,當剛開始元素很無序的時候,步長最大,所以插入排序的元素個數很少,速度很快;當元素基本有序了,步長很小, 插入排序對於有序的序列效率很高。所以,希爾排序的時間複雜度會比O(n2)好一些。由於多次插入排序,我們知道一次插入排序是穩定的,不會改變相同元 素的相對順序,但在不同的插入排序過程中,相同的元素可能在各自的插入排序中移動,最後其穩定性就會被打亂,所以shell排序是不穩定的。

程式碼實現

<?php

function shellSort(array $numbers = array())
{
    $count = count($numbers);
    if ($count <= 1) return $numbers;
    for ($gap = floor($count/2); $gap > 0; $gap = floor($gap/=2)) {
        for($i = $gap; $i <$count; ++$i) {
            for($j = $i - $gap; $j >= 0 && $numbers[$j+$gap] < $numbers[$j]; $j = $j - $gap) {
                $temp = $numbers[$j];
                $numbers[$j] = $numbers[$j+$gap];
                $numbers[$j+$gap] = $temp;
            }
        }
    }
    return $numbers;
}
複製程式碼

(六)、歸併排序演算法

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

歸併過程為:比較a[i]和a[j]的大小,若a[i]≤a[j],則將第一個有序表中的元素a[i]複製到r[k]中,並令i和k分別加上1;否則將第二個有序表中的元素a[j]複製到r[k]中,並令j和k分別加上1,如此迴圈下去,直到其中一個有序表取完,然後再將另一個有序表中剩餘的元素複製到r中從下標k到下標t的單元。歸併排序的演算法我們通常用遞迴實現,先把待排序區間[s,t]以中點二分,接著把左邊子區間排序,再把右邊子區間排序,最後把左區間和右區間用一次歸併操作合併成有序的區間[s,t]。

演算法步驟

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

  2. 設定兩個指標(即陣列下標),最初位置分別為兩個已經排序序列的起始位置;

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

  4. 重複步驟3直到某一指標達到序列尾;

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

效率分析

歸併排序的效率是比較高的,設數列長為N,將數列分開成小數列一共要logN步,每步都是一個合併有序數列的過程,時間複雜度可以記為O(N),故一共為O(n * logn)。因為歸併排序每次都是在相鄰的資料中進行操作,所以歸併排序在O(N * logN)的幾種排序方法(快速排序,歸併排序,希爾排序,堆排序)也是效率比較高的。

演算法穩定性

歸併排序是把序列遞迴地分成短序列,遞迴出口是短序列只有1個元素(認為直接有序)或者2個序列(1次比較和交換),然後把各個有序的段序列合併成一個有 序的長序列,不斷合併直到原序列全部排好序。可以發現,在1個或2個元素時,1個元素不會交換,2個元素如果大小相等也沒有人故意交換,這不會破壞穩定 性。那麼,在短的有序序列合併的過程中,穩定是是否受到破壞?沒有,合併過程中我們可以保證如果兩個當前元素相等時,我們把處在前面的序列的元素儲存在結 果序列的前面,這樣就保證了穩定性。所以,歸併排序也是穩定的排序演算法。

程式碼實現

<?php

function mergeSort(array $numbers = array())
{
    $count = count($numbers);
    if($count <= 1) return $numbers;
    //將陣列分成兩份,$half = ceil($count/2)
    $half = ($count >> 1) + ($count & 1);
    $arr2d = array_chunk($numbers, $half);
    $left = mergeSort($arr2d[0]);
    $right = mergeSort($arr2d[1]);
    while (count($left) && count($right)) {
        if ($left[0] < $right[0]) {
            $reg[] = array_shift($left);
        } else {
            $reg[] = array_shift($right);
        }
        return array_merge($reg, $left, $right); 
    }
}

複製程式碼

(七)、桶排序演算法

桶排序或所謂的箱排序的原理是將陣列分到有限數量的桶子裡,然後對每個桶子再分別排序(有可能再使用別的排序演算法或是以遞迴方式繼續使用桶排序進行排序),最後將各個桶中的資料有序的合併起來。

假設有一組長度為N的待排序關鍵字序列K[1...n]。首先將這個序列劃分成M個的子區間(桶)。然後基於某種對映函式,將待排序序列的關鍵字K對映到第i個桶中(即桶陣列B的下標i),那麼該關鍵字k就作用B[i]中的元素(每個桶B[i]都是一組大小為N/M的序列)。接著對每個桶B[i]中的所有元素進行比較排序(可以使用快速排序)。然後依次列舉輸出B[0]...B[M]中的全部內容即是一個有序序列。

對映函式:bindex = f(k) 其中,bindex 為桶陣列B的下標(即第bindex個桶),k為待排序列的關鍵字。桶排序之所以能夠高效,其關鍵在於這個對映函式,它必須做到:如果關鍵字k1 < k2,那麼f(k1) <= f(k2)。也就是說B(i)中的最小資料都要大於B(i-1)中最大資料。很顯然,對映函式的確定與資料本身的特點有很大的關係,我們下面舉個例子:

假如待排序列K= {49、 38 、 35、 97 、 76、 73 、 27、 49 }。這些資料全部在1—100之間。因此我們定製10個桶,然後確定對映函式f(k)=k/10。則第一個關鍵字49將定位到第4個桶中(49/10=4)。依次將所有關鍵字全部堆入桶中,並在每個非空的桶中進行快速排序後得到如下圖所示:

經典排序演算法PHP實現

對上圖只要順序輸出每個B[i]中的資料就可以得到有序序列了。

演算法步驟

  1. 假設待排序的一組數統一的分佈在一個範圍中,並將這一範圍劃分成幾個子範圍,也就是桶。

  2. 將待排序的一組數,分檔規入這些子桶,並將桶中的資料進行排序。

  3. 將各個桶中的資料有序的合併起來。

效率分析

桶排序利用函式的對映關係,減少了幾乎所有的比較工作。實際上,桶排序的f(k)值的計算,其作用就相當於快排中劃分,已經把大量資料分割成了基本有序的資料塊(桶)。然後只需要對桶中的少量資料做先進的比較排序即可。

對N個關鍵字進行桶排序的時間複雜度分為兩個部分:

a )、迴圈計算每個關鍵字的桶對映函式,這個時間複雜度是O(N)。

b )、利用先進的比較排序演算法對每個桶內的所有資料進行排序,其時間複雜度為∑O(Ni * logNi) 。其中 Ni為第i個桶的資料量。

很顯然,第b部分是桶排序效能好壞的決定因素。儘量減少桶內資料的數量是提高效率的唯一辦法(因為基於比較排序的最好平均時間複雜度只能達到O(N*logN)了)。因此,我們需要儘量做到下面兩點:

a )、對映函式f(k)能夠將N個資料平均的分配到M個桶中,這樣每個桶就有[N/M]個資料量。

b )、儘量的增大桶的數量。極限情況下每個桶只能得到一個資料,這樣就完全避開了桶內資料的“比較”排序操作。 當然,做到這一點很不容易,資料量巨大的情況下,f(k)函式會使得桶集合的數量巨大,空間浪費嚴重。這就是一個時間代價和空間代價的權衡問題了。

對於N個待排資料,M個桶,平均每個桶[N/M]個資料的桶排序平均時間複雜度為:O(N)+O(M*(N/M)log(N/M))=O(N+N(logN-logM)) = O(N+NlogN-NlogM)

當N=M時,即極限情況下每個桶只有一個資料時。桶排序的最好效率能夠達到O(N)。

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

演算法穩定性

桶排序中,假如升序排列,a已經在桶中,b插進來是永遠都會a右邊的(因為一般是從右到左,如果不小於當前元素,則插入改元素的右側),所以桶排序是穩定的。

程式碼實現

<?php

function bucketSort($max, $array)
{
    //填充木桶
    $arr = array_fill(0, $max+1, 0);
    //開始標示木桶
    for ($i = 0; $i < count($array) - 1; ++$i) {
        ++$arr[$array[$i]];
    }
    $mutong = array();
    //開始從木桶中拿出資料
    for ($i = 0; $i < $max; ++$i) {
        for ($j = 1; $j <= $arr[$i]; ++$j) {
            //這一行主要用來控制輸出多個數字
            $mutong[] = $i;
        }
    }
    return $mutong;
}
複製程式碼

(八)堆排序

堆排序(Heapsort)是指利用堆積樹(堆)這種資料結構所設計的一種排序演算法,它是選擇排序的一種。可以利用陣列的特點快速定位指定索引的元素。堆分為大根堆和小根堆,是完全二叉樹。大根堆的要求是每個節點的值都不大於其父節點的值,即A[PARENT[i]] >= A[i]。在陣列的非降序排序中,需要使用的就是大根堆,因為根據大根堆的要求可知,最大的值一定在堆頂。

/**
 * 堆排序.
 * heap sort algorithm.
 *
 * @param array $value 待排序陣列 the array that is waiting for sorting
 *
 * @return array
 */
function heap(&$values = [])
{
    //堆化陣列
    // the array that is simulated a heap
    $heap = [];
    foreach ($values as $i=>$v) {
        $heap[$i] = $v;
        $heap = minHeapFixUp($heap, $i);
    }
    $values = $heap;

    //堆排序
    //heap sort algorithm
    $n = count($values);
    for ($i = $n-1; $i>=1; $i--) {
        swap($values[$i], $values[0]);
        minHeapFixDown($values, 0, $i);
    }
    return $values;
}

function swap(&$a, &$b) {
    $temp = $a;
    $a= $b;
    $b = $temp;
}

/**
 * 堆插入資料
 * the method that is used to insert data to a heap 
 * 
 * @param $values
 * @param $i
 * @return mixed
 */
function minHeapFixUp($values, $i) {

    $j = ($i-1)/2;
    $temp = $values[$i];

    while($j >= 0 && $i != 0) {

        if($values[$j] <= $temp) {
            break;
        }
        $values[$i] = $values[$j];
        $i = $j;
        $j = ($i-1)/2;
    }
    $values[$i] = $temp;
    return $values;
}

/**
 * 調整堆,可用於刪除堆節點
 * to adjust heap, it can be used to remove a node of heap.
 * 
 * @param $heap
 * @param $i
 * @param $n
 */
function minHeapFixDown(&$heap, $i, $n) {
    $j = 2*$i + 1;
    $temp = $heap[$i];

    while ($j < $n) {
        if($j+1 <$n && $heap[$j+1] < $heap[$j]) {
            $j++;
        }
        if($heap[$j] >= $temp) {
            break;
        }
        $heap[$i] = $heap[$j];
        $i = $j;
        $j = 2*$i + 1;
    }
    $heap[$i] = $temp;
}
複製程式碼

(九)基數排序

/**
 * 基數排序
 *
 * Least Significant Digit first
 *
 * 最低位優先排序
 *
 * @param array $value 待排序陣列
 *
 * @return array
 */
function radix_lsd(&$value = [])
{
  // 獲取序列值最大位數
  $max = 0;
  foreach ($value as $v) {
    $length = strlen((string)$v);
    if ($length > $max) {
      $max = $length;// 更新
    }
  }
  unset($v);
  $splice = 1;// 取最小位 初始從右往左數第一位

  while ($splice <= $max) {
    // 分配數字到桶中
    for ($i=0; $i < 10; $i++) {
      foreach ($value as $k => $v) {
        $length = strlen((string)$v);
        // 當前位索引位置
        $index = $length-$splice;
        // 不存在該位 則認為為0
        if ($index < 0) {
          if ($i === 0) {
            $arr[0][] = $v;
          }
          continue;
        }
        $aaa = ((string)$v)[$index];
        if (((string)$v)[$index] === (string)$i) {
          $arr[$i][] = $v;
        }
      }
      unset($v);
    }
    // 合併桶中數字
    unset($value);
    foreach ($arr as $tmp) {
      foreach ($tmp as $v) {
        $value[] = $v;
      }
    }
    unset($tmp);
    unset($v);
    unset($arr);
    ++$splice;
  }
  return $value;
}

/**
 * 基數排序
 *
 * Most Significant Digit first
 *
 * 最高位優先排序
 *
 * @param array $value 待排序陣列
 * @param integer $max 序列最大位數
 *
 * @return array
 */
function radix_msd(&$value = [], $max=0, $max_origin=0, $length_origin=0, &$origin=[], $key=0)
{
  if ($max < 1) {
    return;
  }

  // 按最高位分組,不存在當前位則認為0
  $arr = [];
  for ($i=0; $i < 10; $i++) {
    foreach ($value as $v) {
      $length = strlen((string)$v);
      $index = $length - $max;
      if ($index < 0) {
        if ($i === 0) {
          $arr[0][] = $v;
        }
        continue;
      }
      if (((string)$v)[$index] === (string)$i) {
        $arr[$i][] = $v;
      }
    }
    unset($v);
  }
  unset($i);
  --$max;

  if (!empty($origin)) {
    $origin[$key] = $arr;
  }else{
    $value = $arr;
  }
  foreach ($value as $k => &$v) {
    radix_msd($v, $max, $max_origin, $length_origin, $value, $k);
  }

  if ($max < $max_origin-1) {
    return;
  }

  // 重新拼接
  return get_value($value, $length_origin);
}

/**
 *&emsp;合併排序
 *
 * 合併最後按個位排序完成的值
 *
 * @param  [type]  $value  排序後值
 * @param  integer $length 原始陣列長度
 * @param  array  $result 存放排序後數的新空間
 * @return array          排序後陣列
 */
function get_value($value=[], $length=0, &$result=[])
{
  if (count($result) === $length) {
    return;
  }
  foreach ($value as $k => $v) {
    if (is_array($v)) {
      get_value($v, $length, $result);
      continue;
    }
    $result[] = $v;
  }
  return $result;
}

複製程式碼

(十)打賞

覺得可以請小弟喝瓶礦泉水吧。嘻嘻嘻

經典排序演算法PHP實現            經典排序演算法PHP實現

相關文章