ソート ・ O(n log n)

クイックソート

基準の値で左右に分け、それぞれを同じやり方で並べます。

1 歩ずつ動かす

[5, 2, 8, 1, 9, 3, 7, 4] を小さい順に並べる

  1. 5
    0
  2. 2
    1
  3. 8
    2
  4. 1
    3
  5. 9
    4
  6. 3
    5
  7. 7
    6
  8. 4
    7
  • 比較中
  • 入れ替えた
  • 位置が決まった
  • いま扱う範囲
  • 箱の上の字は、基準と、境目の i と、見ている j
  • 箱の下の数字は添字

1 歩目 / 全 39 歩比べた回数 0 回入れ替えた回数 0 回

8 個の値を小さい順に並べます。末尾の値を基準にして、基準以下の側と大きい側に分けていきます。

いま動いている疑似コードの行

  1. 1quick_sort(lo, hi) を次のとおり行う
  2. 2lo >= hi なら何もしない (1 個は並んでいる)
  3. 3基準 = a[hi]、i = lo - 1
  4. 4j を lo から hi - 1 まで 1 つずつ増やす
  5. 5a[j] <= 基準 なら i を 1 つ進め、a[i] と a[j] を入れ替える
  6. 6a[i + 1] と a[hi] を入れ替える (基準の添字 p が決まる)
  7. 7quick_sort(lo, p - 1) と quick_sort(p + 1, hi) を行う

見本を変える

予想してみる

この歩には問題がありません。「1 歩進む」で、次の問題が出るところまで進めましょう。

今回わかること

  • 基準の値で、基準以下の側と大きい側に分ける流れ
  • 平均では O(n log n) なのに、並んだ入力ではいちばん遅くなる理由

前提

  • 配列と添字 (0 から数える番号)
  • 再帰 (関数が自分を呼ぶ書き方)
  • 2 つの値を入れ替える書き方

どんな並べ方か

クイックソートは、基準になる値を 1 つ選び、基準以下の値を左、基準より大きい値を右に分けてから、左右をそれぞれ同じやり方で並べる並べ方です。分け終えた時点で、基準の位置は最終の位置に決まります。

ここでは、範囲の末尾を基準にして、左から順に基準以下の値を集めていく書き方を使います。基準が真ん中あたりの値になれば半分ずつに分かれて速く、端の値ばかりになると 1 個ずつしか減らず遅くなります。

手順と具体例

  1. 1範囲の末尾の値を、基準にする
  2. 2左から順に見て、基準以下の値を左端のほうへ集める
  3. 3最後に基準を、基準以下の側と大きい側の境目へ入れ替える
  4. 4基準の位置が決まる
  5. 5基準の左と右を、それぞれ同じやり方で並べる (1 個になったら終わり)
[5, 2, 8, 1, 9, 3, 7, 4] を並べるとき (比べた 16 回、入れ替えた 7 回)
区切り比べた / 入れ替えた終わったときの並び
基準 4 で分ける (添字 0 から 7)7 回 / 4 回2, 1, 3, 4, 9, 8, 7, 5
基準 3 で分ける (添字 0 から 2)2 回 / 0 回2, 1, 3, 4, 9, 8, 7, 5
基準 1 で分ける (添字 0 から 1)1 回 / 1 回1, 2, 3, 4, 9, 8, 7, 5
基準 5 で分ける (添字 4 から 7)3 回 / 1 回1, 2, 3, 4, 5, 8, 7, 9
基準 9 で分ける (添字 5 から 7)2 回 / 0 回1, 2, 3, 4, 5, 8, 7, 9
基準 7 で分ける (添字 5 から 6)1 回 / 1 回1, 2, 3, 4, 5, 7, 8, 9

比べる回数と計算量

場合比べる回数計算量
いちばん速いおよそ n log₂ n 回 (毎回ほぼ半分に分かれる)O(n log n)
平均およそ 1.39 × n log₂ n 回O(n log n)
いちばん遅いn(n - 1) ÷ 2 回 (基準がいつも端の値)O(n²)

末尾を基準にすると、もう並んでいる 8 個で 28 回比べる、いちばん遅い形になります。ばらばらの見本では 16 回です。基準を真ん中の値やランダムに選ぶと、この遅い形を避けやすくなります。

使いどころ

向いているとき

  • 要素が多く、作業用の列を用意せずに速く並べたいとき
  • 平均の速さを重視し、同じ値の順番は気にしないとき
  • 基準をランダムに選ぶなど、遅い形を避ける工夫ができるとき

向いていないとき

  • 同じ値の順番を崩したくないとき (安定な並べ方ではない)
  • どんな入力でも最悪の時間を抑えたいとき (マージソートを使う)

コード

疑似コード

  1. 1quick_sort(lo, hi) を次のとおり行う
  2. 2lo >= hi なら何もしない (1 個は並んでいる)
  3. 3基準 = a[hi]、i = lo - 1
  4. 4j を lo から hi - 1 まで 1 つずつ増やす
  5. 5a[j] <= 基準 なら i を 1 つ進め、a[i] と a[j] を入れ替える
  6. 6a[i + 1] と a[hi] を入れ替える (基準の添字 p が決まる)
  7. 7quick_sort(lo, p - 1) と quick_sort(p + 1, hi) を行う

実際のコード

python実行可能
出力

▶ 実行ボタンを押すと結果がここに表示されます

Python と JavaScript は、書き換えて「実行」を押すと、その場で結果が出ます。Java は読むだけの欄です。

よくある疑問

もう並んでいる配列が、なぜいちばん遅いのですか?
末尾が最大の値なので、基準以下の側に残り全部が入り、大きい側は空になるからです。範囲が 1 個ずつしか減らず、n 回の分け直しで n(n - 1) ÷ 2 回比べます。
基準は、末尾の値でないといけませんか?
いいえ。真ん中の値や、ランダムに選んだ値でも並べられます。その値を末尾と入れ替えてから同じ手順を使えば、並んだ入力でも遅くなりにくくなります。
クイックソートは、なぜ安定な並べ方ではないのですか?
基準以下の値を左へ集めるときに、離れた値どうしを入れ替えるからです。同じ値の順番が前後することがあります。

関連