乾貨分享:大話12種排序算法

常見的排序算法:php

  • 快速排序、堆排序、歸併排序、選擇排序
  • 插入排序、二分插入排序
  • 冒泡排序、雞尾酒排序
  • 桶排序、計數排序、基數排序、位圖排序

技能點
1.歸併排序在O(N*logN)的幾種排序方法(快速排序,歸併排序,希爾排序,堆排序)效率比較高。
2.位圖排序、計數排序 不是比較排序,經過空間消耗,實現時間複雜度O(n)的排序。算法

快速排序

經過一趟排序將待排記錄分割成獨立的A、B兩部分,A部分所有小於基準值,B部分所有大於基準值。而後在對兩部分作相同的處理,已完成排序的功能。數組

算法描述與分析
  1. 從數列中挑選一個元素,做爲基準值, pivot;
  2. 便利排序數列,比基準值小的放在左邊A,大的放在右邊B(相同的放在任意一邊)。待分組完成後基準值就處於A、B的中間位置。 這個過程稱爲分區(partition)操做,
    兩種實現方式具體實現見代碼
  3. 遞歸排序A、B兩部分,重複上面1 2兩步,進行排序。bash


    快速排序-圖例
複雜度

時間複雜度:
A. 數據結構

o(n logn)
* : 將數組分解爲一些列小問題進行獨立解決,上述的過程重複logn次獲得有序的序列; 每次都須要對n個進行一次處理,因此時間複雜度爲 n*logn

B. o(n^2): 固定選擇第一個元素作基準值,對倒序數組進行正序排列;每次劃分只獲得一個比上一次劃分少一個記錄的子序列,每次只排一個。至關於向後冒泡排序,因此時間複雜度爲o(n^2)app

空間複雜度:
額外使用的空間須要看具體的代碼實現,理論上最優狀況下空間複雜度爲o(1),只是用一個交換空間; 若是考慮遞歸的實現邏輯則複雜度爲 o(logn)ide

代碼實現

教科書式實現:大數據

//普通方法實現
function quick_sort( &$arr, $left, $right ){
    if( $left < $right){
        //取基準值,取最左邊的一個元素
        $pivot = $arr[$left];
        //隨機取基準值 - 避免對倒序數組進行正序排序時,時間複雜度 O(n^2)的狀況
        // $k = rand($left, $right);
        // swap($arr, $left, $k);
        // $pivot = $arr[$left];

        $i = $left; $j = $right;
       //一趟排序比較
        while($i < $j){
            //基準值從左邊選取的,因此先從右側開始比較;反之亦然。(注)
            while( $arr[$j] >= $pivot && $j > $i){
                $j--;
            }
            swap($arr, $i, $j);
            while( $arr[$i] < $pivot && $i < $j ){
                $i++;
            }
            swap($arr, $i, $j);
        }
        quick_sort($arr, $left, $i-1);  //對左邊內容進行排序
        quick_sort($arr, $i+1, $right); //右邊內容進行排序
    }
}

$arr = [49, 38, 65, 97, 76, 13, 27, 50];
quick_sort($arr, 0, count($arr)-1 );
echo implode(',', $arr);
複製代碼

劍指offer的實現:動畫

//分區方法. 這個思想很重要,能夠在多個問題中使用。如:取前幾名、字符串大小寫字母分組等
function partition(&$arr, $left, $right){
    //隨機、取最後一個爲基準值,方便從頭開始遍歷
    $k = rand($left, $right);
    swap($arr, $k, $right);
    $pivot = $arr[$right]; 
    // 取兩個指針A B,從{$left}開始走,B在遇到小於基準值是前走; 
    // 當A遇到小於基準值時和B處交換值;而後B指針前移一位。 
    // 完成一遍遍歷,B指針的位置就是分割位。在把基準值替換到B+1便可
    $b = $left-1;
    for($a=$left; $a<=$right; $a++){
        if( $arr[$a] < $pivot){
            $b++;
            if( $a != $b){
                swap($arr, $a, $b);
            }
        }
    }

    $b++;  //指針後移一位,而後替換基準值,保證前面的都小、後面的都大
    swap($arr, $b, $right);
    return $b;
}
//交換元素。除了這種方式還可使用:
// a=a+b;  b=a-b ;  a=a-b,加法來實現,不申請額外空間
// a=a^b;  b = a^b; a = b^a,異或來實現
function swap(&$arr, $i, $j){
    $t = $arr[$i];
    $arr[$i] = $arr[$j];
    $arr[$j] = $t;
}
//排序
function quick_sort( &$arr, $left, $right ){
    if( $left < $right){
        $index = partition($arr, $left, $right);
        quick_sort($arr, $left, $index-1);
        quick_sort($arr, $index+1, $right);
    }
}

$arr = [49, 38, 65, 97, 76, 13, 27, 50];
quick_sort($arr, 0, count($arr)-1 );
echo implode(',', $arr);

複製代碼

運行耗時出現兩種狀況:ui

A: 13,27,38,49,50,65,76,97[Finished in 0.2s]
B: 13,27,38,49,50,65,76,97[Finished in 0.1s]

緣由:排序使用了隨機基準值的方法,因此分區比較、分區大小都是隨機的。因此遞歸次數會有不一樣,耗時天然也就不一樣。
複製代碼

堆排序

利用堆這種數據結構,堆積生成一個近似徹底的二叉樹,而且知足堆積性質:即子節點的建值老是小於(或大於)它的父節點。 二叉樹使用數組來實現,從頭至尾對應堆的從上到下。

示例
最大堆(父節點大於任何一個子節點),則根節點時最大值。將根節點取出來,剩下的進行堆調整生成最大堆;重複上述步驟,直到堆無節點時完成排序。

海量數據TopK問題
用堆排序來解決海量數據TopK 問題會很是好。構建K個節點的最小堆;遍歷數據,當數據大於最小根節點時替換根節點,進行堆調整,生成最小堆;遍歷結束時,堆會保存其中最大的K個元素; 在進行堆排序,生成K個元素從大到小的排列。

算法描述與分析:

咱們這裏先介紹幾個問題,一步步推到堆排序。
什麼是堆
定義上面已有說明,示例以下:

最小堆結構

使用數組存儲:$arr = [1, 2, 3, 17, 19, 36, 7, 15, 170]

堆調整
爲了保證堆的特性而作的一個操做。將該根節點就行下沉操做,一直沉到合適的位置,使得剛纔的子樹知足堆的性質。
例如對最大堆進行堆調整:
1.對應的數組元素A[i], 左孩子 left(2i+1), 右孩子right(2i+2), 中找最大的那一個,將其下標存入largest中;
2.若是A[i] 是最大的元素,則程序直接結束;
3.不然,i的某個子節點爲最大元素,將A[i] 與 A[largest] 交換;
4.在從交換的子子節點開始,重複1,2,3步,直到葉子節點,完成一次堆調整。

堆調示例

建堆
建堆就是一個不斷進行堆調整的過程。在數組中,咱們通常從第n/2個數開始作堆調整,一直到下標爲0的數

(由於下標大於n/2的數,都是葉子節點,無子數,因此其子數已經知足堆的性質了)

堆調整隻判斷節點子樹,進行該節點下沉操做,並不和父節點進行比較,
因此建堆時必須從後向前進行,首先保證子樹知足特性,在一步步往上推。

ps: 數組下標從0開始,建堆節點應從下標 n/2 -1 開始

堆排序
堆排序是在建堆完成以後,進行的操做。
以最大堆爲例,堆以數組形式進行存儲,因此A[0]是最大值,將A[0]與A[n-1]交換,而後對A[0n-2]進行堆調整。第二次將A[0]與A[n-2]進行交換,A[0n-3]進行堆調整,重複這樣的操做直到A[0]與A[1]進行交換。 每次都將最大的數移入後面的區間,故操做完成以後,所得的數組就是從小到大有序排列的了。

堆排序示例

堆排序圖解

堆排序

堆排序
複雜度

時間複雜度:

O(n
logn)
* : 建堆過程爲O(n), 堆調整過程爲O(nlogn)
空間複雜度:
O(1) : 就地排序

代碼實現
<?php
$arr = [49, 38, 65, 97, 76, 13, 27, 50];
sortHeap($arr);

function sortHeap(&$arr){
    $heap_size = count($arr) - 1;
    createHeap($arr, $heap_size );
    echo '建堆結果:'.implode(',', $arr)."\r\n";
    heapPrint($arr, $heap_size);

    //堆排序
    $n = $heap_size;
    for($i=0; $i<=$n; $i++ ){
        swap($arr, 0, $heap_size); //最大值替換到最後面
        $heap_size--;
        adjustHeap($arr, $heap_size, 0); //A[0]替換爲新的值,從該節點進行堆調整
    }
    echo '堆排序結果'.implode(',', $arr)."\r\n";
}

//建堆
function createHeap( &$arr, $heap_size){
    $n = floor($heap_size/2) -1;
    for($i=$n; $i>=0; $i-- ){  
        // 從最後一個非葉子節點開始,下標遞減進行建堆。 
        // 保證節點的子樹知足堆的性質,才能進一步對節點的父節點進行堆調整; 不然會有問題
        adjustHeap($arr, $heap_size, $i);
    }

}

//堆調整 - 最大堆特性
function adjustHeap( &$arr, $heap_size, $i){
    $left = 2*$i + 1;    //左子節點
    $right = 2*$i + 2;   //右子節點
    $largest = $i;       //默認最大節點爲當前節點
    while( $left < $heap_size || $right<$heap_size){
        if( $left < $heap_size && $arr[$left] > $arr[$largest] ){
            $largest = $left; //左子節點大於目前值最大節點
        }
        if( $right<$heap_size && $arr[$right] > $arr[$largest] ){
            $largest = $right; //右子節點大於最大節點
        }
        //子節點大於該節點,須要將該節點下沉,並對下沉後的節點進行子樹堆調整
        if($largest != $i){
            swap($arr, $largest, $i);
            $i = $largest;
            $left = 2*$i+1;
            $right = 2*$i+2;
        }else{
            // 該節點值是最大值時,結束調整。 
            // 由於:深層次的子樹在建堆時已經知足堆性質,不須要再進行判斷
            break;
        }
    }
}

function swap(&$arr, $i, $j){
    $t = $arr[$i];  $arr[$i] = $arr[$j];  $arr[$j] = $t;
}

//堆打印
function heapPrint($arr, $heap_size){
    //判斷有多少行
    $rows = 1;
    while( pow(2, $rows-1) < $heap_size ){ $rows++;}
    //最後一行葉子節點的個數,最大個數
    $lastNumbers = pow(2, $rows-1);

    //輸出
    $row = 1;
    $num = pow(2, $row-1);
    for( $i=0; $i<$heap_size; $i++){
        $t = $lastNumbers/ ( pow(2, $row-1) +1 ); //空格平均分割。除以(數字個數+1)
        $t = ceil($t);
        for($j=0; $j<$t; $j++){ echo " " ; }
        echo $arr[$i];
        if( $i+1 == $num ){
            echo "\r\n";
            $row++;
            $num += pow(2, $row-1);
        }
    }
    echo "\r\n";
}
複製代碼

運行結果:

建堆結果:97,76,65,38,49,13,27,50
    97
   76   65
  38  49  13  27

堆排序結果: 13,27,38,49,50,65,76,97
[Finished in 0.2s]
複製代碼

歸併排序

歸併排序是 分治法(Divide and Conquer)的一個很是經典的應用。將大序列拆分紅n個小序列,先使小序列有序,而後合併有序子序列,獲得排序結果。
將兩個有序序列合併成一個序列的方法叫作2路歸併

算法描述與分析:

遞歸方法實現:
1.Divide: 把長度爲n的序列分紅長度爲 n/2的子序列;
2.Conquer: 對每一個子序列採用歸併排序;
3.Combine:將排序好的兩個子序列合併成最終的排序序列。

歸併排序
歸併排序動畫

歸併操做的工做原理以下:
第一步:申請空間,使其大小爲兩個已經排序序列之和,該空間用來存放合併後的序列
第二步:設定兩個指針最初位置分別爲兩個已經排序序列的起始位置
第三步:比較兩個指針所指向的元素,選擇相對小的元素放入到合併空間,並移動指針到下一位置
重複步驟3直到某一指針超出序列尾
將另外一序列剩下的全部元素直接複製到合併序列尾。

複雜度:

時間複雜度:
O(nlogn): 將序列分紅小序列須要logn步,每一步合併都須要對n個元素進行比較,時間複雜度爲O(n),故一共爲 o(nlogn)
空間複雜度:
O(n)

代碼實現:
function mergeSort($arr){    
    $len = count($arr);
    if($len<=1) return $arr;

    $mid = intval($len/2);
    $left = array_slice($arr, 0, $mid);
    $right = array_slice($arr, $mid);

    $a = mergeSort($left);  //拆分排序子序列
    $b = mergeSort($right); //
    $arr = merge($a, $b);   //合併,拆分幾回,就合併幾回

    return $arr;
}
//2路合併
function merge($arrA, $arrB){
    $arrC = [];
    while ( count($arrA)>0 && count($arrB)>0 ) {
        $arrC[] = ($arrA[0] < $arrB[0]) ?  array_shift($arrA) : array_shift($arrB);
    }
    return array_merge($arrC, $arrA, $arrB);

}
$arr = [49, 38, 65, 97, 76, 13, 27, 50];
$arr = mergeSort($arr);
echo implode(',', $arr);
複製代碼

輸出結果:13,27,38,49,50,65,76,97[Finished in 0.2s]

插入排序

簡介:

構建有序序列,對於未排序數據,在已排序序列從後向前掃描,找到合適的位置K並插入。 位置K以後的元素須要所有後移。

算法描述與分析:
  1. 取第一個元素構建有序序列;
  2. 取出下一個元素B,對有序序列從後向前掃描; 若是掃描到的元素大於B,則後移該元素;不然將B插入該元素後面;
  3. 重複第2步,直到遍歷完全部數據。
插入排序
複雜度:

時間:O(n^2)
空間:O(1)

代碼實現:
function insert_sort($arr){
    for( $i =1; $i<count($arr); $i++){
        $t = $arr[$i]; //待排元素
        for($j=$i-1; $j>=0; $j--){
            if( $arr[$j] > $t ){
                $arr[$j+1] = $arr[$j]; //大於待排元素,則後移
            }else{
                break;
            }
        }
        $arr[$j+1] = $t;
    }
    return $arr;
}
複製代碼

二分插入排序

原理同上。 只是在第二步尋找合適位置時,使用二分查找法,快速定位插入位置K;而後將K後的元素後移;在K位置插入待排值

複雜度:

同插入排序。

代碼實現:
function binary_insertion_sort($arr){
    $count = count($arr);
    for( $i=1; $i<$count; $i++){
        $tmp = $arr[$i];
        //查找須要替換的位置
        $left = 0;
        $right = $i-1;
        while( $left <= $right){
            $middle = intval( ($right+$left)/2 ); //向上加1
            if( $arr[$middle] > $tmp){
                $right = $middle-1;
            }else{
                $left = $middle+1;
            }
        }
        //位置後的元素後移
        for( $j; $j>=$left; $j--){
            $arr[$j+1] = $arr[$j];
        }
        $arr[$left] = $tmp;
    }
    echo implode(',', $arr)."\r\n";
}

複製代碼

選擇排序

從待排序列中選擇最小(大)的元素,放在序列的起始位置;而後在從剩下的序列中繼續選擇最小(大)的元素,放在已排序序列的隊尾。

選擇排序
複雜度:

時間:O(n^2)
空間:O(1)

代碼實現:

冒泡排序

兩層循環,外層循環次數=元素個數;
內層循環挨個比較,當 A[j] 大於 A[j+1]時交換兩個值;
當內循環結束時,最多有n-1次交換,會將最大值置於尾部;
當一次外循環無元素交換時,說明序列已有序,可提早結束外層循環。

複雜度

時間: o(n^2); 最優是已排序列進過一層循環發現無元素交換,直接退出程序,複雜度O(n)
空間:O(1)

雞尾酒排序

冒泡排序的變種,不一樣的地方在於從低到高後從高到低。一次層循環內完成一大一小兩個元素的冒泡。

雞尾酒 - 雙向冒泡排序
複雜度

同上

代碼實現
function cocktail_sort($arr){
    $i=1;
    $start =0;
    $len = count($arr)-1;
    $end = 0;
    while($i>0){    //有交換元素時繼續執行
        $i=0;
        for($j=$start; $j<$len-$end; $j++){
            if( $arr[$j] > $arr[$j+1]){
                $t = $arr[$j];
                $arr[$j] = $arr[$j+1];
                $arr[$j+1] = $t;
                $i=1;
            }
        }
        //逆向冒泡來一次
        for($j=$len-$end; $j>$start; $j--){
            if( $arr[$j] < $arr[$j-1]){
                $t = $arr[$j];
                $arr[$j] = $arr[$j-1];
                $arr[$j-1] = $t;
                $i=1;
            }
        }
        $end++;  //經過冒泡,尾部有序序列個數加一
        $start++; //頭部有序序列個數加一
    }
}
複製代碼

桶排序

將數組分別放到有限數量的桶中。每一個桶在進行獨立的排序。 分桶要求桶中的最大數據小於下一個桶的最小數據。

複雜度

時間:
空間: O(N+M)

計數排序

1.找出待排序列的最大值和最小值;使用數組C來標識 C[min] - c[max];
2.統計數組中每一個值出現的次數,存入C[i]中;
3.反向填充目標數組:順序遍歷這個數組C,將下標解釋成數據, 將該位置的值表示該數據的重複數量,獲得排序好的數組。

  1. 當輸入的元素是n 個0到k之間的整數時,它的運行時間是 O(n + k)。計數排序不是比較排序,排序的速度快於任何比較排序算法。
  2. 只能對整數進行排序
複雜度

時間:O(n)
空間:最大O(2n)

基數排序

將整數按位數切割成不一樣的數字,而後按每一個位數分別比較。

位圖排序

要求數據不重複,用bit位進行位圖排序,能節省空間。

譬如: 每一個學生答題一次,將已答題的學號記錄在文件中。學號只出現一次,且學號連續。輸入一個學號,查看是否答題。
解決: 將學號進行排序,在查找時使用二分查找。 由於學號不重複,因此可以使用位圖排序。

複雜度

位圖排序不是比較排序,時間複雜度爲 O(n)

相關閱讀:

相關文章
相關標籤/搜索