快速排序算法圖解與PHP實現講解

概述

快速排序(QuickSort)最初由東尼·霍爾提出,是一種平均時間複雜度爲O(N*lgN),最差時間複雜度爲O(N2)的排序算法。這種排序法使用的策略是基於分治法,其排序步驟如wiki百科-快速排序所述:php

步驟爲:

1.從數列中挑出一個元素,稱爲"基準"(pivot)
2.從新排序數列,全部比基準值小的元素擺放在基準前面,全部比基準值大的元素擺在基準後面(相同的數能夠到任何一邊)。在這個分區結束以後,該基準就處於數列的中間位置。這個稱爲分區(partition)操做。
3.遞歸地(recursively)把小於基準值元素的子數列和大於基準值元素的子數列排序。算法

遞歸到最底部時,數列的大小是零或一,也就是已經排序好了。這個算法必定會結束,由於在每次的迭代(iteration)中,它至少會把一個元素擺到它最後的位置去。數組

用一張圖簡單地表現以上步驟(注:圖中v就是基準元素)。dom

快速排序法原理簡述

下面,我將談談實現這種算法的一種簡單的方式。性能

算法實現圖解

1. 算法步驟、變量和指針

快速排序算法-指針詳解

  1. 選定序列最左端的元素v爲基準元素,指針i指向當前待比較的元素e指針j老是指向<v區域的最右端,l爲序列的最左端,r爲序列的最右端。
  2. 若是e ≥ v,就將e擺放在深黃色的>v區域;若是e < v,就將v擺放在淺黃色的<v區域。「擺放」操做的詳細方法會在下面解說。
  3. 完成一次比較以後,指針i會向右移動一位,繼續比較下一個元素與基準元素的大小。

2. 「擺放」操做與指針移動

情形一:e ≥ v

單路快速排序流程 if (e ≥ v)

  • 元素e的位置不改變,天然併入≥v的區域。
  • 指針i向右移動一位,指向下一個待比較元素e。
  • 指針j不須要移動。

情形二:e < v

單路快速排序流程 if (e &lt; v)

  • 交換元素e與≥v區域的最左端的元素,即swap(i, j+1)。<v區域擴充到j+1,≥v區域整體不變(可是首元素與末元素調換了位置)。
  • 指針i向右移動一位,指向下一個待比較元素e。
  • 指針j向右移動一位,指向當前<v區域的最右端。

情形三:單輪排序結束

單路快速排序流程 END

此時,如圖中的第一個序列,v在最左端,而後是<v的區域和≥v的區域,指針j指向<v區域的最右端,指針i指向序列的最左端。
交換基準元素v與指針i所指元素,即swap(l, j),將整個序列分割爲<v和≥v兩個區域,如圖中的第二個序列。
接下來,再分別對<v和≥v兩個序列進行下一輪排序,以此類推,直至後代序列只剩下一個元素,整個序列的排序完畢了。測試

PHP實現的例子

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;
    }
}

QuickSort 類的結構

  • sort()方法是供外部調用快速排序算法的入口。
  • partition()方法對序列分區排序,對應步驟二。
  • sortRecursion()方法遞歸地調用排序方法,對應步驟三。
  • swap()方法用於交換序列中的兩個元素。

測試算法效率與複雜度

徹底隨機序列排序結果

如下面的方法分別生成元素個數爲1萬、10萬的徹底隨機數組,並用快速排序算法對其排序。優化

// 生成指定元素個數的隨機數組
public static function generateRandomArray($n) {
    $list = [];
    for ($i=0; $i<$n; $i++) {
        $list[$i] = rand();
    }
    return $list;
}

在個人計算機運行程序,ui

  • 當數組元素個數爲1萬時,排序時間爲21.929025650024 ms
  • 當數組元素個數爲10萬時,排序時間爲286.66996955872 ms

元素個數變成原來的10倍,運行時間不到原來的14倍,可見算法的複雜度是O(N*lgN)級別的。
可是,當待排序的數組是近似順序排序的數組時,這個算法就會退化爲O(N2)算法。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萬的近似順序排序數組,測試結果:指針

  • 1萬:444.75889205933 ms
  • 10萬:52281.121969223 ms

由此結果可知:

  • 近似順序序列的排序時間遠遠大於徹底隨機序列。
  • 1萬與10萬的運行時間相差117倍。固然,因爲計算機性能不穩定,程序每次的運行結果都是不一樣的,可是1萬和10萬的差距必定是在100這個數量級左右的數字,也就是算法複雜度爲O(N2)級別。

快速排序算法退化

當待排序的序列是近似順序排序時,由於算法選取的基準點是最左端的點(很大機率是最小的值),因此分區的結果是左邊的<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萬的近似順序排序數組,排序時間:

  • 1萬:21.579027175903 ms
  • 10萬:274.99508857727 ms

可見,排序的時間複雜度又變回O(N*lgN)級別了。

總結

  • 理解算法實現實現過程的關鍵:分區的方法,以及指針i和j是如何移動的。
  • 近似順序序列致使算法從O(N*lgN)級別退化到O(N2)級別,隨機挑選基準點是解決方法。
  • 這個算法還存在其餘的問題,爲了解決這些問題,衍生了諸如雙路排序和三路排序的快速排序算法,有空再寫寫單路排序算法的其餘問題,並介紹那兩種改進的算法。
相關文章
相關標籤/搜索