さらに読むには、 ツリーの概念が必要です 。また、アルゴリズムの複雑さの評価について知ることもお勧めします 。 この記事のアルゴリズムには、C#コードが付随します。
はじめに
バイナリヒープは、ヒープのメインプロパティが満たされる完全なバイナリツリーです。各頂点の優先度は、その子孫の優先度よりも高くなっています。 最も単純な場合、各頂点の優先順位はその値と等しいと見なすことができます。 この場合、サブツリーのルートはサブツリーの要素の値の最大値であるため、構造はmax-heapと呼ばれます 。 この記事では、簡単にするためにそのような表現を使用しています。 各頂点に2つ以下の子孫しかなく、頂点レベルの充填が上から下に(同じレベルから左から右に)行われる場合、ツリーは完全なバイナリと呼ばれることを思い出してください。
バイナリヒープを1次元配列の形式で保存すると便利です。インデックス
i
の頂点の左の子孫はインデックス
2*i+1
で、右の子孫は
2*i+2
です。 ツリーのルートはインデックス0の要素です。バイナリヒープの高さはツリーの高さ、つまりlog 2 Nに等しく、
N
は配列内の要素の数です。
C#でクラスを空白にします:
public class BinaryHeap { private List<int> list; public int heapSize { get { return this.list.Count(); } } }
アイテムを追加
新しい要素は、配列の最後の場所、つまり
heapSize
インデックスの位置に
heapSize
ます。
新しい要素は親よりも大きいため、これがヒープのメインプロパティに違反する可能性があります。 この場合、ヒープのメインプロパティが観測されるまで、新しい要素を1レベル「親頂点で変更」する必要があります。
言い換えると、新しい要素は、それが配置されるまで「ポップアップ」し、「プッシュ」します。 アルゴリズムの複雑さはバイナリヒープの高さを超えない(「リフト」の数がツリーの高さより大きくないため)、つまりO(log 2 N)に等しい。
public void add(int value) { list.Add(value); int i = heapSize - 1; int parent = (i - 1) / 2; while (i > 0 && list[parent] < list[i]) { int temp = list[i]; list[i] = list[parent]; list[parent] = temp; i = parent; parent = (i - 1) / 2; } }
バイナリヒープの順序
既に構築されたバイナリヒープを使用する他の操作の過程で、ヒープのメインプロパティも違反する可能性があります。頂点はその子孫よりも小さくなる可能性があります。
heapify
メソッドは、両方のサブツリーが満たす限り、i番目の頂点にルートを持つツリーの基本的なヒーププロパティを復元します。 このためには、メインプロパティが復元されるまで(最大の子孫との交換)、i番目の頂点を「下げる」必要があります(子孫がなく、その大きな親がなくなるとプロセスは終了します)。 このアルゴリズムの複雑さもO(log 2 N)に等しいことがわかります。
public void heapify(int i) { int leftChild; int rightChild; int largestChild; for (; ; ) { leftChild = 2 * i + 1; rightChild = 2 * i + 2; largestChild = i; if (leftChild < heapSize && list[leftChild] > list[largestChild]) { largestChild = leftChild; } if (rightChild < heapSize && list[rightChild] > list[largestChild]) { largestChild = rightChild; } if (largestChild == i) { break; } int temp = list[i]; list[i] = list[largestChild]; list[largestChild] = temp; i = largestChild; } }
バイナリヒープの構築
順序付けられていない配列から束を構築する最も明白な方法は、すべての要素を順番に追加することです。 このようなアルゴリズムの推定時間はO(N log 2 N)です。 ただし、O(N)の場合は、さらに高速で束を作成できます。 まず、ヒープのメインプロパティを監視することなく、配列のすべての要素からツリーを構築する必要があります。次に、少なくとも1つの子孫を持つすべての頂点に対して
heapify
メソッドを呼び出します(子孫のない1つの頂点で構成されるサブツリーが既に順序付けられているため)。 最初の
heapSize/2
頂点には子孫があることが保証されています。
public void buildHeap(int[] sourceArray) { list = sourceArray.ToList(); for (int i = heapSize / 2; i >= 0; i--) { heapify(i); } }
最大要素の抽出(削除)
順序付けされた
max-heap
最大要素は常にルートに格納されます。 最大の要素を削除した後にバイナリヒープの順序を復元するには、最後の要素をその場所に
heapify
し、ルートに対して
heapify
を呼び出します。つまり、ツリー全体を順序付けます。
public int getMax() { int result = list[0]; list[0] = list[heapSize - 1]; list.RemoveAt(heapSize - 1); return result; }
バイナリヒープソート
最初にバイナリヒープを構築してから、最大要素を順番に抽出することで、配列をソートできることに注意してください。 そのような要素の時間の複雑さを評価しましょう:ヒープ構築-O(N)、
N
要素の抽出-O(N log 2 N)。 したがって、最終スコアはO(N log 2 N)です。 この場合、アレイ用の追加メモリは使用されません。
public void heapSort(int[] array) { buildHeap(array); for (int i = array.Length - 1; i >= 0; i--) { array[i] = getMax(); heapify(0); } }
おわりに
したがって、バイナリヒープは(頂点の数に対して)対数の高さのツリー構造を持ち、対数時間で要素を追加し、一定の時間で最大の優先順位を持つ要素を抽出できます。 同時に、バイナリヒープは簡単に実装でき、追加のメモリは必要ありません。