快速排序(QuickSort)最初由東尼·霍爾提出,是一種平均時間複雜度爲,最差時間複雜度爲的排序算法。這種排序法使用的策略是基於分治法,其排序步驟如wiki百科-快速排序所述:php
步驟爲:1.從數列中挑出一個元素,稱爲"基準"(pivot),
2.從新排序數列,全部比基準值小的元素擺放在基準前面,全部比基準值大的元素擺在基準後面(相同的數能夠到任何一邊)。在這個分區結束以後,該基準就處於數列的中間位置。這個稱爲分區(partition)操做。
3.遞歸地(recursively)把小於基準值元素的子數列和大於基準值元素的子數列排序。算法遞歸到最底部時,數列的大小是零或一,也就是已經排序好了。這個算法必定會結束,由於在每次的迭代(iteration)中,它至少會把一個元素擺到它最後的位置去。數組
用一張圖簡單地表現以上步驟(注:圖中v就是基準元素)。dom
下面,我將談談實現這種算法的一種簡單的方式。性能
swap(i, j+1)
。<v區域擴充到j+1,≥v區域整體不變(可是首元素與末元素調換了位置)。
此時,如圖中的第一個序列,v在最左端,而後是<v的區域和≥v的區域,指針j指向<v區域的最右端,指針i指向序列的最左端。
交換基準元素v與指針i所指元素,即swap(l, j)
,將整個序列分割爲<v和≥v兩個區域,如圖中的第二個序列。
接下來,再分別對<v和≥v兩個序列進行下一輪排序,以此類推,直至後代序列只剩下一個元素,整個序列的排序完畢了。測試
class QuickSort { /** * 外部調用快速排序的方法 * * @param $arr array 整個序列 */ public static function sort(&$arr) { $length = count($arr); self::sortRecursion($arr,0,$length-1); } /** * 遞歸地對序列分區排序 * * @param $arr array 整個序列 * @param $l int 待排序的序列左端 * @param $r int 待排序的序列右端 */ private static function sortRecursion(&$arr,$l,$r) { if ($l >= $r) { return; } $p = self::partition($arr,$l,$r); //對基準點左右區域遞歸調用排序算法 self::sortRecursion($arr,$l,$p-1); self::sortRecursion($arr,$p+1,$r); } /** * 分區操做 * * @param $arr array 整個序列 * @param $l int 待排序的序列左端 * @param $r int 待排序的序列右端 * @return mixed 基準點 */ private static function partition(&$arr,$l,$r) { $v = $arr[$l]; $j = $l; for ($i=$l+1; $i<=$r; $i++) { if ($arr[$i] < $v) { $j++; self::swap($arr,$i,$j); } } self::swap($arr,$l,$j); return $j; } /** * 交換數組的兩個元素 * * @param $arr array * @param $i int * @param $j int */ private static function swap(&$arr,$i, $j) { $tmp = $arr[$i]; $arr[$i] = $arr[$j]; $arr[$j] = $tmp; } }
sort()
方法是供外部調用快速排序算法的入口。partition()
方法對序列分區排序,對應步驟二。sortRecursion()
方法遞歸地調用排序方法,對應步驟三。swap()
方法用於交換序列中的兩個元素。如下面的方法分別生成元素個數爲1萬、10萬的徹底隨機數組,並用快速排序算法對其排序。優化
// 生成指定元素個數的隨機數組 public static function generateRandomArray($n) { $list = []; for ($i=0; $i<$n; $i++) { $list[$i] = rand(); } return $list; }
在個人計算機運行程序,ui
元素個數變成原來的10倍,運行時間不到原來的14倍,可見算法的複雜度是級別的。
可是,當待排序的數組是近似順序排序的數組時,這個算法就會退化爲算法。spa
/** * 生成近似順序排序的數組 * * @param $n int 元素個數 * @param $swapTimes int 交換次數 * @return array 生成的數組 */ public static function generateNearlyOrderedIntArray($n,$swapTimes) { $arr = []; for ($i=0; $i<$n; $i++) { $arr[] = $i; } //交換數組中的任意兩個元素 for ($i=0; $i<$swapTimes; $i++) { $indexA = rand() % $n; $indexB = rand() % $n; $tmp = $arr[$indexA]; $arr[$indexA] = $arr[$indexB]; $arr[$indexB] = $tmp; } return $arr; }
使用上面的方法生成元素個數爲1萬和10萬的近似順序排序數組,測試結果:指針
由此結果可知:
當待排序的序列是近似順序排序時,由於算法選取的基準點是最左端的點(很大機率是最小的值),因此分區的結果是左邊的<v區域很短或者沒有,右邊的≥v區域很長,總的迭代次數接近序列的長度n,若是序列的長度變爲原來的10倍,那麼迭代的次數也變爲原來的10倍,而每輪排序的時間也是原來的10倍,因此總的排序時間是原來的100倍。
針對順序排序致使的算法時間複雜度上升的問題,一個頗有效的辦法就是改進基準點的選取方法。若是基準點是隨機選取的,就能夠消除這個問題了。
private static function partition(&$arr,$l,$r) { //優化1:從數組中隨機選擇一個數與最左端的數交換,達到隨機挑選的效果 //這個優化使得快速排序在應對近似有序數組排序時,迭代次數更少,排序算法效率更高 self::swap($arr,$l,rand($l+1,$r)); $v = $arr[$l]; $j = $l; for ($i=$l+1; $i<=$r; $i++) { if ($arr[$i] < $v) { $j++; self::swap($arr,$i,$j); } } self::swap($arr,$l,$j); return $j; }
依然是1萬和10萬的近似順序排序數組,排序時間:
可見,排序的時間複雜度又變回級別了。