docs: add Japanese translate documents (#1812)
* docs: add Japanese documents (`ja/docs`) * docs: add Japanese documents (`ja/codes`) * docs: add Japanese documents * Remove pythontutor blocks in ja/ * Add an empty at the end of each markdown file. * Add the missing figures (use the English version temporarily). * Add index.md for Japanese version. * Add index.html for Japanese version. * Add missing index.assets * Fix backtracking_algorithm.md for Japanese version. * Add avatar_eltociear.jpg. Fix image links on the Japanese landing page. * Add the Japanese banner. --------- Co-authored-by: krahets <krahets@163.com>
|
After Width: | Height: | Size: 20 KiB |
@@ -0,0 +1,74 @@
|
||||
# ヒープ構築操作
|
||||
|
||||
場合によっては、リストのすべての要素を使用してヒープを構築したいことがあり、このプロセスは「ヒープ構築操作」として知られています。
|
||||
|
||||
## ヒープ挿入操作による実装
|
||||
|
||||
まず、空のヒープを作成し、次にリストを反復処理して、各要素に対して順番に「ヒープ挿入操作」を実行します。これは、要素をヒープの末尾に追加し、次に下から上に「ヒープ化」することを意味します。
|
||||
|
||||
ヒープに要素が追加されるたびに、ヒープの長さは1つずつ増加します。ノードは二分木に上から下に追加されるため、ヒープは「上から下に」構築されます。
|
||||
|
||||
要素数を$n$とすると、各要素の挿入操作は$O(\log{n})$時間かかるため、このヒープ構築方法の時間計算量は$O(n \log n)$です。
|
||||
|
||||
## 走査によるヒープ化の実装
|
||||
|
||||
実際には、2つのステップでより効率的なヒープ構築方法を実装できます。
|
||||
|
||||
1. リストのすべての要素をそのままヒープに追加します。この時点では、ヒープの性質はまだ満たされていません。
|
||||
2. ヒープを逆順(レベル順走査の逆)で走査し、各非葉ノードに対して「上から下のヒープ化」を実行します。
|
||||
|
||||
**ノードをヒープ化した後、そのノードを根とする部分木は有効な部分ヒープになります**。走査が逆順であるため、ヒープは「下から上に」構築されます。
|
||||
|
||||
逆走査を選択する理由は、現在のノードの下の部分木がすでに有効な部分ヒープであることを保証し、現在のノードのヒープ化を効果的にするためです。
|
||||
|
||||
言及する価値があるのは、**葉ノードは子を持たないため、自然に有効な部分ヒープを形成し、ヒープ化する必要がない**ということです。以下のコードに示すように、最後の非葉ノードは最後のノードの親です。そこから開始して逆順に走査してヒープ化を実行します:
|
||||
|
||||
```src
|
||||
[file]{my_heap}-[class]{max_heap}-[func]{__init__}
|
||||
```
|
||||
|
||||
## 計算量分析
|
||||
|
||||
次に、この第2のヒープ構築方法の時間計算量を計算してみましょう。
|
||||
|
||||
- 完備二分木のノード数を$n$と仮定すると、葉ノードの数は$(n + 1) / 2$です。ここで$/$ は整数除算です。したがって、ヒープ化が必要なノードの数は$(n - 1) / 2$です。
|
||||
- 「上から下のヒープ化」のプロセスでは、各ノードは最大で葉ノードまでヒープ化されるため、最大反復回数は二分木の高さ$\log n$です。
|
||||
|
||||
この2つを掛け合わせると、ヒープ構築プロセスの時間計算量は$O(n \log n)$となります。**しかし、この推定は正確ではありません。二分木の下位レベルには上位よりもはるかに多くのノードがあるという性質を考慮していないからです。**
|
||||
|
||||
より正確な計算を行いましょう。計算を簡素化するため、$n$個のノードと高さ$h$を持つ「完全二分木」を仮定します。この仮定は結果の正確性に影響しません。
|
||||
|
||||

|
||||
|
||||
上図に示すように、ノードが「上から下にヒープ化される」最大反復回数は、そのノードから葉ノードまでの距離と等しく、これは正確に「ノードの高さ」です。したがって、各レベルで「ノード数×ノードの高さ」を合計して、**すべてのノードの総ヒープ化反復回数を得る**ことができます。
|
||||
|
||||
$$
|
||||
T(h) = 2^0h + 2^1(h-1) + 2^2(h-2) + \dots + 2^{(h-1)}\times1
|
||||
$$
|
||||
|
||||
上記の方程式を簡素化するために、高校の数列の知識を使用する必要があります。まず$T(h)$に$2$を掛けて以下を得ます:
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
T(h) & = 2^0h + 2^1(h-1) + 2^2(h-2) + \dots + 2^{h-1}\times1 \newline
|
||||
2T(h) & = 2^1h + 2^2(h-1) + 2^3(h-2) + \dots + 2^h\times1 \newline
|
||||
\end{aligned}
|
||||
$$
|
||||
|
||||
変位法を使用して$2T(h)$から$T(h)$を減算すると、以下を得ます:
|
||||
|
||||
$$
|
||||
2T(h) - T(h) = T(h) = -2^0h + 2^1 + 2^2 + \dots + 2^{h-1} + 2^h
|
||||
$$
|
||||
|
||||
方程式を観察すると、$T(h)$は等比数列であり、和の公式を使用して直接計算でき、時間計算量は以下になります:
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
T(h) & = 2 \frac{1 - 2^h}{1 - 2} - h \newline
|
||||
& = 2^{h+1} - h - 2 \newline
|
||||
& = O(2^h)
|
||||
\end{aligned}
|
||||
$$
|
||||
|
||||
さらに、高さ$h$の完全二分木は$n = 2^{h+1} - 1$個のノードを持つため、計算量は$O(2^h) = O(n)$です。この計算は、**リストを入力してヒープを構築する時間計算量が$O(n)$であり、非常に効率的である**ことを示しています。
|
||||
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 39 KiB |
@@ -0,0 +1,534 @@
|
||||
# ヒープ
|
||||
|
||||
<u>ヒープ</u>は特定の条件を満たす完備二分木で、主に次の2つのタイプに分類されます(下図参照)。
|
||||
|
||||
- <u>最小ヒープ</u>:任意のノードの値 $\leq$ その子ノードの値。
|
||||
- <u>最大ヒープ</u>:任意のノードの値 $\geq$ その子ノードの値。
|
||||
|
||||

|
||||
|
||||
完備二分木の特別なケースとして、ヒープには以下の特性があります:
|
||||
|
||||
- 最下位層のノードは左から右に埋められ、他の層のノードは完全に埋められています。
|
||||
- 二分木の根ノードをヒープの「先頭」と呼び、最も右下のノードをヒープの「末尾」と呼びます。
|
||||
- 最大ヒープ(最小ヒープ)の場合、先頭要素(根)の値はすべての要素の中で最大(最小)です。
|
||||
|
||||
## ヒープの一般的な操作
|
||||
|
||||
多くのプログラミング言語が<u>優先度キュー</u>を提供していることに注意してください。これは優先度付きソートを持つキューとして定義される抽象データ構造です。
|
||||
|
||||
実際には、**ヒープは優先度キューを実装するためによく使用されます。最大ヒープは、要素が降順でデキューされる優先度キューに対応します**。使用の観点から、「優先度キュー」と「ヒープ」を同等のデータ構造と考えることができます。したがって、この本では両者を特別に区別せず、統一して「ヒープ」と呼びます。
|
||||
|
||||
ヒープの一般的な操作を下表に示します。メソッド名はプログラミング言語によって異なる場合があります。
|
||||
|
||||
<p align="center"> 表 <id> ヒープ操作の効率 </p>
|
||||
|
||||
| メソッド名 | 説明 | 時間計算量 |
|
||||
| ----------- | ------------------------------------------------- | ----------- |
|
||||
| `push()` | ヒープに要素を追加 | $O(\log n)$ |
|
||||
| `pop()` | ヒープから先頭要素を削除 | $O(\log n)$ |
|
||||
| `peek()` | 先頭要素にアクセス(最大/最小ヒープの場合、最大/最小値) | $O(1)$ |
|
||||
| `size()` | ヒープ内の要素数を取得 | $O(1)$ |
|
||||
| `isEmpty()` | ヒープが空かどうかをチェック | $O(1)$ |
|
||||
|
||||
実際には、プログラミング言語によって提供されるヒープクラス(または優先度キュークラス)を直接使用できます。
|
||||
|
||||
ソートアルゴリズムで「昇順」と「降順」があるように、`flag`を設定するか`Comparator`を変更することで「最小ヒープ」と「最大ヒープ」を切り替えることができます。コードは以下の通りです:
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python title="heap.py"
|
||||
# 最小ヒープの初期化
|
||||
min_heap, flag = [], 1
|
||||
# 最大ヒープの初期化
|
||||
max_heap, flag = [], -1
|
||||
|
||||
# Pythonのheapqモジュールはデフォルトで最小ヒープを実装
|
||||
# 要素をヒープにプッシュする前に負の値にすることで、順序を反転させ、最大ヒープを実装
|
||||
# この例では、flag = 1は最小ヒープに対応し、flag = -1は最大ヒープに対応
|
||||
|
||||
# ヒープに要素をプッシュ
|
||||
heapq.heappush(max_heap, flag * 1)
|
||||
heapq.heappush(max_heap, flag * 3)
|
||||
heapq.heappush(max_heap, flag * 2)
|
||||
heapq.heappush(max_heap, flag * 5)
|
||||
heapq.heappush(max_heap, flag * 4)
|
||||
|
||||
# ヒープの先頭要素を取得
|
||||
peek: int = flag * max_heap[0] # 5
|
||||
|
||||
# ヒープの先頭要素をポップ
|
||||
# ポップされた要素は降順のシーケンスを形成
|
||||
val = flag * heapq.heappop(max_heap) # 5
|
||||
val = flag * heapq.heappop(max_heap) # 4
|
||||
val = flag * heapq.heappop(max_heap) # 3
|
||||
val = flag * heapq.heappop(max_heap) # 2
|
||||
val = flag * heapq.heappop(max_heap) # 1
|
||||
|
||||
# ヒープのサイズを取得
|
||||
size: int = len(max_heap)
|
||||
|
||||
# ヒープが空かどうかをチェック
|
||||
is_empty: bool = not max_heap
|
||||
|
||||
# リストからヒープを作成
|
||||
min_heap: list[int] = [1, 3, 2, 5, 4]
|
||||
heapq.heapify(min_heap)
|
||||
```
|
||||
|
||||
=== "C++"
|
||||
|
||||
```cpp title="heap.cpp"
|
||||
/* ヒープの初期化 */
|
||||
// 最小ヒープの初期化
|
||||
priority_queue<int, vector<int>, greater<int>> minHeap;
|
||||
// 最大ヒープの初期化
|
||||
priority_queue<int, vector<int>, less<int>> maxHeap;
|
||||
|
||||
/* ヒープに要素をプッシュ */
|
||||
maxHeap.push(1);
|
||||
maxHeap.push(3);
|
||||
maxHeap.push(2);
|
||||
maxHeap.push(5);
|
||||
maxHeap.push(4);
|
||||
|
||||
/* ヒープの先頭要素を取得 */
|
||||
int peek = maxHeap.top(); // 5
|
||||
|
||||
/* ヒープの先頭要素をポップ */
|
||||
// ポップされた要素は降順のシーケンスを形成
|
||||
maxHeap.pop(); // 5
|
||||
maxHeap.pop(); // 4
|
||||
maxHeap.pop(); // 3
|
||||
maxHeap.pop(); // 2
|
||||
maxHeap.pop(); // 1
|
||||
|
||||
/* ヒープのサイズを取得 */
|
||||
int size = maxHeap.size();
|
||||
|
||||
/* ヒープが空かどうかをチェック */
|
||||
bool isEmpty = maxHeap.empty();
|
||||
|
||||
/* リストからヒープを作成 */
|
||||
vector<int> input{1, 3, 2, 5, 4};
|
||||
priority_queue<int, vector<int>, greater<int>> minHeap(input.begin(), input.end());
|
||||
```
|
||||
|
||||
=== "Java"
|
||||
|
||||
```java title="heap.java"
|
||||
/* ヒープの初期化 */
|
||||
// 最小ヒープの初期化
|
||||
Queue<Integer> minHeap = new PriorityQueue<>();
|
||||
// 最大ヒープの初期化(ラムダ式でComparatorを変更するだけ)
|
||||
Queue<Integer> maxHeap = new PriorityQueue<>((a, b) -> b - a);
|
||||
|
||||
/* ヒープに要素をプッシュ */
|
||||
maxHeap.offer(1);
|
||||
maxHeap.offer(3);
|
||||
maxHeap.offer(2);
|
||||
maxHeap.offer(5);
|
||||
maxHeap.offer(4);
|
||||
|
||||
/* ヒープの先頭要素を取得 */
|
||||
int peek = maxHeap.peek(); // 5
|
||||
|
||||
/* ヒープの先頭要素をポップ */
|
||||
// ポップされた要素は降順のシーケンスを形成
|
||||
peek = maxHeap.poll(); // 5
|
||||
peek = maxHeap.poll(); // 4
|
||||
peek = maxHeap.poll(); // 3
|
||||
peek = maxHeap.poll(); // 2
|
||||
peek = maxHeap.poll(); // 1
|
||||
|
||||
/* ヒープのサイズを取得 */
|
||||
int size = maxHeap.size();
|
||||
|
||||
/* ヒープが空かどうかをチェック */
|
||||
boolean isEmpty = maxHeap.isEmpty();
|
||||
|
||||
/* リストからヒープを作成 */
|
||||
minHeap = new PriorityQueue<>(Arrays.asList(1, 3, 2, 5, 4));
|
||||
```
|
||||
|
||||
=== "C#"
|
||||
|
||||
```csharp title="heap.cs"
|
||||
/* ヒープの初期化 */
|
||||
// 最小ヒープの初期化
|
||||
PriorityQueue<int, int> minHeap = new();
|
||||
// 最大ヒープの初期化(ラムダ式でComparatorを変更するだけ)
|
||||
PriorityQueue<int, int> maxHeap = new(Comparer<int>.Create((x, y) => y - x));
|
||||
|
||||
/* ヒープに要素をプッシュ */
|
||||
maxHeap.Enqueue(1, 1);
|
||||
maxHeap.Enqueue(3, 3);
|
||||
maxHeap.Enqueue(2, 2);
|
||||
maxHeap.Enqueue(5, 5);
|
||||
maxHeap.Enqueue(4, 4);
|
||||
|
||||
/* ヒープの先頭要素を取得 */
|
||||
int peek = maxHeap.Peek();//5
|
||||
|
||||
/* ヒープの先頭要素をポップ */
|
||||
// ポップされた要素は降順のシーケンスを形成
|
||||
peek = maxHeap.Dequeue(); // 5
|
||||
peek = maxHeap.Dequeue(); // 4
|
||||
peek = maxHeap.Dequeue(); // 3
|
||||
peek = maxHeap.Dequeue(); // 2
|
||||
peek = maxHeap.Dequeue(); // 1
|
||||
|
||||
/* ヒープのサイズを取得 */
|
||||
int size = maxHeap.Count;
|
||||
|
||||
/* ヒープが空かどうかをチェック */
|
||||
bool isEmpty = maxHeap.Count == 0;
|
||||
|
||||
/* リストからヒープを作成 */
|
||||
minHeap = new PriorityQueue<int, int>([(1, 1), (3, 3), (2, 2), (5, 5), (4, 4)]);
|
||||
```
|
||||
|
||||
=== "Go"
|
||||
|
||||
```go title="heap.go"
|
||||
// Goでは、heap.Interfaceを実装することで整数の最大ヒープを構築できます
|
||||
// heap.Interfaceを実装するには、sort.Interfaceも実装する必要があります
|
||||
type intHeap []any
|
||||
|
||||
// heap.InterfaceのPushメソッド、要素をヒープにプッシュ
|
||||
func (h *intHeap) Push(x any) {
|
||||
// PushとPopの両方でポインタレシーバーを使用
|
||||
// スライスの要素を調整するだけでなく、その長さも変更するため
|
||||
*h = append(*h, x.(int))
|
||||
}
|
||||
|
||||
// heap.InterfaceのPopメソッド、ヒープの先頭要素を削除
|
||||
func (h *intHeap) Pop() any {
|
||||
// ヒープからポップする要素は末尾に格納
|
||||
last := (*h)[len(*h)-1]
|
||||
*h = (*h)[:len(*h)-1]
|
||||
return last
|
||||
}
|
||||
|
||||
// sort.InterfaceのLenメソッド
|
||||
func (h *intHeap) Len() int {
|
||||
return len(*h)
|
||||
}
|
||||
|
||||
// sort.InterfaceのLessメソッド
|
||||
func (h *intHeap) Less(i, j int) bool {
|
||||
// 最小ヒープを実装したい場合は、これを小なり比較に変更
|
||||
return (*h)[i].(int) > (*h)[j].(int)
|
||||
}
|
||||
|
||||
// sort.InterfaceのSwapメソッド
|
||||
func (h *intHeap) Swap(i, j int) {
|
||||
(*h)[i], (*h)[j] = (*h)[j], (*h)[i]
|
||||
}
|
||||
|
||||
// Top ヒープの先頭要素を取得
|
||||
func (h *intHeap) Top() any {
|
||||
return (*h)[0]
|
||||
}
|
||||
|
||||
/* ドライバーコード */
|
||||
func TestHeap(t *testing.T) {
|
||||
/* ヒープの初期化 */
|
||||
// 最大ヒープの初期化
|
||||
maxHeap := &intHeap{}
|
||||
heap.Init(maxHeap)
|
||||
/* ヒープに要素をプッシュ */
|
||||
// heap.Interfaceのメソッドを呼び出して要素を追加
|
||||
heap.Push(maxHeap, 1)
|
||||
heap.Push(maxHeap, 3)
|
||||
heap.Push(maxHeap, 2)
|
||||
heap.Push(maxHeap, 4)
|
||||
heap.Push(maxHeap, 5)
|
||||
|
||||
/* ヒープの先頭要素を取得 */
|
||||
top := maxHeap.Top()
|
||||
fmt.Printf("ヒープの先頭要素は %d\n", top)
|
||||
|
||||
/* ヒープの先頭要素をポップ */
|
||||
// heap.Interfaceのメソッドを呼び出して要素を削除
|
||||
heap.Pop(maxHeap) // 5
|
||||
heap.Pop(maxHeap) // 4
|
||||
heap.Pop(maxHeap) // 3
|
||||
heap.Pop(maxHeap) // 2
|
||||
heap.Pop(maxHeap) // 1
|
||||
|
||||
/* ヒープのサイズを取得 */
|
||||
size := len(*maxHeap)
|
||||
fmt.Printf("ヒープ内の要素数は %d\n", size)
|
||||
|
||||
/* ヒープが空かどうかをチェック */
|
||||
isEmpty := len(*maxHeap) == 0
|
||||
fmt.Printf("ヒープは空ですか? %t\n", isEmpty)
|
||||
}
|
||||
```
|
||||
|
||||
=== "Swift"
|
||||
|
||||
```swift title="heap.swift"
|
||||
/* ヒープの初期化 */
|
||||
// SwiftのHeap型は最大ヒープと最小ヒープの両方をサポートし、swift-collectionsライブラリが必要
|
||||
var heap = Heap<Int>()
|
||||
|
||||
/* ヒープに要素をプッシュ */
|
||||
heap.insert(1)
|
||||
heap.insert(3)
|
||||
heap.insert(2)
|
||||
heap.insert(5)
|
||||
heap.insert(4)
|
||||
|
||||
/* ヒープの先頭要素を取得 */
|
||||
var peek = heap.max()!
|
||||
|
||||
/* ヒープの先頭要素をポップ */
|
||||
peek = heap.removeMax() // 5
|
||||
peek = heap.removeMax() // 4
|
||||
peek = heap.removeMax() // 3
|
||||
peek = heap.removeMax() // 2
|
||||
peek = heap.removeMax() // 1
|
||||
|
||||
/* ヒープのサイズを取得 */
|
||||
let size = heap.count
|
||||
|
||||
/* ヒープが空かどうかをチェック */
|
||||
let isEmpty = heap.isEmpty
|
||||
|
||||
/* リストからヒープを作成 */
|
||||
let heap2 = Heap([1, 3, 2, 5, 4])
|
||||
```
|
||||
|
||||
=== "JS"
|
||||
|
||||
```javascript title="heap.js"
|
||||
// JavaScriptは組み込みのHeapクラスを提供していません
|
||||
```
|
||||
|
||||
=== "TS"
|
||||
|
||||
```typescript title="heap.ts"
|
||||
// TypeScriptは組み込みのHeapクラスを提供していません
|
||||
```
|
||||
|
||||
=== "Dart"
|
||||
|
||||
```dart title="heap.dart"
|
||||
// Dartは組み込みのHeapクラスを提供していません
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="heap.rs"
|
||||
use std::collections::BinaryHeap;
|
||||
use std::cmp::Reverse;
|
||||
|
||||
/* ヒープの初期化 */
|
||||
// 最小ヒープの初期化
|
||||
let mut min_heap = BinaryHeap::<Reverse<i32>>::new();
|
||||
// 最大ヒープの初期化
|
||||
let mut max_heap = BinaryHeap::new();
|
||||
|
||||
/* ヒープに要素をプッシュ */
|
||||
max_heap.push(1);
|
||||
max_heap.push(3);
|
||||
max_heap.push(2);
|
||||
max_heap.push(5);
|
||||
max_heap.push(4);
|
||||
|
||||
/* ヒープの先頭要素を取得 */
|
||||
let peek = max_heap.peek().unwrap(); // 5
|
||||
|
||||
/* ヒープの先頭要素をポップ */
|
||||
// ポップされた要素は降順のシーケンスを形成
|
||||
let peek = max_heap.pop().unwrap(); // 5
|
||||
let peek = max_heap.pop().unwrap(); // 4
|
||||
let peek = max_heap.pop().unwrap(); // 3
|
||||
let peek = max_heap.pop().unwrap(); // 2
|
||||
let peek = max_heap.pop().unwrap(); // 1
|
||||
|
||||
/* ヒープのサイズを取得 */
|
||||
let size = max_heap.len();
|
||||
|
||||
/* ヒープが空かどうかをチェック */
|
||||
let is_empty = max_heap.is_empty();
|
||||
|
||||
/* リストからヒープを作成 */
|
||||
let min_heap = BinaryHeap::from(vec![Reverse(1), Reverse(3), Reverse(2), Reverse(5), Reverse(4)]);
|
||||
```
|
||||
|
||||
=== "C"
|
||||
|
||||
```c title="heap.c"
|
||||
// Cは組み込みのHeapクラスを提供していません
|
||||
```
|
||||
|
||||
=== "Kotlin"
|
||||
|
||||
```kotlin title="heap.kt"
|
||||
/* ヒープの初期化 */
|
||||
// 最小ヒープの初期化
|
||||
var minHeap = PriorityQueue<Int>()
|
||||
// 最大ヒープの初期化(ラムダ式でComparatorを変更するだけ)
|
||||
val maxHeap = PriorityQueue { a: Int, b: Int -> b - a }
|
||||
|
||||
/* ヒープに要素をプッシュ */
|
||||
maxHeap.offer(1)
|
||||
maxHeap.offer(3)
|
||||
maxHeap.offer(2)
|
||||
maxHeap.offer(5)
|
||||
maxHeap.offer(4)
|
||||
|
||||
/* ヒープの先頭要素を取得 */
|
||||
var peek = maxHeap.peek() // 5
|
||||
|
||||
/* ヒープの先頭要素をポップ */
|
||||
// ポップされた要素は降順のシーケンスを形成
|
||||
peek = maxHeap.poll() // 5
|
||||
peek = maxHeap.poll() // 4
|
||||
peek = maxHeap.poll() // 3
|
||||
peek = maxHeap.poll() // 2
|
||||
peek = maxHeap.poll() // 1
|
||||
|
||||
/* ヒープのサイズを取得 */
|
||||
val size = maxHeap.size
|
||||
|
||||
/* ヒープが空かどうかをチェック */
|
||||
val isEmpty = maxHeap.isEmpty()
|
||||
|
||||
/* リストからヒープを作成 */
|
||||
minHeap = PriorityQueue(mutableListOf(1, 3, 2, 5, 4))
|
||||
```
|
||||
|
||||
=== "Ruby"
|
||||
|
||||
```ruby title="heap.rb"
|
||||
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="heap.zig"
|
||||
|
||||
```
|
||||
|
||||
## ヒープの実装
|
||||
|
||||
以下の実装は最大ヒープです。最小ヒープに変換するには、すべてのサイズ論理比較を反転させるだけです(例えば、$\geq$を$\leq$に置き換える)。興味のある読者は自分で実装することをお勧めします。
|
||||
|
||||
### ヒープの格納と表現
|
||||
|
||||
「二分木」の節で述べたように、完備二分木は配列表現に非常に適しています。ヒープは完備二分木の一種なので、**配列を使用してヒープを格納します**。
|
||||
|
||||
配列を使用して二分木を表現する場合、要素はノード値を表し、インデックスは二分木内のノード位置を表します。**ノードポインタはインデックスマッピング公式を通じて実装されます**。
|
||||
|
||||
下図に示すように、インデックス$i$が与えられた場合、その左の子のインデックスは$2i + 1$、右の子のインデックスは$2i + 2$、親のインデックスは$(i - 1) / 2$(床除算)です。インデックスが範囲外の場合、nullノードまたはノードが存在しないことを意味します。
|
||||
|
||||

|
||||
|
||||
後で便利に使用するため、インデックスマッピング公式を関数にカプセル化できます:
|
||||
|
||||
```src
|
||||
[file]{my_heap}-[class]{max_heap}-[func]{parent}
|
||||
```
|
||||
|
||||
### ヒープの先頭要素へのアクセス
|
||||
|
||||
ヒープの先頭要素は二分木の根ノードで、リストの最初の要素でもあります:
|
||||
|
||||
```src
|
||||
[file]{my_heap}-[class]{max_heap}-[func]{peek}
|
||||
```
|
||||
|
||||
### ヒープへの要素挿入
|
||||
|
||||
要素`val`が与えられた場合、まずそれをヒープの底に追加します。追加後、`val`がヒープ内の他の要素より大きい可能性があるため、ヒープの完全性が損なわれる可能性があります。**したがって、挿入されたノードから根ノードまでのパスを修復する必要があります**。この操作は<u>ヒープ化</u>と呼ばれます。
|
||||
|
||||
挿入されたノードから開始して、**下から上にヒープ化を実行します**。下図に示すように、挿入されたノードの値をその親ノードと比較し、挿入されたノードが大きい場合はそれらを交換します。次にこの操作を続行し、根に到達するか、交換が不要なノードに遭遇するまで、下から上にヒープ内の各ノードを修復します。
|
||||
|
||||
=== "<1>"
|
||||

|
||||
|
||||
=== "<2>"
|
||||

|
||||
|
||||
=== "<3>"
|
||||

|
||||
|
||||
=== "<4>"
|
||||

|
||||
|
||||
=== "<5>"
|
||||

|
||||
|
||||
=== "<6>"
|
||||

|
||||
|
||||
=== "<7>"
|
||||

|
||||
|
||||
=== "<8>"
|
||||

|
||||
|
||||
=== "<9>"
|
||||

|
||||
|
||||
総ノード数を$n$とすると、木の高さは$O(\log n)$です。したがって、ヒープ化操作のループ反復回数は最大$O(\log n)$で、**要素挿入操作の時間計算量は$O(\log n)$になります**。コードは以下の通りです:
|
||||
|
||||
```src
|
||||
[file]{my_heap}-[class]{max_heap}-[func]{sift_up}
|
||||
```
|
||||
|
||||
### ヒープからの先頭要素削除
|
||||
|
||||
ヒープの先頭要素は二分木の根ノード、つまりリストの最初の要素です。リストから最初の要素を直接削除すると、二分木内のすべてのノードインデックスが変更され、後続の修復にヒープ化を使用することが困難になります。要素インデックスの変更を最小限に抑えるため、次の手順を使用します。
|
||||
|
||||
1. ヒープの先頭要素と底の要素を交換します(根ノードと最も右の葉ノードを交換)。
|
||||
2. 交換後、リストからヒープの底を削除します(交換されているため、実際には元の先頭要素が削除される)。
|
||||
3. 根ノードから開始して、**上から下にヒープ化を実行します**。
|
||||
|
||||
下図に示すように、**「上から下のヒープ化」の方向は「下から上のヒープ化」と反対です**。根ノードの値をその2つの子と比較し、最大の子と交換します。次に、葉ノードに到達するか、交換が不要なノードに遭遇するまで、この操作を繰り返します。
|
||||
|
||||
=== "<1>"
|
||||

|
||||
|
||||
=== "<2>"
|
||||

|
||||
|
||||
=== "<3>"
|
||||

|
||||
|
||||
=== "<4>"
|
||||

|
||||
|
||||
=== "<5>"
|
||||

|
||||
|
||||
=== "<6>"
|
||||

|
||||
|
||||
=== "<7>"
|
||||

|
||||
|
||||
=== "<8>"
|
||||

|
||||
|
||||
=== "<9>"
|
||||

|
||||
|
||||
=== "<10>"
|
||||

|
||||
|
||||
要素挿入操作と同様に、先頭要素削除操作の時間計算量も$O(\log n)$です。コードは以下の通りです:
|
||||
|
||||
```src
|
||||
[file]{my_heap}-[class]{max_heap}-[func]{sift_down}
|
||||
```
|
||||
|
||||
## ヒープの一般的な応用
|
||||
|
||||
- **優先度キュー**:ヒープは優先度キューを実装するための好ましいデータ構造で、エンキュー操作とデキュー操作の両方の時間計算量が$O(\log n)$、キュー構築の時間計算量が$O(n)$で、すべて非常に効率的です。
|
||||
- **ヒープソート**:データセットが与えられた場合、それらからヒープを作成し、次に要素削除操作を継続的に実行して順序付けされたデータを取得できます。ただし、ヒープソートを実装するより洗練された方法があり、「ヒープソート」の章で説明されています。
|
||||
- **最大$k$個の要素の発見**:これは古典的なアルゴリズム問題であり、一般的な使用例でもあります。Weiboホット検索のトップ10ホットニュースの選択や、トップ10の売れ筋商品の選択などです。
|
||||
@@ -0,0 +1,9 @@
|
||||
# ヒープ
|
||||
|
||||

|
||||
|
||||
!!! abstract
|
||||
|
||||
ヒープは山とその険しい峰のように、層をなして起伏し、それぞれが独特の形を持っています。
|
||||
|
||||
各山の頂は散らばった高さで上下しますが、最も高いものが常に最初に注目を集めます。
|
||||
@@ -0,0 +1,17 @@
|
||||
# まとめ
|
||||
|
||||
### 重要な復習
|
||||
|
||||
- ヒープは完備二分木で、その構築性質に基づいて最大ヒープまたは最小ヒープに分類できます。最大ヒープの先頭要素は最大で、最小ヒープの先頭要素は最小です。
|
||||
- 優先度キューは、デキューの優先度を持つキューとして定義され、通常ヒープを使用して実装されます。
|
||||
- ヒープの一般的な操作とそれに対応する時間計算量には以下があります:ヒープへの要素挿入$O(\log n)$、ヒープからの先頭要素削除$O(\log n)$、ヒープの先頭要素へのアクセス$O(1)$。
|
||||
- 完備二分木は配列で表現するのに適しているため、ヒープは一般的に配列を使用して格納されます。
|
||||
- ヒープ化操作はヒープの性質を維持するために使用され、ヒープの挿入操作と削除操作の両方で使用されます。
|
||||
- $n$個の要素が入力として与えられた場合のヒープ構築の時間計算量は$O(n)$に最適化でき、これは非常に効率的です。
|
||||
- Top-kは古典的なアルゴリズム問題で、ヒープデータ構造を使用して効率的に解決でき、時間計算量は$O(n \log k)$です。
|
||||
|
||||
### Q & A
|
||||
|
||||
**Q**: データ構造の「ヒープ」とメモリ管理の「ヒープ」は同じ概念ですか?
|
||||
|
||||
この2つは、どちらも「ヒープ」と呼ばれますが、同じ概念ではありません。コンピュータシステムメモリのヒープは動的メモリ割り当ての一部で、プログラムが実行中にデータを格納するために使用できます。プログラムは、オブジェクトや配列などの複雑な構造を格納するために、一定量のヒープメモリを要求できます。割り当てられたデータが不要になったときは、メモリリークを防ぐためにプログラムがこのメモリを解放する必要があります。スタックメモリと比較して、ヒープメモリの管理と使用にはより多くの注意が必要で、不適切な使用はメモリリークやダングリングポインタにつながる可能性があります。
|
||||
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 22 KiB |
@@ -0,0 +1,73 @@
|
||||
# Top-k問題
|
||||
|
||||
!!! question
|
||||
|
||||
長さ$n$の順序付けられていない配列`nums`が与えられたとき、配列内の最大$k$個の要素を返してください。
|
||||
|
||||
この問題について、まず2つの直接的な解法を紹介し、次により効率的なヒープベースの方法を説明します。
|
||||
|
||||
## 方法1:反復選択
|
||||
|
||||
下図に示すように、$k$回の反復を実行し、各回で$1$番目、$2$番目、$\dots$、$k$番目に大きい要素を抽出できます。時間計算量は$O(nk)$です。
|
||||
|
||||
この方法は$k \ll n$の場合にのみ適しています。$k$が$n$に近い場合、時間計算量は$O(n^2)$に近づき、非常に時間がかかります。
|
||||
|
||||

|
||||
|
||||
!!! tip
|
||||
|
||||
$k = n$の場合、完全に順序付けられたシーケンスを得ることができ、これは「選択ソート」アルゴリズムと同等です。
|
||||
|
||||
## 方法2:ソート
|
||||
|
||||
下図に示すように、まず配列`nums`をソートし、次に最後の$k$個の要素を返すことができます。時間計算量は$O(n \log n)$です。
|
||||
|
||||
明らかに、この方法はタスクを「やりすぎ」ています。最大$k$個の要素を見つけるだけでよく、他の要素をソートする必要はありません。
|
||||
|
||||

|
||||
|
||||
## 方法3:ヒープ
|
||||
|
||||
以下のプロセスに示すように、ヒープに基づいてTop-k問題をより効率的に解決できます。
|
||||
|
||||
1. 最小ヒープを初期化します。先頭要素が最小になります。
|
||||
2. まず、配列の最初の$k$個の要素をヒープに挿入します。
|
||||
3. $k + 1$番目の要素から開始し、現在の要素がヒープの先頭要素より大きい場合、ヒープの先頭要素を削除し、現在の要素をヒープに挿入します。
|
||||
4. 走査を完了した後、ヒープには最大$k$個の要素が含まれています。
|
||||
|
||||
=== "<1>"
|
||||

|
||||
|
||||
=== "<2>"
|
||||

|
||||
|
||||
=== "<3>"
|
||||

|
||||
|
||||
=== "<4>"
|
||||

|
||||
|
||||
=== "<5>"
|
||||

|
||||
|
||||
=== "<6>"
|
||||

|
||||
|
||||
=== "<7>"
|
||||

|
||||
|
||||
=== "<8>"
|
||||

|
||||
|
||||
=== "<9>"
|
||||

|
||||
|
||||
サンプルコードは以下の通りです:
|
||||
|
||||
```src
|
||||
[file]{top_k}-[class]{}-[func]{top_k_heap}
|
||||
```
|
||||
|
||||
合計$n$回のヒープ挿入と削除が実行され、最大ヒープサイズが$k$であるため、時間計算量は$O(n \log k)$です。この方法は非常に効率的で、$k$が小さい場合、時間計算量は$O(n)$に近づき、$k$が大きい場合でも、時間計算量は$O(n \log n)$を超えません。
|
||||
|
||||
さらに、この方法は動的データストリームのシナリオに適しています。データを継続的に追加することで、ヒープ内の要素を維持し、最大$k$個の要素の動的更新を実現できます。
|
||||