目次

前のトピックへ

8.3. collections — 高性能なコンテナ・データ型

次のトピックへ

8.5. bisect — 配列二分法アルゴリズム

このページ

8.4. heapq — ヒープキューアルゴリズム

バージョン 2.3 で追加.

このモジュールではヒープキューアルゴリズムの一実装を提供しています。優先度キューアルゴリズムとしても知られています。

ヒープとは、全ての k に対して、ゼロから要素を数えていった際に、 heap[k] <= heap[2*k+1] かつ heap[k] <= heap[2*k+2] となる配列です。比較のために、存在しない要素は無限大として扱われます。ヒープの興味深い属性は heap[0] が常に最小の要素になることです。

以下の API は教科書におけるヒープアルゴリズムとは 2 つの側面で異なっています: (a) ゼロベースのインデクス化を行っています。これにより、ノードに対するインデクスとその子ノードのインデクスの関係がやや明瞭でなくなりますが、 Python はゼロベースのインデクス化を使っているのでよりしっくりきます。 (b) われわれの pop メソッドは最大の要素ではなく最小の要素 (教科書では “min heap:最小ヒープ” と呼ばれています; 教科書では並べ替えをインプレースで行うのに適した “max heap:最大ヒープ” が一般的です)。

これらの 2 点によって、ユーザに戸惑いを与えることなく、ヒープを通常の Python リストとして見ることができます: heap[0] が最小の要素となり、 heap.sort() はヒープ不変式を保ちます!

ヒープを作成するには、 [] に初期化されたリストを使うか、 heapify() を用いて要素の入ったリストを変換します。

以下の関数が提供されています:

heapq.heappush(heap, item)

itemheap に push します。ヒープ不変式を保ちます。

heapq.heappop(heap)

pop を行い、 heap から最初の要素を返します。ヒープ不変式は保たれます。ヒープが空の場合、 IndexError が送出されます。

heapq.heappushpop(heap, item)

itemheap に push した後、pop を行って heap から最初の要素を返します。この一続きの動作を heappush() に引き続いて heappop() を別々に呼び出すよりも効率的に実行します。

バージョン 2.6 で追加.

heapq.heapify(x)

リスト x をインプレース処理し、線形時間でヒープに変換します。

heapq.heapreplace(heap, item)

heap から最小の要素を pop して返し、新たに item を push します。ヒープのサイズは変更されません。ヒープが空の場合、 IndexError が送出されます。この関数は heappop() に次いで heappush() を送出するよりも効率的で、固定サイズのヒープを用いている場合にはより適しています。返される値は item よりも大きくなるかもしれないので気をつけてください! これにより、このルーチンの合理的な利用法は条件つき置換の一部として使われることに制限されています。

if item > heap[0]:
    item = heapreplace(heap, item)

使用例を以下に示します:

>>> from heapq import heappush, heappop
>>> heap = []
>>> data = [1, 3, 5, 7, 9, 2, 4, 6, 8, 0]
>>> for item in data:
...     heappush(heap, item)
...
>>> ordered = []
>>> while heap:
...     ordered.append(heappop(heap))
...
>>> print ordered
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> data.sort()
>>> print data == ordered
True

ヒープを使ってアイテムを優先度キューの正しい位置に挿入します:

>>> heap = []
>>> data = [(1, 'J'), (4, 'N'), (3, 'H'), (2, 'O')]
>>> for item in data:
...     heappush(heap, item)
...
>>> while heap:
...     print heappop(heap)[1]
J
O
H
N

このモジュールではさらに3つのヒープに基く汎用関数を提供します。

heapq.merge(*iterables)

複数のソートされた入力をマージ(merge)して一つのソートされた出力にします (たとえば、複数のログファイルの時刻の入ったエントリーをマージします)。ソートされた値にわたる iterator を返します。

sorted(itertools.chain(*iterables)) と似ていますが、イテレータを返し、一度にはデータをメモリに読み込みまず、それぞれの入力が(最小から最大へ)ソートされていることを仮定します。

バージョン 2.6 で追加.

heapq.nlargest(n, iterable[, key])

iterable で定義されるデータセットのうち、最大値から降順に n 個の値のリストを返します。 (あたえられた場合) key は、引数を一つとる、 iterable のそれぞれの要素から比較キーを生成する関数を指定します: key=str.lower 以下のコードと同等です: sorted(iterable, key=key, reverse=True)[:n]

バージョン 2.4 で追加.

バージョン 2.5 で変更: 省略可能な key 引数を追加.

heapq.nsmallest(n, iterable[, key])

iterable で定義されるデータセットのうち、最小値から昇順に n 個の値のリストを返します。 (あたえられた場合) key は、引数を一つとる、 iterable のそれぞれの要素から比較キーを生成する関数を指定します: key=str.lower 以下のコードと同等です: sorted(iterable, key=key)[:n]

バージョン 2.4 で追加.

バージョン 2.5 で変更: 省略可能な key 引数を追加.

後ろ二つの関数は n の値が小さな場合に最適な動作をします。大きな値の時には sorted() 関数の方が効率的です。さらに、 n==1 の時には min() および max() 関数の方が効率的です。

8.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) がこれらのランを判定します。この統合処理はしばしば非常に巧妙に組織されています [1] 。重要なのは、最初のソートが可能な限り長いランを生成することです。勝ち抜き戦はこれを行うための良い方法です。もし利用可能な全てのメモリを使って勝ち抜き戦を行い、要素を置換および濾過処理して現在のランに収めれば、ランダムな入力に対してメモリの二倍のサイズのランを生成することになり、大体順序づけがなされている入力に対してはもっと高い効率になります。

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

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

Footnotes

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