(説明は François Pinard によるものです。このモジュールの Python コード は Kevin O'Connor の貢献によるものです。)
ヒープとは、全ての k について、要素を 0 から数えたときに、
a[k] <= a[2*k+1]
かつ
a[k] <= a[2*k+2]
となる配列です。
比較のために、存在しない要素を無限大と考えます。
ヒープの興味深い属性は heap[0]
が常に最小の要素に
なることです。
上記の奇妙な不変式は、勝ち抜き戦判定の際に効率的なメモリ表現を行う
ためのものです。
以下の番号は a[k]
ではなく k とします:
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
上の木構造では、各セル k は 2*k+1
および
2*k+2
を最大値としています。
スポーツに見られるような通常の 2 つ組勝ち抜き戦では、各セルはその
下にある二つのセルに対する勝者となっていて、個々のセルの勝者を
追跡していくことにより、そのセルに対する全ての相手を見ることが
できます。しかしながら、このような勝ち抜き戦を使う計算機
アプリケーションの多くでは、勝歴を追跡する必要はりません。
メモリ効率をより高めるために、勝者が上位に進級した際、
下のレベルから持ってきて置き換えることにすると、あるセルと
その下位にある二つのセルは異なる三つの要素を含み、かつ
上位のセルは二つの下位のセルに対して "勝者と" なります。
このヒープ不変式が常に守られれば、インデクス 0 は明らかに 最勝者となります。最勝者の要素を除去し、"次の" 勝者を見つける ための最も単純なアルゴリズム的手法は、ある敗者要素 (ここでは上図の セル 30 とします) を 0 の場所に持っていき、この新しい 0 を 濾過するようにしてツリーを下らせて値を交換してゆきます。不変関係が 再構築されるまでこれを続けます。この操作は明らかに、ツリー内の 全ての要素数に対して対数的な計算量となります。全ての要素について 繰り返すと、 O(n log n) のソート(並べ替え)になります。
このソートの良い点は、新たに挿入する要素が、その最に取り出す 0 番目の 要素よりも "良い値" でない限り、ソートを行っている最中に新たな要素を 効率的に追加できるというところです。
この性質は、シミュレーション的な状況で、ツリーで全ての入力 イベントを保持し、"勝者となる状況" を最小のスケジュール時刻にする ような場合に特に便利です。あるイベントが他のイベント群の実行を スケジュールする際、それらは未来にスケジュールされることになるので、 それらのイベント群を容易にヒープに積むことができます。 すなわち、ヒープはスケジューラを実装する上で良いデータ構造で あるといえます (私は MIDI シーケンサで使っているものです :-).
これまでスケジューラを実装するための様々なデータ構造が広範に 研究されています。ヒープは十分高速で、速度もおおむね一定であり、 最悪の場合でも平均的な速度とさほど変わらないため良いデータ構造と いえます。しかし、最悪の場合がひどい速度になることを除き、 たいていでより効率の高い他のデータ構造表現も存在します。
ヒープはまた、巨大なディスクのソートでも非常に有用です。 おそらくご存知のように、巨大なソートを行うと、複数の "ラン (run)" (予めソートされた配列で、そのサイズは通常 CPU メモリの量に関係 しています) が生成され、続いて統合処理 (merging) がこれらのランを 判定します。この統合処理はしばしば非常に巧妙に組織されています 5.1。 重要なのは、最初のソートが可能な限り長いランを生成することです。 勝ち抜き戦はこれを行うための良い方法です。もし利用可能な全ての メモリを使って勝ち抜き戦を行い、要素を置換および濾過処理して 現在のランに収めれば、ランダムな入力に対してメモリの二倍の サイズのランを生成することになり、大体順序づけがなされている入力に 対してはもっと高い効率になります。
さらに、ディスク上の 0 番目の要素を出力して、現在の勝ち抜き戦に (最後に出力した値に "勝って" しまうために) 収められない入力を得た なら、ヒープには収まらないため、ヒープのサイズは減少します。 解放されたメモリは二つ目のヒープを段階的に構築するために巧妙に再利用 することができ、この二つ目のヒープは最初のヒープが崩壊していく のと同じ速度で成長します。最初のヒープが完全に消滅したら、 ヒープを切り替えて新たなランを開始します。なんと巧妙で 効率的なのでしょう!
一言で言うと、ヒープは知って得するメモリ構造です。
私はいくつかのアプリケーションでヒープを使っていて、
`ヒープ' モジュールを常備するのはいい事だと考えています。 :-)