LoginSignup
7
6

More than 3 years have passed since last update.

PHPでクイックソートを実装する

Last updated at Posted at 2020-06-07

クイックソートとは

分割統治法(Divide-and-Conquer)に基づいたアルゴリズムです。

基準値(ピボット)より大きいグループと小さいグループに分割し,分割後の要素数が1になるまで再帰的に分割を繰り返します。

コードだけ見たい方はこちら

計算量

計算量とは,実行時間を入力データ量の関数で表したものです。
O(オーダー)記法が用いられます。

クイックソートの平均計算量は O(n logn) で,最悪計算量は O(n^2) です。

なぜそうなるのかは,計算量についてで簡単に解説しています。

処理の流れ

  1. 調べる範囲の両端にポインタ( left と right )をセットし,任意のピボットを決める。
  2. left はピボットより大きい値を指すまで右に進む。right はピボットより小さい値を指すまで左に進む。
  3. 二つのポインタが衝突(※)すれば,そこで分割終了。5へ。
  4. 衝突していなければ,左右のポインタが指している値を交換する。2へ。
  5. 分割後の左右それぞれブロックにおいて,要素が二つ以上あれば再帰的に同じ処理をする。

※ 二つのポインタが衝突するとは
(1) [right | left] のように左右が逆になり交差して止まった場合
(2) left と right が同じ要素を指して止まった場合

(1)は二つのポインタの間が分割の境界線となる。
(2)は指してる要素がピボットの場合である。ピボットの位置はそこで確定するので,再帰処理の対象に含める必要はない。

よって,再帰処理の対象範囲は以下になります。

  • 先頭 ~ left-1
  • right+1 ~ 末尾

PHPでの実装

quickSort.php
/**
 * クイックソート
 * 渡された配列を破壊的に変更します
 *
 * @param int $start 調べる範囲の先頭の位置(インデックス番号)
 * @param int $end 調べる範囲の末尾の位置(インデックス番号)
 * @param array $arr ソートしたい配列 参照渡し
 * @return void
 */
function quickSort(int $start, int $end, array &$arr): void
{
    // ソートする必要がない場合
    if($end <= $start || count($arr) < 2 ){
        return;
    }

    // 任意の値(ここでは真ん中の位置にある値)をピボットに設定
    $pivot = $arr[(int)(($start + $end) / 2)];

    // 両端をそれぞれポインタの初期値として設定
    [$left, $right] = [$start, $end];

    // 2つに分割する
    while (true) {
        // $left の値がピボットより小さければ,ポインタを右へ進める
        while ($arr[$left] < $pivot) {
            $left++;
        }
        // $right の値がピボットより大きければ,ポインタを左へ進める
        while ($pivot < $arr[$right]) {
            $right--;
        }
        // $left と $right が衝突したらループを抜ける
        if ($right <= $left){
            break;
        }
        // $left と $right の値をスワップ
        [$arr[$left], $arr[$right]] = [$arr[$right], $arr[$left]];
        // 交換したら1つ進める
        $left++;
        $right--;
    }

    // 左側に2つ以上要素が存在するなら再帰的にソート
    if ($start < $left-1) {
        quickSort($start, $left-1, $arr);
    }
    // 右側に2つ以上要素が存在するなら再帰的にソート
    if ($right+1 < $end) {
        quickSort($right+1, $end, $arr);
    }

}

ソートする

// ソートする
$a = range(0, 100);
shuffle($a);
quickSort(0, count($a)-1, $a);

※ PHPには標準でsort関数が用意されています。これは,C言語レベルでソートを実装しています。わざわざ自分で実装する必要はありません。


蛇足(PHPのソート関数について)

sort()はクイックソートで実装されているみたいです。

PHP の大半のソート関数と同様、sort() は » Quicksort でそれを実装しています。 ピボットは、既にソート済みの部分に対して時間的に最適なところを選択します。

PHPのソースです。処理の実体は恐らくここ(php-src/Zend/zend_sort.c)にあります。
要素数が5つ以下の場合は,シンプルなソートをしているようですね。


計算量について

直感的に理解できるように簡単に考えます。

クイックソートは,マージソートと同じく分割統治法です。

データの数を n とします。データを2分割していき,最終的に分割後のデータ数が1になればソートが完了します。

ここで,何回分割を繰り返せば良いでしょうか?

例えば n=8 で考えると,8 → 4 → 2 → 1 のように3回の分割が必要です。
8 × (1/2) × (1/2) × (1/2) = 1 という計算をしています。

× (1/2) を何回行うのかを知りたいのです。

これを一般化すると以下の式になります。k は任意の正の整数です。

n (1/2)^k = 1

これを k について解くと

n (1/2)^k = 1\\
(1/2)^k = 1/n\\
2^k = n\\
log_2 2^k = log_2 n\\
k log_2 2 = log_2 n\\
k = log_2 n\\

(n > 0, k > 0)

n個のデータがあるとき,log n 回の分割でソートが完了するということです。

「分割された状態からマージする」と逆から考えることもできます。
(2 × 2 × 2 × 2 ...) と 2を何乗したら n になるか?

log n は「底の2を何乗したらnになるのか」を表しているのでそのままですね。

ここまでは,バイナリサーチ(2分探索)の計算量と同じです。

「k 回分割する」を「k 段の階層ができた」と言い換えます。

各段において,各要素はピボットと一回だけ比較されるので,一段あたりの計算量は O(n) となります。

したがって全体の計算量は

k × O(n)\\
= log_2 n × O(n)\\
⇒ O(n log_2 n)

毎回,範囲中の最小値または最大値をピボットに選んでしまうと k=n-1 となります。

よって,最悪計算量は以下になります。

(n-1) × O(n)\\
⇒ O(n^2)\\

おまけ(JavaScript でも実装してみる)

quickSort.js
/**
 * クイックソート
 * 
 * @param {Number} start 
 * @param {Number} end 
 * @param {Array} arr 
 */
const quickSort = (start, end, arr) => {

    const pivot = arr[Math.floor((start + end) / 2)];

    let [left, right] = [start, end];

    while (true) {
        while (arr[left] < pivot) {
            left++;
        }
        while (pivot < arr[right]) {
            right--;
        }
        if (right <= left){
            break;
        }
        [arr[left], arr[right]] = [arr[right], arr[left]];
        left++;
        right--;
    }

    if (start < left-1) {
        quickSort(start, left-1, arr);
    }
    if (right+1 < end) {
        quickSort(right+1, end, arr);
    }

};

ソートする

// ソートする
const a = [3, 7, 1, 5, 6, 2, 4];
quickSort(0, a.length-1, a);

間違っている部分などあれば、ご指摘ください。

7
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
6