Processing math: 100%

[データ構造] ヒープ

優先度付きキュー 優先度付きキュー(ゆうせんどつき -、英:priority queue)は、以下の4つの操作をサポートする抽象データ型である。 キューに対して要素を優先度付きで追加する。最も高い優先度を持つ要素をキューから取り除き、それを返す。(オプ...

ヒープは過去にもまとめています。

2分ヒープ 2分ヒープを理解して、Pythonで実装します。 まず、heap とは、「積み重なった山のようなもの」です。a heap of stones と言えば、石が山のように積み重なっているものです。 ヒープソートは、この山のように積み重なったものの一番上...

最初よく混乱しますが、メモリのヒープとデータ構造のヒープは別のものです。

2分ヒープ

二分ヒープ(にぶんヒープ,バイナリヒープ,Binary heap)とは、二分木を使って作られるヒープ(データ構造)の特に単純な種類のひとつである。

出典: フリー百科事典『ウィキペディア(Wikipedia)』

2分ヒープは、2つの子を持つ木構造である2分木を使って作られ、要素の中で最大のもの(または最小のもの)がルートに来るように構成されるデータ構造です。

要素の中で最大のものがルートになる場合は max heap、最小のものがルートになる場合は min heap と呼ばれます。

2分ヒープを使うことで、要素の取得がO(1)で行える優先度付きキューを実装できます。

また、グラフの経路問題を解くダイクストラ法プリム法にも使われます。

Heap Properties 2分ヒープの特性

赤黒木が守らなければならない Red Black Properties というルールがあったように、ヒープにも守らなければならない Heap Properties 2分ヒープの特性 があります。

完全2分木

ヒープの2分木は、完全2分木 ‘complete binary tree’ になっていなければなりません。

完全2分木 ‘complete binary tree’ とは、最下層以外はノードが全て埋まっており、また最下層のノードがある場合は、可能な限り左側に寄っている2分木です。

もしデータの挿入をする時は、次の空いている場所に挿入します。

親が子より常に大きい、又は常に小さい

max heap であれば「親が子より常に大きい」、min heap であれば「親が子より常に小さい」 という制約を満たします。

2分探索木のように 「左の子孫の値 ≤ 親の値 ≤ 右の子孫の値」というような制約はありません。

結果として、max heap であればルートが最大、min heap であればルートが最小になり、最大値や最小値の取得をO(1) で行うことができます。

ヒープと配列

max heap で考えます。

この max heap は、ルート→左の子→右の子→左の左の子→…という順で全てのノードにインデックスを振ることで、以下のような配列になります。

index0123456
5030201510816

つまり、ヒープは一次元の配列で構成されます。

また、インデックス i のノードがあるとして、その子ノード、親ノードのインデックスは以下のようになります。

  • そのノードの左側の子のインデックスは 2 * i + 1
  • そのノードの右側の子のインデックスは 2 * i + 2
  • そのノードの親のインデックスは i / 2 (切り下げ)

ヒープの操作

データの挿入

データの挿入が行われると、そのデータは最下層の空いている中で一番左側に挿入され、Heap Properties が維持されているかの確認が行い、必要であればデータを入れ替えます。

このヒープ構造の維持のためのデータの入れ替えを heapify と呼び、データの挿入はボトムアップ的に行われるので、heapify-up と呼ばれます。

wikipedia の例そのままです。

以下のような max heap を考えます。

このヒープに15を挿入します。

index1234567
11583415

15は親の8より大きいので入れ替えます。

index1234567
11515348

さらに、15は親の11より大きいので入れ替えます。

index1234567
15511348

挿入が終わりました。

この操作は最悪木の高さだけ行われるので O(logN) の計算量になります。

ヒープの構築

ヒープの構築は、配列に値を挿入する操作と、挿入した値が Heap Properties を維持できない場合に入れ替える Heapify の操作で行われます。

計算量は配列への挿入はO(N) 、Heapify はO(log(N) で、O(N)+O(log(N)=O(N) になります。

データのルートからの削除

ヒープからのデータの削除とは、通常はルートからの削除(つまり最大値/最小値の取り出し)を考えます。

ルートが削除されると、一番最後の要素をルートに持ってきて、その後 Heap Properties を維持するように値の入れ替えを行います。

一番最後の要素へのアクセスは、配列の一番最後の要素にインデックスでアクセスすれば良いので、O(1)で行えます。

この heapify はトップダウン的に行われるので heapify-down と呼ばれます。

また、wikipeida からの転載です。

以下のような max heap を考えます。

index1234567
115834

11を削除すると、一番最後の4と入れ替えます。

index1234567
4583

5 は Heap Properties を満たしますが、8は満たさないので、値の入れ替えを行います。

index1234567
8543

この操作は最悪木の高さだけ行われるので O(logN) の計算量になります。

任意の箇所のデータの削除

通常は考えませんが、もしヒープで任意の箇所の値の削除を行う場合は、まず配列上で削除する値を探し削除し、その後ヒープを再構築します。

計算量は、配列での値の削除はO(N) 、Heapify はO(logN) で、O(N)+O(logN)=O(N) になります。

ヒープソート

ヒープソート (heap sort) とはリストの並べ替えを二分ヒープ木を用いて行うソートアルゴリズムである[2]ヒープ領域とは無関係であることに注意する)。

出典: フリー百科事典『ウィキペディア(Wikipedia)』
Python でヒープソートを実装します。 ヒープソート ヒープソート(heap sort) とはリストの並べ替えを二分ヒープ木を用いて行うソートのアルゴリズムである(ヒープ領域とは無関係であることに注意する)。 出典: フリー百科事典『ウィキペディア(Wiki...

ヒープソートは、探索によって最小値/最大値を探しソートするのではなく、ヒープのデータ構造を利用して最大値/最小値を探しソートします。

最悪で計算量が O(NlogN)であり、クイックソートは最悪ではO(N2) になるので、最悪を考えた場合はクイックソートより良くなります。

最大値/最小値を先頭から取得したら、その要素を配列の一番最後の要素と交換し、ヒープからはその要素を削除します。

こうすると、配列上は存在しますが、ヒープ上は存在しない要素になります。

この操作を続けると、最終的にヒープから要素はなくなり配列上にはソートされた順序で要素が並ぶことになります。

ソートを行うために、追加的なメモリを必要としません。

ソートするためにまずヒープを構築しなければならないので、最初にO(N) の計算量が必要になります。

ヒープの計算量

空間計算量は、N個の要素を保存するので O(N) になります。

最大値/最小値の探索は、配列の一番最初の値を探すだけなのでO(1) で行えます。

値の挿入は、配列の一番最後への挿入がO(1) 、その後 heapify の操作が木の高さだけ行われ O(logN) の計算量になります。

削除も同様に、値の削除は O(1) ですが、その後 heapify の操作が木の高さだけ行われ O(logN) の計算量になります。

2分探索木では最大値/最小値の取得は、一番右端/左端の要素に移動するためO(logN) の計算量になりましたが、ヒープは最大値/最小値の取得 が O(1)で行うことができるので、最大値/最小値の取得が必要なアルゴリズムでは、ヒープは有効な選択肢になります。

その他のヒープ構造

2項ヒープ

二項ヒープ(にこうヒープ、binomial heap)とは、計算機科学におけるデータ構造ヒープ)の1つである。

出典: フリー百科事典『ウィキペディア(Wikipedia)』

2分ヒープは2分木を使いましたが、2項ヒープは 「2項木の集合」を使い、2つのヒープのマージをO(logN) で行うことができます。

フィボナッチヒープ

フィボナッチヒープ: Fibonacci heap)とは、計算機科学におけるデータ構造ヒープ)の1つ。

出典: フリー百科事典『ウィキペディア(Wikipedia)』

2分ヒープより高速ですが実装が難しいです。2項ヒープとおなじようにマージの操作を持つヒープで、特に値の追加をO(1) で行うことができます。

Python のリストを使い Max Heap を2分ヒープで実装します。 初期化 配列の最大要素数を定数MAX_NUM_ITEMSとして定めます。 また、self.heap_size という定数を用意して、配列の最後の要素にアクセスする際...