ソート ・ O(n log n)

マージソート

半分ずつに分け、並んだ 2 つの列を先頭から比べて 1 つに合わせます。

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

作業用の列 (合わせる前の左右)

  • 比較中
  • 書いた
  • 空き
  • 書き終えた (作業用の列)
  • いま扱う範囲
  • 箱の上の字は、分ける真ん中 (mid)
  • 箱の下の数字は添字

1 歩目 / 全 57 歩比べた回数 0 回書き込んだ回数 0 回

8 個の値を小さい順に並べます。半分ずつに分け、並んだ 2 つの列を 1 つに合わせていきます。

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

  1. 1merge_sort(lo, hi) を次のとおり行う
  2. 2lo >= hi なら何もしない (1 個は並んでいる)
  3. 3mid = (lo + hi) ÷ 2 (小数は切り捨て)
  4. 4merge_sort(lo, mid) と merge_sort(mid + 1, hi) を行う
  5. 5左右の先頭を比べ、小さいほうを配列へ書く (同じなら左)
  6. 6片方が空になったら、もう片方の残りをそのまま書く

見本を変える

予想してみる

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

今回わかること

  • 半分に分け、並んだ 2 つの列を 1 つに合わせる流れ
  • 入力の並びによらず、計算量が O(n log n) になる理由

前提

  • 配列と添字 (0 から数える番号)
  • 再帰 (関数が自分を呼ぶ書き方)
  • 配列を一時的に別の場所へ写す考え方

どんな並べ方か

マージソートは、配列を半分に分け、それぞれを並べてから、並んだ 2 つの列を 1 つに合わせる並べ方です。分けた先も同じやり方で並べるので、再帰で書きます。

並んだ 2 つの列を合わせるときは、左右の先頭どうしを比べて、小さいほうを書いていくだけです。分ける深さが log n 段で、各段で n 個を書くので、入力によらず n log n に収まります。

手順と具体例

  1. 1範囲を、真ん中で左右の 2 つに分ける
  2. 2左と右を、それぞれ同じやり方で並べる (1 個になったら並んでいる)
  3. 3左右を作業用の列に写し、配列の範囲を空ける
  4. 4左右の先頭を比べ、小さいほうを配列へ書く (同じなら左)
  5. 5片方が空になったら、もう片方の残りをそのまま書く
[5, 2, 8, 1, 9, 3, 7, 4] を並べるとき (比べた 17 回、書き込んだ 24 回)
区切り比べた / 書き込んだ終わったときの並び
添字 0 から 1 を合わせる1 回 / 2 回2, 5, 8, 1, 9, 3, 7, 4
添字 2 から 3 を合わせる1 回 / 2 回2, 5, 1, 8, 9, 3, 7, 4
添字 0 から 3 を合わせる3 回 / 4 回1, 2, 5, 8, 9, 3, 7, 4
添字 4 から 5 を合わせる1 回 / 2 回1, 2, 5, 8, 3, 9, 7, 4
添字 6 から 7 を合わせる1 回 / 2 回1, 2, 5, 8, 3, 9, 4, 7
添字 4 から 7 を合わせる3 回 / 4 回1, 2, 5, 8, 3, 4, 7, 9
添字 0 から 7 を合わせる7 回 / 8 回1, 2, 3, 4, 5, 7, 8, 9

比べる回数と計算量

場合比べる回数計算量
いちばん速いおよそ n log₂ n ÷ 2 回 (もう並んでいる)O(n log n)
平均およそ n log₂ n - 1.25n 回O(n log n)
いちばん遅いn log₂ n - n + 1 回O(n log n)

8 個なら、どんな並びでも書き込みは 24 回 (8 × 3 段) です。比べる回数は、もう並んでいれば 12 回、いちばん多くても 17 回で済みます。

使いどころ

向いているとき

  • 要素が多く、どんな並びでも安定して速く並べたいとき
  • 同じ値の順番を崩したくないとき (安定な並べ方)
  • 連結リストや、ファイルのように後ろから読むしかないデータを並べるとき

向いていないとき

  • 要素が少なく、作業用の列を用意する手間のほうが大きいとき
  • メモリに余裕がなく、配列と同じ大きさの作業用の列を持てないとき

コード

疑似コード

  1. 1merge_sort(lo, hi) を次のとおり行う
  2. 2lo >= hi なら何もしない (1 個は並んでいる)
  3. 3mid = (lo + hi) ÷ 2 (小数は切り捨て)
  4. 4merge_sort(lo, mid) と merge_sort(mid + 1, hi) を行う
  5. 5左右の先頭を比べ、小さいほうを配列へ書く (同じなら左)
  6. 6片方が空になったら、もう片方の残りをそのまま書く

実際のコード

python実行可能
出力

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

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

よくある疑問

buf[left] <= buf[right] を < にすると、何が変わりますか?
同じ値のとき右を先に書くので、同じ値どうしの順番が入れ替わります。<= にして左を先に書くから、マージソートは安定な並べ方になります。
作業用の列は、なぜ要るのですか?
配列へ書きながら左右の先頭を読むと、まだ読んでいない値を上書きしてしまうからです。先に範囲を写しておけば、安心して配列へ書き戻せます。
クイックソートと比べて、どちらが速いですか?
入力によらず n log n に収まるのがマージソートの強みです。クイックソートは平均では速く、作業用の列も要りませんが、基準の選び方が悪いと n² まで遅くなります。

関連