5.4.1 理論

(説明は 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

上の木構造では、各セル k2*k+1 および 2*k+2 を最大値としています。 スポーツに見られるような通常の 2 つ組勝ち抜き戦では、各セルはその 下にある二つのセルに対する勝者となっていて、個々のセルの勝者を 追跡していくことにより、そのセルに対する全ての相手を見ることが できます。しかしながら、このような勝ち抜き戦を使う計算機 アプリケーションの多くでは、勝歴を追跡する必要はりません。 メモリ効率をより高めるために、勝者が上位に進級した際、 下のレベルから持ってきて置き換えることにすると、あるセルと その下位にある二つのセルは異なる三つの要素を含み、かつ 上位のセルは二つの下位のセルに対して "勝者と" なります。

このヒープ不変式が常に守られれば、インデクス 0 は明らかに 最勝者となります。最勝者の要素を除去し、"次の" 勝者を見つける ための最も単純なアルゴリズム的手法は、ある敗者要素 (ここでは上図の セル 30 とします) を 0 の場所に持っていき、この新しい 0 を 濾過するようにしてツリーを下らせて値を交換してゆきます。不変関係が 再構築されるまでこれを続けます。この操作は明らかに、ツリー内の 全ての要素数に対して対数的な計算量となります。全ての要素について 繰り返すと、 O(n log n) のソート(並べ替え)になります。

このソートの良い点は、新たに挿入する要素が、その最に取り出す 0 番目の 要素よりも "良い値" でない限り、ソートを行っている最中に新たな要素を 効率的に追加できるというところです。

この性質は、シミュレーション的な状況で、ツリーで全ての入力 イベントを保持し、"勝者となる状況" を最小のスケジュール時刻にする ような場合に特に便利です。あるイベントが他のイベント群の実行を スケジュールする際、それらは未来にスケジュールされることになるので、 それらのイベント群を容易にヒープに積むことができます。 すなわち、ヒープはスケジューラを実装する上で良いデータ構造で あるといえます (私は MIDI シーケンサで使っているものです :-).

これまでスケジューラを実装するための様々なデータ構造が広範に 研究されています。ヒープは十分高速で、速度もおおむね一定であり、 最悪の場合でも平均的な速度とさほど変わらないため良いデータ構造と いえます。しかし、最悪の場合がひどい速度になることを除き、 たいていでより効率の高い他のデータ構造表現も存在します。

ヒープはまた、巨大なディスクのソートでも非常に有用です。 おそらくご存知のように、巨大なソートを行うと、複数の "ラン (run)" (予めソートされた配列で、そのサイズは通常 CPU メモリの量に関係 しています) が生成され、続いて統合処理 (merging) がこれらのランを 判定します。この統合処理はしばしば非常に巧妙に組織されています 5.1。 重要なのは、最初のソートが可能な限り長いランを生成することです。 勝ち抜き戦はこれを行うための良い方法です。もし利用可能な全ての メモリを使って勝ち抜き戦を行い、要素を置換および濾過処理して 現在のランに収めれば、ランダムな入力に対してメモリの二倍の サイズのランを生成することになり、大体順序づけがなされている入力に 対してはもっと高い効率になります。

さらに、ディスク上の 0 番目の要素を出力して、現在の勝ち抜き戦に (最後に出力した値に "勝って" しまうために) 収められない入力を得た なら、ヒープには収まらないため、ヒープのサイズは減少します。 解放されたメモリは二つ目のヒープを段階的に構築するために巧妙に再利用 することができ、この二つ目のヒープは最初のヒープが崩壊していく のと同じ速度で成長します。最初のヒープが完全に消滅したら、 ヒープを切り替えて新たなランを開始します。なんと巧妙で 効率的なのでしょう!

一言で言うと、ヒープは知って得するメモリ構造です。 私はいくつかのアプリケーションでヒープを使っていて、 `ヒープ' モジュールを常備するのはいい事だと考えています。 :-)


脚注

... 判定します。この統合処理はしばしば非常に巧妙に組織されています5.1
現在使われているディスクバランス化アルゴリズムは、最近は もはや巧妙というよりも目障りであり、このためにディスクに対するシーク 機能が重要になっています。巨大な容量を持つテープのようにシーク不能な デバイスでは、事情は全く異なり、個々のテープ上の移動が可能な限り 効率的に行われるように非常に巧妙な処理を (相当前もって) 行わねば なりません (すなわち、もっとも統合処理の "進行" に関係があります)。 テープによっては逆方向に読むことさえでき、巻き戻しに時間を取られる のを避けるために使うこともできます。正直、本当に良いテープソート は見ていて素晴らしく驚異的なものです!ソートというのは常に偉大な 芸術なのです!:-)
ご意見やご指摘をお寄せになりたい方は、 このドキュメントについて... をご覧ください。