--- comments: true --- # 8.1 ヒープ ヒープは特定の条件を満たす完備二分木で、主に次の2つのタイプに分類されます(下図参照)。 - 最小ヒープ:任意のノードの値 $\leq$ その子ノードの値。 - 最大ヒープ:任意のノードの値 $\geq$ その子ノードの値。 { class="animation-figure" }
図 8-1 最小ヒープと最大ヒープ
完備二分木の特別なケースとして、ヒープには以下の特性があります: - 最下位層のノードは左から右に埋められ、他の層のノードは完全に埋められています。 - 二分木の根ノードをヒープの「先頭」と呼び、最も右下のノードをヒープの「末尾」と呼びます。 - 最大ヒープ(最小ヒープ)の場合、先頭要素(根)の値はすべての要素の中で最大(最小)です。 ## 8.1.1 ヒープの一般的な操作 多くのプログラミング言語が優先度キューを提供していることに注意してください。これは優先度付きソートを持つキューとして定義される抽象データ構造です。 実際には、**ヒープは優先度キューを実装するためによく使用されます。最大ヒープは、要素が降順でデキューされる優先度キューに対応します**。使用の観点から、「優先度キュー」と「ヒープ」を同等のデータ構造と考えることができます。したがって、この本では両者を特別に区別せず、統一して「ヒープ」と呼びます。 ヒープの一般的な操作を下表に示します。メソッド名はプログラミング言語によって異なる場合があります。表 8-1 ヒープ操作の効率
図 8-2 ヒープの表現と格納
後で便利に使用するため、インデックスマッピング公式を関数にカプセル化できます: === "Python" ```python title="my_heap.py" def left(self, i: int) -> int: """左の子ノードのインデックスを取得""" return 2 * i + 1 def right(self, i: int) -> int: """右の子ノードのインデックスを取得""" return 2 * i + 2 def parent(self, i: int) -> int: """親ノードのインデックスを取得""" return (i - 1) // 2 # 整数除算で切り下げ ``` === "C++" ```cpp title="my_heap.cpp" /* 左の子ノードのインデックスを取得 */ int left(int i) { return 2 * i + 1; } /* 右の子ノードのインデックスを取得 */ int right(int i) { return 2 * i + 2; } /* 親ノードのインデックスを取得 */ int parent(int i) { return (i - 1) / 2; // 整数除算で切り下げ } ``` === "Java" ```java title="my_heap.java" /* 左の子ノードのインデックスを取得 */ int left(int i) { return 2 * i + 1; } /* 右の子ノードのインデックスを取得 */ int right(int i) { return 2 * i + 2; } /* 親ノードのインデックスを取得 */ int parent(int i) { return (i - 1) / 2; // 整数除算で切り下げ } ``` === "C#" ```csharp title="my_heap.cs" [class]{MaxHeap}-[func]{Left} [class]{MaxHeap}-[func]{Right} [class]{MaxHeap}-[func]{Parent} ``` === "Go" ```go title="my_heap.go" [class]{maxHeap}-[func]{left} [class]{maxHeap}-[func]{right} [class]{maxHeap}-[func]{parent} ``` === "Swift" ```swift title="my_heap.swift" [class]{MaxHeap}-[func]{left} [class]{MaxHeap}-[func]{right} [class]{MaxHeap}-[func]{parent} ``` === "JS" ```javascript title="my_heap.js" [class]{MaxHeap}-[func]{left} [class]{MaxHeap}-[func]{right} [class]{MaxHeap}-[func]{parent} ``` === "TS" ```typescript title="my_heap.ts" [class]{MaxHeap}-[func]{left} [class]{MaxHeap}-[func]{right} [class]{MaxHeap}-[func]{parent} ``` === "Dart" ```dart title="my_heap.dart" [class]{MaxHeap}-[func]{_left} [class]{MaxHeap}-[func]{_right} [class]{MaxHeap}-[func]{_parent} ``` === "Rust" ```rust title="my_heap.rs" [class]{MaxHeap}-[func]{left} [class]{MaxHeap}-[func]{right} [class]{MaxHeap}-[func]{parent} ``` === "C" ```c title="my_heap.c" [class]{MaxHeap}-[func]{left} [class]{MaxHeap}-[func]{right} [class]{MaxHeap}-[func]{parent} ``` === "Kotlin" ```kotlin title="my_heap.kt" [class]{MaxHeap}-[func]{left} [class]{MaxHeap}-[func]{right} [class]{MaxHeap}-[func]{parent} ``` === "Ruby" ```ruby title="my_heap.rb" [class]{MaxHeap}-[func]{left} [class]{MaxHeap}-[func]{right} [class]{MaxHeap}-[func]{parent} ``` ### 2. ヒープの先頭要素へのアクセス ヒープの先頭要素は二分木の根ノードで、リストの最初の要素でもあります: === "Python" ```python title="my_heap.py" def peek(self) -> int: """ヒープの先頭要素にアクセス""" return self.max_heap[0] ``` === "C++" ```cpp title="my_heap.cpp" /* ヒープの先頭要素にアクセス */ int peek() { return maxHeap[0]; } ``` === "Java" ```java title="my_heap.java" /* ヒープの先頭要素にアクセス */ int peek() { return maxHeap.get(0); } ``` === "C#" ```csharp title="my_heap.cs" [class]{MaxHeap}-[func]{Peek} ``` === "Go" ```go title="my_heap.go" [class]{maxHeap}-[func]{peek} ``` === "Swift" ```swift title="my_heap.swift" [class]{MaxHeap}-[func]{peek} ``` === "JS" ```javascript title="my_heap.js" [class]{MaxHeap}-[func]{peek} ``` === "TS" ```typescript title="my_heap.ts" [class]{MaxHeap}-[func]{peek} ``` === "Dart" ```dart title="my_heap.dart" [class]{MaxHeap}-[func]{peek} ``` === "Rust" ```rust title="my_heap.rs" [class]{MaxHeap}-[func]{peek} ``` === "C" ```c title="my_heap.c" [class]{MaxHeap}-[func]{peek} ``` === "Kotlin" ```kotlin title="my_heap.kt" [class]{MaxHeap}-[func]{peek} ``` === "Ruby" ```ruby title="my_heap.rb" [class]{MaxHeap}-[func]{peek} ``` ### 3. ヒープへの要素挿入 要素`val`が与えられた場合、まずそれをヒープの底に追加します。追加後、`val`がヒープ内の他の要素より大きい可能性があるため、ヒープの完全性が損なわれる可能性があります。**したがって、挿入されたノードから根ノードまでのパスを修復する必要があります**。この操作はヒープ化と呼ばれます。 挿入されたノードから開始して、**下から上にヒープ化を実行します**。下図に示すように、挿入されたノードの値をその親ノードと比較し、挿入されたノードが大きい場合はそれらを交換します。次にこの操作を続行し、根に到達するか、交換が不要なノードに遭遇するまで、下から上にヒープ内の各ノードを修復します。 === "<1>" { class="animation-figure" } === "<2>" { class="animation-figure" } === "<3>" { class="animation-figure" } === "<4>" { class="animation-figure" } === "<5>" { class="animation-figure" } === "<6>" { class="animation-figure" } === "<7>" { class="animation-figure" } === "<8>" { class="animation-figure" } === "<9>" { class="animation-figure" }図 8-3 ヒープへの要素挿入の手順
総ノード数を$n$とすると、木の高さは$O(\log n)$です。したがって、ヒープ化操作のループ反復回数は最大$O(\log n)$で、**要素挿入操作の時間計算量は$O(\log n)$になります**。コードは以下の通りです: === "Python" ```python title="my_heap.py" def push(self, val: int): """ヒープに要素をプッシュ""" # ノードを追加 self.max_heap.append(val) # 下から上へヒープ化 self.sift_up(self.size() - 1) def sift_up(self, i: int): """ノードiから開始して、下から上へヒープ化""" while True: # ノードiの親ノードを取得 p = self.parent(i) # 「ルートノードを越える」または「ノードが修復不要」の場合、ヒープ化を終了 if p < 0 or self.max_heap[i] <= self.max_heap[p]: break # 2つのノードを交換 self.swap(i, p) # 上向きのループヒープ化 i = p ``` === "C++" ```cpp title="my_heap.cpp" /* ヒープに要素をプッシュ */ void push(int val) { // ノードを追加 maxHeap.push_back(val); // 下から上へヒープ化 siftUp(size() - 1); } /* ノードiから上向きにヒープ化を開始 */ void siftUp(int i) { while (true) { // ノードiの親ノードを取得 int p = parent(i); // 「ルートノードを超える」または「ノードが修復不要」の場合、ヒープ化を終了 if (p < 0 || maxHeap[i] <= maxHeap[p]) break; // 2つのノードを交換 swap(maxHeap[i], maxHeap[p]); // 上向きにループしてヒープ化 i = p; } } ``` === "Java" ```java title="my_heap.java" /* 要素をヒープにプッシュ */ void push(int val) { // ノードを追加 maxHeap.add(val); // 下から上へヒープ化 siftUp(size() - 1); } /* ノード i から上向きにヒープ化を開始 */ void siftUp(int i) { while (true) { // ノード i の親ノードを取得 int p = parent(i); // 「根ノードを越える」または「ノードが修復不要」の場合、ヒープ化を終了 if (p < 0 || maxHeap.get(i) <= maxHeap.get(p)) break; // 2つのノードを交換 swap(i, p); // 上向きにヒープ化をループ i = p; } } ``` === "C#" ```csharp title="my_heap.cs" [class]{MaxHeap}-[func]{Push} [class]{MaxHeap}-[func]{SiftUp} ``` === "Go" ```go title="my_heap.go" [class]{maxHeap}-[func]{push} [class]{maxHeap}-[func]{siftUp} ``` === "Swift" ```swift title="my_heap.swift" [class]{MaxHeap}-[func]{push} [class]{MaxHeap}-[func]{siftUp} ``` === "JS" ```javascript title="my_heap.js" [class]{MaxHeap}-[func]{push} [class]{MaxHeap}-[func]{siftUp} ``` === "TS" ```typescript title="my_heap.ts" [class]{MaxHeap}-[func]{push} [class]{MaxHeap}-[func]{siftUp} ``` === "Dart" ```dart title="my_heap.dart" [class]{MaxHeap}-[func]{push} [class]{MaxHeap}-[func]{siftUp} ``` === "Rust" ```rust title="my_heap.rs" [class]{MaxHeap}-[func]{push} [class]{MaxHeap}-[func]{sift_up} ``` === "C" ```c title="my_heap.c" [class]{MaxHeap}-[func]{push} [class]{MaxHeap}-[func]{siftUp} ``` === "Kotlin" ```kotlin title="my_heap.kt" [class]{MaxHeap}-[func]{push} [class]{MaxHeap}-[func]{siftUp} ``` === "Ruby" ```ruby title="my_heap.rb" [class]{MaxHeap}-[func]{push} [class]{MaxHeap}-[func]{sift_up} ``` ### 4. ヒープからの先頭要素削除 ヒープの先頭要素は二分木の根ノード、つまりリストの最初の要素です。リストから最初の要素を直接削除すると、二分木内のすべてのノードインデックスが変更され、後続の修復にヒープ化を使用することが困難になります。要素インデックスの変更を最小限に抑えるため、次の手順を使用します。 1. ヒープの先頭要素と底の要素を交換します(根ノードと最も右の葉ノードを交換)。 2. 交換後、リストからヒープの底を削除します(交換されているため、実際には元の先頭要素が削除される)。 3. 根ノードから開始して、**上から下にヒープ化を実行します**。 下図に示すように、**「上から下のヒープ化」の方向は「下から上のヒープ化」と反対です**。根ノードの値をその2つの子と比較し、最大の子と交換します。次に、葉ノードに到達するか、交換が不要なノードに遭遇するまで、この操作を繰り返します。 === "<1>" { class="animation-figure" } === "<2>" { class="animation-figure" } === "<3>" { class="animation-figure" } === "<4>" { class="animation-figure" } === "<5>" { class="animation-figure" } === "<6>" { class="animation-figure" } === "<7>" { class="animation-figure" } === "<8>" { class="animation-figure" } === "<9>" { class="animation-figure" } === "<10>" { class="animation-figure" }図 8-4 ヒープからの先頭要素削除の手順
要素挿入操作と同様に、先頭要素削除操作の時間計算量も$O(\log n)$です。コードは以下の通りです: === "Python" ```python title="my_heap.py" def pop(self) -> int: """要素をヒープから出す""" # 空の処理 if self.is_empty(): raise IndexError("Heap is empty") # ルートノードと最右端の葉ノードを交換(最初の要素と最後の要素を交換) self.swap(0, self.size() - 1) # ノードを削除 val = self.max_heap.pop() # 上から下へヒープ化 self.sift_down(0) # ヒープの先頭要素を返す return val def sift_down(self, i: int): """ノードiから開始して、上から下へヒープ化""" while True: # i、l、rの中で最大のノードを決定し、maとする l, r, ma = self.left(i), self.right(i), i if l < self.size() and self.max_heap[l] > self.max_heap[ma]: ma = l if r < self.size() and self.max_heap[r] > self.max_heap[ma]: ma = r # ノードiが最大またはインデックスl、rが範囲外の場合、さらなるヒープ化は不要、ブレーク if ma == i: break # 2つのノードを交換 self.swap(i, ma) # 下向きのループヒープ化 i = ma ``` === "C++" ```cpp title="my_heap.cpp" /* 要素がヒープから退出 */ void pop() { // 空の処理 if (isEmpty()) { throw out_of_range("Heap is empty"); } // ルートノードを最も右の葉ノードと交換(最初の要素と最後の要素を交換) swap(maxHeap[0], maxHeap[size() - 1]); // ノードを削除 maxHeap.pop_back(); // 上から下へヒープ化 siftDown(0); } /* ノードiから下向きにヒープ化を開始 */ void siftDown(int i) { while (true) { // i、l、rの中で最大のノードを決定し、maとして記録 int l = left(i), r = right(i), ma = i; if (l < size() && maxHeap[l] > maxHeap[ma]) ma = l; if (r < size() && maxHeap[r] > maxHeap[ma]) ma = r; // ノードiが最大、またはインデックスl、rが範囲外の場合、これ以上のヒープ化は不要、ブレーク if (ma == i) break; swap(maxHeap[i], maxHeap[ma]); // 下向きにループしてヒープ化 i = ma; } } ``` === "Java" ```java title="my_heap.java" /* 要素がヒープから退出 */ int pop() { // 空の処理 if (isEmpty()) throw new IndexOutOfBoundsException(); // 根ノードを最も右の葉ノードと交換(最初の要素を最後の要素と交換) swap(0, size() - 1); // ノードを削除 int val = maxHeap.remove(size() - 1); // 上から下へヒープ化 siftDown(0); // ヒープの先頭要素を返す return val; } /* ノード i から下向きにヒープ化を開始 */ void siftDown(int i) { while (true) { // i、l、r の中で最大のノードを決定し、ma とする int l = left(i), r = right(i), ma = i; if (l < size() && maxHeap.get(l) > maxHeap.get(ma)) ma = l; if (r < size() && maxHeap.get(r) > maxHeap.get(ma)) ma = r; // ノード i が最大の場合、またはインデックス l、r が範囲外の場合、さらなるヒープ化は不要、終了 if (ma == i) break; // 2つのノードを交換 swap(i, ma); // 下向きにヒープ化をループ i = ma; } } ``` === "C#" ```csharp title="my_heap.cs" [class]{MaxHeap}-[func]{Pop} [class]{MaxHeap}-[func]{SiftDown} ``` === "Go" ```go title="my_heap.go" [class]{maxHeap}-[func]{pop} [class]{maxHeap}-[func]{siftDown} ``` === "Swift" ```swift title="my_heap.swift" [class]{MaxHeap}-[func]{pop} [class]{MaxHeap}-[func]{siftDown} ``` === "JS" ```javascript title="my_heap.js" [class]{MaxHeap}-[func]{pop} [class]{MaxHeap}-[func]{siftDown} ``` === "TS" ```typescript title="my_heap.ts" [class]{MaxHeap}-[func]{pop} [class]{MaxHeap}-[func]{siftDown} ``` === "Dart" ```dart title="my_heap.dart" [class]{MaxHeap}-[func]{pop} [class]{MaxHeap}-[func]{siftDown} ``` === "Rust" ```rust title="my_heap.rs" [class]{MaxHeap}-[func]{pop} [class]{MaxHeap}-[func]{sift_down} ``` === "C" ```c title="my_heap.c" [class]{MaxHeap}-[func]{pop} [class]{MaxHeap}-[func]{siftDown} ``` === "Kotlin" ```kotlin title="my_heap.kt" [class]{MaxHeap}-[func]{pop} [class]{MaxHeap}-[func]{siftDown} ``` === "Ruby" ```ruby title="my_heap.rb" [class]{MaxHeap}-[func]{pop} [class]{MaxHeap}-[func]{sift_down} ``` ## 8.1.3 ヒープの一般的な応用 - **優先度キュー**:ヒープは優先度キューを実装するための好ましいデータ構造で、エンキュー操作とデキュー操作の両方の時間計算量が$O(\log n)$、キュー構築の時間計算量が$O(n)$で、すべて非常に効率的です。 - **ヒープソート**:データセットが与えられた場合、それらからヒープを作成し、次に要素削除操作を継続的に実行して順序付けされたデータを取得できます。ただし、ヒープソートを実装するより洗練された方法があり、「ヒープソート」の章で説明されています。 - **最大$k$個の要素の発見**:これは古典的なアルゴリズム問題であり、一般的な使用例でもあります。Weiboホット検索のトップ10ホットニュースの選択や、トップ10の売れ筋商品の選択などです。