クイックソートとマージソート

回 2-3 道 2 データ構造とアルゴリズム

クイックソートは、基準 (ピボット) の値を 1 つ選び、それより小さい値を左、大きい値を右に分け、左右でも同じことをくり返して並べる方法です。平均では比べる回数が n log n の形で済みますが、基準がいつも一番小さい値か一番大きい値になると n(n − 1) / 2 回に落ち、400 件の並んだデータでは 79800 回比べます。マージソートは半分に割って混ぜるので、並び方によらず n log n の形です。

半分に分けて並べる 2 つの速いソートを、比べる回数、遅くなる並び、余分に使う場所で見分けられるようになります。

並べ替えを速くするには、何を変えればよいか

速く並べるには、データを半分に分け、分けた片方ずつを並べてからまとめます。半分に分ける回数は、データが 2 倍になっても 1 回増えるだけだからです。

前の回 2-2 の遅いソートは、データが 2 倍になると比べる回数が約 4 倍になりました。

この回の 2 つのソートは、どちらも「分けて解く」やり方です。マージソートは、まず半分に割り、並んだ山どうしを混ぜて 1 本にします。クイックソートは、まず基準の値で左右に分け、分けた側をまた分けます。

クイックソートは平均では速いのですが、ある並びのデータを渡すと、遅いソートと同じ回数まで落ちます。この回の終わりには、その並びを自分で作って見せられるようになります。

基準の 6 より小さい値を左、大きい値を右に分け、並んだ列で最小を基準にすると 1 冊しか減らない図

クイックソートとマージソートはどう動くのか

クイックソートは先に基準で分けてあとは何もせず、マージソートは先に割ってあとで混ぜます。手間をかける場所が逆です。

マージソートは割って混ぜる

マージソートは、データを 1 個になるまで半分に割り続けます。1 個の山は、それだけで並んでいます。次に、並んだ 2 つの山を 1 本に混ぜます。

混ぜるときは、2 つの山の先頭どうしだけを比べ、小さい方を取り出します。どちらの山も並んでいるので、先頭より小さい値は山の中にありません。片方の山が空になったら、残りをそのまま後ろに付けます。

割る段の数は、データが 2 倍になっても 1 段増えるだけです。1 段ごとに全部の値を 1 回ずつ取り出すので、比べる回数は n log n の形になります。データの並び方によらず、この形は変わりません。

2 本を 1 本にするところ

クイックソートは基準で左右に分ける

クイックソートは、データから基準 (ピボット) の値を 1 つ選び、残りの値を、基準より小さい物と、基準以上の物に分けます。基準は、その 2 つの間に置けば位置が決まります。左右の山でも同じことをくり返し、山が 1 個以下になったら止めます。

基準と残りの値を 1 回ずつ比べるので、1 回分けるたびに、山の個数より 1 少ない回数を比べます。この頁では、この「基準と比べた回数」を数えます。

同じ値が無く、基準が山のちょうど真ん中の値なら、左右は半分ずつに分かれ、マージソートと同じく n log n の形になります。基準がいつも一番小さい値なら、左は空で、右に残り全部が行きます。1 回分けても 1 個しか減らず、遅いソートと同じ n(n − 1) / 2 回になります。

クイックソートと pivot

ここまでの説明は、分かりやすさのために左右の山を新しいリストに分けて書きました。実際のクイックソートは、配列の中で値を入れ替えながら分けるので、新しいリストを作りません。次のコードは、その書き方の 1 つ (ロムートの分け方) です。末尾の値を基準にし、基準より小さい値を見つけるたびに、左から詰めて置きます。最後に基準を、小さい値の並びのすぐ右へ入れ替えます。1 行が 1 回の分け方で、分けたあとの配列全体と、位置が決まった基準を出します。

動かす 配列の中で入れ替えて分ける

python実行可能
出力

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

実行すると、次の通りに出ます

[2, 3, 1, 5, 9, 8, 6] 基準 5
[1, 3, 2, 5, 9, 8, 6] 基準 1
[1, 2, 3, 5, 9, 8, 6] 基準 2
[1, 2, 3, 5, 6, 8, 9] 基準 6
[1, 2, 3, 5, 6, 8, 9] 基準 9
入れ替えた回数 7

1 行目で、基準の 5 より小さい 2、3、1 が左に詰まり、5 が 4 番目に入りました。そのあとは 5 の左と右で、それぞれ同じことをくり返します。配列は最初の 1 本だけで、増えたのは位置を指す数だけです。同じ位置どうしの入れ替えは数えず、7 回の入れ替えで並び終わりました。前の回 2-2 のバブルソートが隣どうしを入れ替えたのと違い、離れた値を 1 回で動かせます。

2 つの違いを表で比べる

見る所マージクイック
平均の計算量O(n log n)O(n log n)
最悪の計算量O(n log n)O(n²)
余分に使う場所n 個分平均 O(log n)、最悪 O(n)
同じ値の順守る崩す

計算量は、データが増えたときの手間の増え方です。クイックソートは、配列の中で値を入れ替えながら分けられるので、余分に使うのは呼び出しを積む場所だけです。積む段の数は分ける深さと同じで、平均は O(log n)、並んだデータのような最悪では O(n) になります (下の節で 400 件が 400 段になります)。マージソートは、混ぜた結果を置く場所として n 個分を使います。V8 の公式ブログは、以前の JavaScript のクイックソートについて、その場で並べられる利点と、安定でない (同じ値の順を守らない) こと、最悪で O(n²) に落ちることを挙げています。この頁の入れ替えのコードのほかは、読みやすさのために新しいリストを作って分けています。

動かして確かめる

1 本目は、図と同じ [6, 2, 8, 3, 9, 1, 5] を、先頭の値を基準にして分けます。1 行が 1 回の分け方で、左から、基準より小さい山、基準、基準以上の山です。行の頭の空白は、何段目の分け方かを表します。

動かす 基準で左右に分ける様子

python実行可能
出力

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

実行すると、次の通りに出ます

 [2, 3, 1, 5] 6 [8, 9]
  [1] 2 [3, 5]
   [] 3 [5]
  [] 8 [9]
[1, 2, 3, 5, 6, 8, 9]

最初は 6 が基準で、[2, 3, 1, 5] と [8, 9] に分かれます。次の段では、左の山を 2 で、右の山を 8 で分けます。山が 1 個以下になると止まり、最後に並んだ [1, 2, 3, 5, 6, 8, 9] が返ります。

2 本目は、1,000 件、2,000 件、4,000 件のばらばらの数を、マージソートとクイックソート (先頭を基準) で並べ、比べた回数を数えます。出力は左から、件数、マージソートの回数、クイックソートの回数、目安の n × log2 n です。並びは、乱数の代わりに決まった式で混ぜているので、誰が動かしても同じになります。最後の行は、2 つの結果が Python の sorted と同じかどうかです。

動かす マージとクイックの回数を数える

python実行可能
出力

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

実行すると、次の通りに出ます

件数 マージ クイック 目安
1000 8723 11336 9966
2000 19416 27556 21932
4000 42860 56109 47863
正しく並んだ True

1,000 件では、マージソートが 8723 回、クイックソートが 11336 回でした。どちらも目安の 9966 に近く、件数が 2 倍になると、回数は 2.0 倍から 2.4 倍ほどです。前の回の遅いソートが 2 倍で約 4 倍だったのと比べると、増え方がずっと緩やかです。

このデータでは、3 つの件数のどれでも、クイックソートの方がマージソートより多く比べています。マージソートはいつもちょうど半分に割りますが、クイックソートは基準が真ん中から外れるたびに、分け方が偏るからです。

クイックソートはどこで遅くなるのか

クイックソートは、基準がいつも山の一番端の値になると、1 回分けても 1 個しか減らず、遅いソートと同じ n(n − 1) / 2 回まで落ちます。

並んだデータを渡すと最悪になる

先頭を基準にするクイックソートに、もともと小さい順に並んだデータを渡します。先頭はいつも山の中で一番小さい値なので、左の山はいつも空です。

次のコードは、同じ件数で、混ぜたデータ (i × 37 を件数で割った余りで並べ替えた数) と、並んだデータを比べます。出力は、件数、データの種類、基準と比べた回数、分ける段の深さです。

動かす 並んだデータで遅くなる

python実行可能
出力

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

実行すると、次の通りに出ます

件数 比べた回数 深さ
100 混ぜた 616 11
100 並んだ 4950 100
200 混ぜた 1708 16
200 並んだ 19900 200
400 混ぜた 4320 20
400 並んだ 79800 400

400 件で、混ぜたデータは 4320 回、深さ 20 段で済みました。並んだデータは 79800 回で、これは 400 × 399 / 2 そのものです。深さも 400 段で、1 段に 1 個ずつしか減っていません。件数が 2 倍になるたびに、並んだデータの回数は 4950、19900、79800 と約 4 倍ずつ増え、遅いソートと同じ形です。

深さが件数と同じになるのも危険です。分ける処理を関数の呼び出しで書いていると、呼び出しが件数の分だけ積み重なります。Python では、呼び出しが深くなりすぎると RecursionError で止まります。基準の選び方を変えるか、分けたあと小さい方の山だけを呼び出しで片付け、大きい方の山はループで続ける書き方にすると、深さを抑えられます。

基準の選び方と、分け方で直す

基準の選び方を変えると、並んだデータでも速くなります。ただし、全部が同じ値のデータでは、選び方だけでは直りません。

次のコードは 400 件で、基準を先頭 (first)、真ん中の位置 (middle)、先頭と真ん中と末尾の 3 つの中央値 (med3) にした場合を比べます。最後の 3分割 は、基準と同じ値を別の山に集め、小さい、同じ、大きいの 3 つに分ける書き方です。出力は、基準と比べた回数と深さです。

動かす 基準の選び方で直す

python実行可能
出力

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

実行すると、次の通りに出ます

並んだ
  first 79800 400
  middle 2698 9
  med3 2698 9
  3分割 2698 9
逆順
  first 79800 400
  middle 2698 9
  med3 2698 9
  3分割 2698 9
全部同じ
  first 79800 400
  middle 79800 400
  med3 79800 400
  3分割 399 2

並んだデータと逆順のデータでは、先頭を基準にすると 79800 回、深さ 400 段ですが、真ん中か 3 つの中央値にすると 2698 回、深さ 9 段に減ります。

全部が同じ値 7 のデータでは、どの基準でも 79800 回、深さ 400 段です。基準と同じ値がすべて「基準以上」の右の山に行くので、1 回分けても 1 個しか減りません。同じ値を別の山に集める 3分割 にすると、399 回、深さ 2 段で終わります。

真ん中や 3 つの中央値を基準にしても、その選び方を狙って作ったデータを渡されると、また偏ることがあります。選び方が決まっていれば、それを逆手に取る並びも作れるからです。

そこで、基準を乱数で選ぶ書き方があります。どの位置が基準になるかを前もって知る方法が無いので、狙って作った並びでも、毎回端の値ばかりを引くことはまず起きません。次のコードは、乱数の種 (乱数の出方を決める最初の値) を 1 に決めて、誰が動かしても同じ出力になるようにしています。仕事で使うときは種を決めません。

動かす 乱数で基準を選ぶ

python実行可能
出力

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

実行すると、次の通りに出ます

並んだ 3690 18
逆順 4438 24
全部同じ 79800 400

並んだデータで 3690 回、深さ 18 段、逆順で 4438 回、深さ 24 段になり、先頭を基準にした 79800 回から大きく減りました。それでも全部が同じ値のデータは 79800 回のままです。どれを基準に選んでも同じ値なので、乱数でも偏りは直らず、3分割 が要ります。実務の道具は、次の節の通り、最悪でも n log n の形で終わることを、決まりや作りで守っています。

クイックソートは仕事でどう確かめるのか

仕事では、自分でクイックソートを書かず、言語のソートを使い、そのソートの最悪の計算量と安定かどうかを公式文書で確かめます。

  1. Python の sorted と sort は、公式文書によると安定なソートで、Timsort というマージソートの仲間を使っている。クイックソートではない
  2. C++ の std::sort は、C++11 からの決まりで、最悪でも O(n log n) 回の比べで並べることが求められている。同じ値の順を守りたいときは std::stable_sort を使う
  3. 自分で書いた分けて解く処理は、並んだ、逆順、全部同じ、の 3 つのデータを必ず試験に入れる。上のコードの通り、この 3 つが最悪を呼びやすい
  4. 件数を 2 倍にして、回数か時間が約 4 倍になったら、基準の偏りを疑う

最悪を呼ぶ並びを試験に入れる

def test_bad_inputs():
    n = 400
    up = list(range(n))
    for a in [up, up[::-1],
              [7] * n]:
        got = my_sort(a)
        assert got == sorted(a)

関わる実例

C++ の決まりが、最悪で 2 乗のソートを許さなくした (2007年8月30日に提起)

C++ の標準ライブラリの不具合の一覧の 713 番は、2007年8月30日に出された問題です。それまでの決まりでは、sort() の手間は「平均でおよそ N log N 回の比べ」とだけ書かれ、最悪の場合の決まりがありませんでした。もともとは、3 つの中央値を基準にするクイックソートを使えるようにするためでした。この書き方は、通常は O(N log N) でも、意地の悪い入力では 2 乗になります。

一覧の 713 番は、最悪で 2 乗になるソートを許す理由はもう無いとし、決まりを「O(N log N) 回の比べ」に改めることを提案しました。クイックソートを使いたい作り手は、1997年に発表されたイントロソート (クイックソートを土台にした並べ方) を使えば、平均の速さを落とさずに最悪でも O(N log N) にできる、と書いています。cppreference は、C++98 では平均だけだった決まりが、最悪の場合の決まりに変わったと記録しています。

崩れた前提は「平均で速ければ十分」です。平均は速くても、最悪の並びが渡されたときの手間を決まりで縛っていませんでした。

出典は LWG Issue 713, sort() complexity is too lax (C++ 標準化委員会の不具合の一覧) と C++ の sort の頁 (cppreference) です。

JavaScript の V8 がクイックソートをやめた (2018年9月28日)

2018年9月28日、Chrome の JavaScript の実行の仕組み V8 は、公式ブログで Array.prototype.sort の作り替えを報告しました。それまでの V8 は、3 つの値の中央値を基準にするクイックソートで、10 個より短い配列は挿入ソートで並べていました。

ブログは、この作りの欠点として、安定でないことと、最悪の場合に O(n²) に落ちる見込みがあることを挙げています。安定なソートを求める要望は、V8 の不具合の記録の中でも最も古いものの 1 つでした。V8 は Python のために作られた Timsort (マージソートの仲間) に切り替え、v7.0 と Chrome 70 から使えるようにしました。ブログによると、Timsort は最悪でも O(n log n) で、すでに並んだ列は O(n) で並べます。

崩れた前提は「平均で速いクイックソートが、どの場面でも一番よい」です。同じ値の順を守ることと、最悪の手間が読めることを、使う人が求めていました。

出典は Getting things sorted in V8 (V8 公式ブログ) です。

よくある問い

クイックソートの計算量はいくつですか

平均は O(n log n) で、最悪は O(n²) です。上のコードでは、400 件の混ぜたデータが 4320 回、並んだデータは 79800 回でした。基準がいつも山の端の値になると最悪になります。

クイックソートはなぜ速いと言われるのですか

基準で分けるたびに、左右の山の間で値を比べる必要が無くなり、平均では半分ずつに近く分かれて n log n の形になるからです。さらに、配列の中で入れ替えながら分けられるので、マージソートのように n 個分の場所を余分に用意しなくて済みます。ただし、最悪の並びでは遅いソートと同じ回数に落ちます。

クイックソートは安定ですか

ふつうの書き方では安定ではありません。配列の中で離れた値を入れ替えながら分けるので、同じ値どうしの順が崩れることがあります。V8 の公式ブログも、以前のクイックソートが安定でないことを、作り替えの理由に挙げています。

一番速いソートはどれですか

どの場面でも一番速いソートはありません。データの数、並び方、同じ値の順を守るか、余分な場所をどれだけ使えるかで変わります。この頁の 1,000 件の混ぜたデータでは、マージソートが 8723 回、クイックソートが 11336 回でしたが、並んだデータでは先頭を基準にしたクイックソートが 79800 回まで落ちました。実務では自分で選ばず、言語のソートを使うのが基本で、Python と v7.0 からの V8 は、マージソートの仲間の Timsort を使っています。

クイックソートは誰が考えましたか

C. A. R. Hoare (トニー・ホーア) です。Hoare は 1962年に The Computer Journal の 5 巻 1 号に論文 Quicksort を出し、計算機の記憶の中で並べる新しい方法として、速さ、記憶の節約、書きやすさの 3 つでほかの方法より優れていると書いています。

確認クイズ

答えだと思う選択肢を押すと、その場で正誤と理由が開きます。4 問すべてに正しく答えると、この回は修了で、石が緑に変わります (この端末のブラウザに残ります)。間違えた問いは、あとから解き直せます。

1 問目

先頭の値を基準にするクイックソートで、比べる回数が一番多くなるのは、どのデータですか。

2 問目

全部が同じ値のデータ 400 件を、真ん中の位置を基準にして、基準より小さい物と基準以上の物の 2 つに分けました。どうなりますか。

3 問目

並び方によらず、比べる回数がいつも n log n の形で済むのはどれですか。

4 問目

このコードを動かすと何が出ますか。cnt は、先頭を基準にしたクイックソートで、基準と比べた回数を数えます。

比べた回数を数える

def cnt(a):
    # 先頭を基準に比べた回数
    if len(a) <= 1:
        return 0
    pv = a[0]
    L = [v for v in a[1:]
         if v < pv]
    R = [v for v in a[1:]
         if v >= pv]
    return (len(a) - 1
            + cnt(L) + cnt(R))


print(cnt([1, 2, 3, 4, 5]))

説明課題

先頭の値を基準にするクイックソートが遅くなる並びを 1 つ作り、なぜ遅くなるのかと、比べる回数がいくつになるかを、3 文以内で説明してください。上の「並んだデータで遅くなる」のコードで自分の並びを試し、回数を確かめてください。書いたら、下の模範解答を開いて比べてください。

模範解答

自分の説明を書いてから開いて、比べてください。

この回の先の一歩

並べ替えの遅さは、件数が増えた画面や集計で突然表に出ます。言語のソートの最悪と安定を公式文書で確かめる癖は、どの現場でも効きます。いまの自分が転職までの道のどの段にいるかは、転職診断で確かめられます。

もっと深く

この回の続きを、ゆめさくのコースの章や道具で深められます。頁の中の確認クイズに答えたあと、手を動かす量を増やしたいときに使ってください。

一次資料

どれも 2026年10月26日に開いて、題と中身を確かめました。

次へ

道 2 データ構造とアルゴリズムの回

  1. 2-0アルゴリズムとは順次、分岐、反復の 3 つで書く手順頁で読む
  2. 2-1線形探索と二分探索100 万件でも 20 回で見つかる頁で読む
  3. 2-2バブルソートと挿入・選択ソート遅いソート 3 つ、2 倍で約 4 倍頁で読む
  4. 2-3クイックソートとマージソート速いソート 2 つ、平均と最悪この回
  5. 2-4スタックとキュー積む順と並ぶ順頁で読む
  6. 2-5再帰自分を呼んで小さくする頁で読む
  7. 2-6木と二分探索木たどる順、片寄り頁で読む
  8. 2-7ハッシュテーブル鍵から一気に引く、衝突頁で読む
  9. 2-8ダイクストラ法と最短経路点と線、幅優先、重みのある道頁で読む
  10. 2-9動的計画法同じ計算を 2 度しない頁で読む
  11. 2-10試験と面接で確かめる基本情報 科目B、道場 10 問、面接 20 問頁で読む

道 2 の頁へ戻るほかの道を見る