- 物理機リージョン/AZ
- ハイパーバイザVM exit で捕まえる
- VMゲストOS
- コンテナnamespace / cgroup
- プロセスアプリ / DB今ここ
本コースの1本目で、計算量は「入力が増えたとき何がどれだけ増えるか」の書き方だと見ました。今回はその計算量に、下から押さえる床がある場合を扱います。
まず結論
要素どうしを比べることだけで並べ替えるなら、必要な比較回数には下限があります。CPython の listsort.txt は lg(n!) を「全順列にわたる平均で、比較に基づく方法が到達できる限界」と書いています。
ただしこれは平均の話です。入力が既に並んでいれば、同じ関数が下限を大きく下回ります。実測では100万件のソートが、並びが違うだけで約6.6倍の差になりました。
同じソート関数に、同じ100万件の値を、並び順だけ変えて渡します。実行時間はどうなるでしょうか。
当てることが目的ではありません。外した予測ほど記憶に残るので、直感で選んでください。
下限とは「これ以上速くできない」という主張
計算量はふつう上限で書かれます。「この方法なら n log n 回で済む」——実装が達成できる保証です。今回扱うのは逆向きの主張、**「どんな方法を考えてもこれ以下にはできない」**という不可能性の線です。
CPython の TimSort 設計文書 listsort.txt は、この線を一文で書いています。
lg(n!) is the information-theoretic limit for the best any comparison-based sorting algorithm can do on average (across all permutations)
この一文には、限定が3つ入っています。落とすと主張が変わります。
- lg(n!) — 下限の値。
n log nではなく、階乗の対数として書かれています - comparison-based — 比較に基づく方法に限る
- on average (across all permutations) — 全順列にわたる平均の話
「ソートは O(n log n) が限界」という言い方は、後ろ2つを落としています。何を使う方法の話かとどの入力についての話かが消えると、当てはまらない場所でも通用しそうに見える一文だけが残ります。
- ・上限だけでは「もっと速い方法があるかも」を否定できない
- ・下限だけでは「そこまで到達できる実装があるか」が言えない
上限側を実物で見る — std::sort と std::stable_sort
上限の側は cppreference の std::sort の Complexity 欄から読めます。
Given
Naslast - first: 1,3) 𝓞(N·log(N)) comparisons usingoperator<(until C++20)std::less{}(since C++20).
これは下限ではありません。 𝓞 記法は上界なので、「実装はこれ以下の比較回数で済ませよ」という要求です。「std::sort が n log n だからソートは n log n が限界」は逆向きの読みです。限界(下限)は前節の情報理論の側から来ます。
同じページの Notes に、その要求が後から強くなった経緯があります。
Before LWG713, the complexity requirement allowed
sort()to be implemented using only Quicksort, which may need 𝓞(N²) comparisons in the worst case.
ページ下部の Defect reports の表が LWG 713 の変更を対で記録します。修正前が the 𝓞(N·log(N)) time complexity was only required on the average、修正後が it is required for the worst case。
かつては平均でしか要求されていなかったわけです。「平均か最悪か」を添える理由が、この記録に残っています。
安定版も見ます。std::stable_sort の Complexity 欄。
1,3) 𝓞(N·log²(N)) comparisons using
operator<(until C++20)std::less{}(since C++20) (or only 𝓞(N·log(N)) comparisons if enough extra memory is available).
括弧の中を落としてはいけません。 追加メモリが十分あれば 𝓞(N·log(N)) です。Notes によれば一時バッファの確保を試み、失敗したときだけ効率の落ちる方が選ばれます。
「ソートは O(n log n) が限界です」という説明に足りない前提はどれですか。
下限が立っている前提を外すと、下をくぐれる
下限は無条件の壁ではなく、**3つの限定の上に立っています。**外し方は2通りです。
(a) 比較以外を使う。 原文は comparison-based と範囲を限っており、値の桁や範囲を扱う方法は外です。ただし代わりの前提が要ります——キーの形を知らなければ桁も範囲も使えません。比較は「2つを比べられる」以外に何も要求しない代わりに、床が付いてくるわけです。具体的な計算量は一次ソースを取得できなかったため、本教材では踏み込みません。
(b) 入力の構造を使う。 on average (across all permutations) は全順列を平らに均したときの話で、個々の入力がそれを下回ることは矛盾しません。listsort.txt は下回る場合の説明も書いています。
When a method gets significantly below that, it's either astronomically lucky, or is finding exploitable structure in the data.
そして TimSort 自身がそれをやっています。同ファイル冒頭です。
It has supernatural performance on many kinds of partially ordered arrays (less than lg(N!) comparisons needed, and as few as N-1)
as few as N-1。 既に並んでいれば隣り合う組を1回ずつ比べて終わりです。下限 lg(n!) を大きく下回りますが、その定義に違反はしていません。
CPython 自身の測定値で確かめる
listsort.txt には比較回数の実測表が載っています。列の記号は直感と逆に見えるので、定義から引きます。
*sort: random data\sort: descending data/sort: ascending data=sort: all equal
n=32,768 での TimSort の比較回数です。
入力の条件 比較回数 lg(n!) との関係*sort(ランダム) 448,885 下限 444,255 のすぐ上\sort(降順) 32,767 n-1。下限を大きく下回る/sort(昇順) 32,767 n-1=sort(全て同じ) 32,767 n-1
ランダムな入力では 448,885 回。 lg(32768!) = 444,255 なので、下限の約1.01倍です。下限のすぐ上に張り付いている——これが「上限と下限が一致する」ことの実物です。
一方、昇順・降順・全同値では 32,767 = n-1 回。降順でも n-1 で済むのは、TimSort が降順の連(run)を見つけて反転するためです。
なお、下限の値そのものを見ておきます。n=100万で計算すると、lg(n!) は約 1,849万、n·log₂n は約 1,993万 です。漸近的に等しいというのは「比が1に近づく」という意味であって、この規模では約8%違います。「lg(n!) は漸近的に n log n に等しい」と一段挟まずに置き換えると、数字が合わなくなります。
cppreference が std::sort について書く 𝓞(N·log(N)) は、何を意味しますか。
ランダムな100万件
float 100万件をランダムな順序で sorted() に渡します。構造が無いので比較回数は下限のすぐ上まで積み上がります。各条件5回、最小値を読んでいます(CPython 3.13.15 / Linux 6.10.14 aarch64 コンテナ / ホストは Apple Silicon)。
条件 最小 中央 最大ランダム 0.1323 0.1334 0.1420ソート済み 0.0201 0.0203 0.0209逆順 0.0223 0.0231 0.0249
既にソート済みの100万件
同じ値の集合を、既に昇順に並んだ状態で同じ sorted() に渡します。0.0201秒で、ランダムの約6.6倍の速さ。件数も値も関数も同じで、違うのは並び順だけです。逆順(0.0223秒)もほぼ同じく速い点に注意してください。
同じ関数・同じ件数・同じ値の集合で、並び順だけが違います。この6.6倍の差は、下限 lg(n!) と矛盾していないでしょうか。
下限の原文に付いていた3つの限定のうち、どれが効いているかを考えてみてください。
この章のまとめ
- 比較だけで並べ替えるなら比較回数に下限がある。listsort.txt は
lg(n!)をthe information-theoretic limit for the best any comparison-based sorting algorithm can do on average (across all permutations)と書いている。 - この一文の限定は3つ。lg(n!) という値、comparison-based という範囲、on average という前提。
std::sortの 𝓞(N·log(N)) は下限ではなく上限(実装への要求)。下限と上限が揃うから「比較だけではこれ以上速くならない」と言える。std::stable_sortの 𝓞(N·log²(N)) にはor only 𝓞(N·log(N)) comparisons if enough extra memory is availableという条件節が付く。安定性は性質で、速度ではない。- 下限は平均の話なので、入力の構造を使えば下をくぐれる。TimSort は既に並んだ列を
as few as N-1で終える。 - 実測(CPython 3.13.15、float 100万件、各条件5回の最小値)はソート済み 0.0201秒・逆順 0.0223秒・ランダム 0.1323秒。同じ関数・同じ件数・並びだけが違って約6.6倍。
自分の現場で確かめる
読んだだけでは身につきません。手元の環境で実際に見てみてください。
理解度チェック
読んだ直後は「分かった」と感じやすいものです。実際に解いて確かめてください。
「比較ソートはこれ以上速くならない」と言えるのは、何と何が揃ったときですか。
用語集(4語)
- 比較ソート (comparison-based sort)
- 要素どうしを「どちらが先か」と比べる操作だけを使って並べ替える方法。値そのものの桁や範囲は利用しない。lg(n!) の下限が掛かるのはこの範囲に限られる。
- lg(n!)
- 1 から n までの積の、2 を底とする対数。listsort.txt が比較ソートの限界として挙げている値。漸近的には n log n に等しいが同じ数ではない(n=100万で約 1,849万 対 約 1,993万)。
- 上限と下限
- 𝓞 記法は上界で「これ以下で済む」という保証。下限は「これ以上必要」という不可能性の主張。両方が同じ位置で揃って初めて「これ以上速くならない」と言える。
- 安定 (stable)
- 等しいと判定された要素どうしの元の順序が、並べ替えた後も変わらない性質。速度とは独立しており、Python の sort は無条件に保証している。