- 物理機リージョン/AZ
- ハイパーバイザVM exit で捕まえる
- VMゲストOS
- コンテナnamespace / cgroup
- プロセスアプリ / DB今ここ
第11期では、ハッシュ表の平均 O(1) が「鍵がよく散らばる」という入力への仮定に立つことを見ました。今回は、その仮定を入力から外して手法の側へ移す方法です。
まず結論
先頭を基準(pivot)にするクイックソートは、ソート済みの500件を渡されると 124,750 回比べます。最悪の入力が決まっていて、それを渡されたら遅いのです。
基準を乱数で選ぶと、要素がすべて異なる限り、どの入力でも比較回数の期待値が同じになります。この期待値は入力の分布への仮定ではなく、手法が自分で振った乱数についての平均です。
先頭の要素を pivot にするクイックソートに、すでに昇順に並んだ500件を渡します。比較の回数はおよそどれくらいでしょうか。
当てることが目的ではありません。外した予測ほど記憶に残るので、直感で選んでください。
最悪の入力が決まっている手法
クイックソートは、1つの要素を pivot(基準) に選び、残りを「pivot より小さい側」と「大きい側」に分け、それぞれを同じやり方で並べます。Hoare が 1961年に CACM の Algorithm 63(partition)・64(Quicksort)として、1962年に The Computer Journal で発表した手法です。
分けるたびに、残りの要素を1回ずつ pivot と比べます。pivot がいつも端の値だと、片側が空になり、もう片側に1個少ないだけの山が残ります。 先頭を pivot にしてソート済みの列を渡すと、まさにそうなります。比較は (n−1) + (n−2) + … + 1 = n(n−1)/2 回です。
- ・n(n−1)/2 = 500 × 499 / 2 = 124,750
- ・半分ずつに割れれば段数は約 log₂ n で済むが、端で割ると段数が n になる
実測です(Python 3.14.7 / aarch64 Linux、2026-10-07)。
first-element pivot, sorted n=500: comparisons=124750first-element pivot, shuffled n=500: comparisons=4522n(n-1)/2 = 124750
同じ手法で、シャッフルなら 4,522 回、ソート済みなら 124,750 回。 入力がでたらめだと仮定すれば速いのですが、その仮定は渡す側が握っています。ソート済みの列は珍しい入力ではありません。
標準ライブラリは「all data sets」をどう守っているか
Java の Arrays.sort(int[])(JDK 25 の Javadoc、2026-10-07 取得)はこう書きます。
Implementation Note: The sorting algorithm is a Dual-Pivot Quicksort by Vladimir Yaroslavskiy, Jon Bentley, and Joshua Bloch. This algorithm offers O(n log(n)) performance on all data sets, and is typically faster than traditional (one-pivot) Quicksort implementations.
on all data sets ——どの入力でも、と書いています。では最悪の入力はどうしたのか。ソース(タグ jdk-25-ga の DualPivotQuicksort.java)を読むと、分割の途中にこうあります。
/*
* Switch to heap sort if execution
* time is becoming quadratic.
*/
if ((bits += DELTA) > MAX_RECURSION_DEPTH) {
heapSort(a, low, high);
return;
}
再帰が深くなりすぎたら、ヒープソート(別の整列法)に切り替えます。 Javadoc の一文からは、どの仕組みが最悪を防いでいるかは分かりません。ソースを読むと、深さを数えて別の手法へ逃げる仕組みが見えます。乱数とは別の防ぎ方です。
Go は 1.19 のリリースノートでこう書きました。
The sorting algorithm has been rewritten to use pattern-defeating quicksort, which is faster for several common scenarios.
pdqsort という綴りはリリースノートには無く、ソース src/sort/zsortfunc.go のコメント「pattern-defeating quicksort(pdqsort)」にあります。そして利用者向けの pkg.go.dev/sort の本文は、手法名を一切書きません。
It makes one call to data.Len to determine n and O(n*log(n)) calls to data.Less and data.Swap. The sort is not guaranteed to be stable.
約束は呼び出し回数だけで、安定であることは保証しない。 中身の手法は約束の外です。
JDK 25 の Arrays.sort(int[]) の Javadoc は「O(n log(n)) performance on all data sets」と書いています。jdk-25-ga のソースでは、この性質を何が支えていますか。
仮定を入力から外して、手法の側へ移す
もう1つの防ぎ方が乱択です。pivot を先頭に決めず、その範囲から乱数で1つ選びます。
こうすると「この並びを渡せば必ず遅い」という入力が無くなります。遅くなるのは、乱数がたまたま端ばかり引いたときです。入力を渡す側は、手法がまだ振っていない乱数を知りません。
ここで期待値を定義します。起こりうる値それぞれに、その値が出る確率を掛けて足したものです。同じことを何度もやり直したときの平均にあたります。乱択クイックソートでは、入力を1つに固定し、手法が振る乱数の出方について比較回数の期待値を取ります。
第11期の『平均O(1)が置いている仮定』の「平均」とは、仮定を置く場所が違います。
| 何について平均するか | 仮定はどこにあるか | |
|---|---|---|
| ハッシュ表の平均 O(1)(第11期) | 渡される入力の分布 | 入力が「よく散らばる」こと。渡す側が握っている |
| 乱択の期待値(今回) | 手法が振る乱数 | 乱数がでたらめであること。手法の側が握っている |
前者は入力しだいで崩れます。後者は、要素がすべて異なれば、どの入力にも同じ値が成り立ちます。
この前提は効きます。全要素が同じ500件を、上の実験の2分割(小さい側と、それ以外)で乱択 pivot に渡すと、種100通りすべてで 124,750 回でした(Python 3.14.6 / macOS arm64、本教材で再実行)。どの pivot を引いても片側が空になるからです。fieldChecks の「小さい・等しい・大きい」の3分割なら 499 回で済みます。
乱択 pivot と、ハッシュの種
① 乱択 pivot。 同じ実験で、種を0〜99の100通りに変えて平均しました。
2 n ln n = 6214.6exact expectation 2(n+1)H_n - 4n = 4806.4 (H_n = 6.7928)random pivot, sorted n=500: mean over 100 seeds=4847.1 min=4211 max=6030 mean/(2 n ln n)=0.780random pivot, shuffled n=500: mean over 100 seeds=4756.8 min=4263 max=5744 mean/(2 n ln n)=0.765
ソート済みでも 4,847、シャッフルでも 4,757。 入力を変えても分布がほぼ同じです。先頭 pivot の 124,750 と比べると約26分の1です。
よく「期待値は約 2n ln n」と書かれますが、n=500 では 2n ln n = 6,215 で、実測はその 0.78 倍です。2n ln n は n が大きいときの先頭項で、小さい n では落とした項が効きます。正確な期待値 2(n+1)H_n − 4n(H_n は 1 + 1/2 + … + 1/n)は 4,806 で、実測とよく合います。
② ハッシュの種。 第11期で見たとおり、ハッシュ表は全部の鍵が同じバケットに入ると線形に劣化します。ハッシュ関数が固定なら、衝突する鍵の組を前もって作れます。Python の公式ドキュメント(-R の項)はこう書きます。
Hash randomization is intended to provide protection against a denial-of-service caused by carefully chosen inputs that exploit the worst case performance of a dict construction, O(n²) complexity.
種は起動ごとに乱数で決まります。
Although they remain constant within an individual Python process, they are not predictable between repeated invocations of Python.
PYTHONHASHSEED に整数を入れると種は固定され、0 なら乱数化が無効になります。目的は「to allow repeatable hashing」と書かれています。SipHash 系の関数は PEP 456(Python 3.4)が「This PEP proposes SipHash as default string and bytes hash algorithm」として導入しました(その後の既定の版は変わっています)。
Python が str と bytes のハッシュ値に起動ごとの乱数を混ぜる理由として、公式ドキュメントが書いているのはどれですか。
乱択 pivot のクイックソート
pivot を範囲から乱数で選びます。n=500 のソート済み入力で、先頭 pivot は 124,750 回、乱択は種100通りの平均で 4,847 回。シャッフル入力でも 4,757 回で、入力によらず同じ程度になります。
ハッシュの種(PYTHONHASHSEED)
str と bytes のハッシュに起動ごとの乱数を混ぜます。種が固定なら衝突する鍵を前もって作れますが、乱数化されていると作れません。公式は、dict 構築の O(n²) を突くサービス妨害への対策だと書いています。
並べ替えと辞書。扱っているものは違いますが、乱数を入れることで何が「作れなくなった」のでしょうか。
それぞれ、乱数を入れる前に「必ず遅くなる入力」を誰が作れたかを考えてください。
この章のまとめ
- 最悪の入力が決まっている手法は、その入力を渡されると遅い。 先頭 pivot はソート済み500件で 124,750 回 = n(n−1)/2。
- 乱数を手法の側に入れると、最悪の入力を前もって作れなくなる。 乱択 pivot は入力によらず約4,800回。ただし要素がすべて異なる前提で、全同値なら2分割では二次に戻る。
- 期待値は、値と確率を掛けて足したもの。 乱択では入力を固定し、手法の乱数について取る。第11期の平均とは仮定の置き場所が違う。
- 「約 2n ln n」は n が大きいときの先頭項。 n=500 では正確な期待値 4,806 のほうが実測と合う。
- ハッシュの種も同じ形。 Python は起動ごとの乱数で、dict 構築の O(n²) を突く入力を防ぐ。
- 乱数とは別の防ぎ方もある。 JDK 25 は再帰が深くなるとヒープソートに切り替える。
- 既存クイズでは、「アルゴリズムとデータ構造」の algorithm-12、「IV(実務と設計判断)」の algorithm-4-12、「III(発展編)」の algorithm-3-13、「確率と統計 V(ランダム化アルゴリズムと平均計算量)」の prob-randomized-algorithms-1〜3、「計算理論と形式言語 II(発展編)」の computation-theory-2-5 がこの章の結論の一行版です。
CLRS(『アルゴリズムイントロダクション』)の目次や索引で randomized algorithm / quicksort(乱択・クイックソート) を探すと、この回の内容に対応する章や節が見つかります。章の題と番号は版で変わるので、手元の目次で確かめてください。
自分の現場で確かめる
読んだだけでは身につきません。手元の環境で実際に見てみてください。
理解度チェック
読んだ直後は「分かった」と感じやすいものです。実際に解いて確かめてください。
「乱択クイックソートの期待比較回数」の「期待」は、何についての平均ですか。
用語集(4語)
- 期待値
- 起こりうる値それぞれに、その値が出る確率を掛けて足したもの。同じことを何度もやり直したときの平均にあたる。乱択アルゴリズムでは、入力を固定し、手法が振る乱数の出方について取る。
- 乱択アルゴリズム
- 途中で乱数を使うアルゴリズム。結果は同じでも、手間が乱数で揺れる。最悪の入力をあらかじめ決められなくなる。
- pivot(基準値)
- クイックソートで、残りの要素を「より小さい側」と「より大きい側」に分けるときの基準にする1要素。選び方で分割の偏りが決まる。
- ハッシュの種(seed)
- ハッシュ関数に混ぜる値。Python は PYTHONHASHSEED が未設定なら起動ごとに乱数を使い、整数を指定すればその値に固定し、0 なら乱数化を無効にする。
出典(一次ソース)
- C. A. R. Hoare, "Quicksort", The Computer Journal 5(1), 1962, pp. 10–16(書誌のみ確認。本文は未取得) ↗
- C. A. R. Hoare, "Algorithm 64: Quicksort", Communications of the ACM 4(7), 1961, p. 321(同号 Algorithm 63 が partition。書誌のみ確認) ↗
- Java SE 25 API — java.util.Arrays(2026-10-07 取得) ↗
- OpenJDK jdk-25-ga — DualPivotQuicksort.java ↗
- Go 1.19 Release Notes(sort 節) ↗
- Go 1.19 ソース — src/sort/zsortfunc.go ↗
- pkg.go.dev — sort ↗
- PEP 456 — Secure and interchangeable hash algorithm ↗
- Python 公式ドキュメント — Command line and environment(-R / PYTHONHASHSEED) ↗