hello-algo/docs/chapter_heap/heap.md
2023-05-24 20:57:08 +08:00

716 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 堆
「堆 Heap」是一种满足特定条件的完全二叉树可分为两种类型
- 「大顶堆 Max Heap」任意节点的值 $\geq$ 其子节点的值;
- 「小顶堆 Min Heap」任意节点的值 $\leq$ 其子节点的值;
![小顶堆与大顶堆](heap.assets/min_heap_and_max_heap.png)
堆作为完全二叉树的一个特例,具有以下特性:
- 最底层节点靠左填充,其他层的节点都被填满。
- 我们将二叉树的根节点称为「堆顶」,将底层最靠右的节点称为「堆底」。
- 对于大顶堆(小顶堆),堆顶元素(即根节点)的值分别是最大(最小)的。
## 堆常用操作
需要指出的是,许多编程语言提供的是「优先队列 Priority Queue」这是一种抽象数据结构定义为具有优先级排序的队列。
实际上,**堆通常用作实现优先队列,大顶堆相当于元素按从大到小顺序出队的优先队列**。从使用角度来看,我们可以将「优先队列」和「堆」看作等价的数据结构。因此,本书对两者不做特别区分,统一使用「堆」来命名。
堆的常用操作见下表,方法名需要根据编程语言来确定。
<div class="center-table" markdown>
| 方法名 | 描述 | 时间复杂度 |
| --------- | ------------------------------------------ | ----------- |
| push() | 元素入堆 | $O(\log n)$ |
| pop() | 堆顶元素出堆 | $O(\log n)$ |
| peek() | 访问堆顶元素(大 / 小顶堆分别为最大 / 小值) | $O(1)$ |
| size() | 获取堆的元素数量 | $O(1)$ |
| isEmpty() | 判断堆是否为空 | $O(1)$ |
</div>
在实际应用中,我们可以直接使用编程语言提供的堆类(或优先队列类)。
!!! tip
类似于排序算法中的“从小到大排列”和“从大到小排列”,我们可以通过修改 Comparator 来实现“小顶堆”与“大顶堆”之间的转换。
=== "Java"
```java title="heap.java"
/* 初始化堆 */
// 初始化小顶堆
Queue<Integer> minHeap = new PriorityQueue<>();
// 初始化大顶堆(使用 lambda 表达式修改 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 = heap.poll(); // 5
peek = heap.poll(); // 4
peek = heap.poll(); // 3
peek = heap.poll(); // 2
peek = heap.poll(); // 1
/* 获取堆大小 */
int size = maxHeap.size();
/* 判断堆是否为空 */
boolean isEmpty = maxHeap.isEmpty();
/* 输入列表并建堆 */
minHeap = new PriorityQueue<>(Arrays.asList(1, 3, 2, 5, 4));
```
=== "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());
```
=== "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)
```
=== "Go"
```go title="heap.go"
// Go 语言中可以通过实现 heap.Interface 来构建整数大顶堆
// 实现 heap.Interface 需要同时实现 sort.Interface
type intHeap []any
// Push heap.Interface 的方法,实现推入元素到堆
func (h *intHeap) Push(x any) {
// Push 和 Pop 使用 pointer receiver 作为参数
// 因为它们不仅会对切片的内容进行调整,还会修改切片的长度。
*h = append(*h, x.(int))
}
// Pop heap.Interface 的方法,实现弹出堆顶元素
func (h *intHeap) Pop() any {
// 待出堆元素存放在最后
last := (*h)[len(*h)-1]
*h = (*h)[:len(*h)-1]
return last
}
// Len sort.Interface 的方法
func (h *intHeap) Len() int {
return len(*h)
}
// Less sort.Interface 的方法
func (h *intHeap) Less(i, j int) bool {
// 如果实现小顶堆,则需要调整为小于号
return (*h)[i].(int) > (*h)[j].(int)
}
// Swap sort.Interface 的方法
func (h *intHeap) Swap(i, j int) {
(*h)[i], (*h)[j] = (*h)[j], (*h)[i]
}
// Top 获取堆顶元素
func (h *intHeap) Top() any {
return (*h)[0]
}
/* Driver Code */
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)
}
```
=== "JavaScript"
```javascript title="heap.js"
// JavaScript 未提供内置 Heap 类
```
=== "TypeScript"
```typescript title="heap.ts"
// TypeScript 未提供内置 Heap 类
```
=== "C"
```c title="heap.c"
// C 未提供内置 Heap 类
```
=== "C#"
```csharp title="heap.cs"
/* 初始化堆 */
// 初始化小顶堆
PriorityQueue<int, int> minHeap = new PriorityQueue<int, int>();
// 初始化大顶堆(使用 lambda 表达式修改 Comparator 即可)
PriorityQueue<int, int> maxHeap = new PriorityQueue<int, int>(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>(new List<(int, int)> { (1, 1), (3, 3), (2, 2), (5, 5), (4, 4), });
```
=== "Swift"
```swift title="heap.swift"
// Swift 未提供内置 Heap 类
```
=== "Zig"
```zig title="heap.zig"
```
## 堆的实现
下文实现的是大顶堆。若要将其转换为小顶堆,只需将所有大小逻辑判断取逆(例如,将 $\geq$ 替换为 $\leq$ )。感兴趣的读者可以自行实现。
### 堆的存储与表示
我们在二叉树章节中学习到,完全二叉树非常适合用数组来表示。由于堆正是一种完全二叉树,**我们将采用数组来存储堆**。
当使用数组表示二叉树时,元素代表节点值,索引代表节点在二叉树中的位置。**节点指针通过索引映射公式来实现**。
具体而言,给定索引 $i$ ,其左子节点索引为 $2i + 1$ ,右子节点索引为 $2i + 2$ ,父节点索引为 $(i - 1) / 2$(向下取整)。当索引越界时,表示空节点或节点不存在。
![堆的表示与存储](heap.assets/representation_of_heap.png)
我们可以将索引映射公式封装成函数,方便后续使用。
=== "Java"
```java title="my_heap.java"
[class]{MaxHeap}-[func]{left}
[class]{MaxHeap}-[func]{right}
[class]{MaxHeap}-[func]{parent}
```
=== "C++"
```cpp title="my_heap.cpp"
[class]{MaxHeap}-[func]{left}
[class]{MaxHeap}-[func]{right}
[class]{MaxHeap}-[func]{parent}
```
=== "Python"
```python title="my_heap.py"
[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}
```
=== "JavaScript"
```javascript title="my_heap.js"
[class]{MaxHeap}-[func]{#left}
[class]{MaxHeap}-[func]{#right}
[class]{MaxHeap}-[func]{#parent}
```
=== "TypeScript"
```typescript title="my_heap.ts"
[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}
```
=== "C#"
```csharp title="my_heap.cs"
[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}
```
=== "Zig"
```zig title="my_heap.zig"
[class]{MaxHeap}-[func]{left}
[class]{MaxHeap}-[func]{right}
[class]{MaxHeap}-[func]{parent}
```
### 访问堆顶元素
堆顶元素即为二叉树的根节点,也就是列表的首个元素。
=== "Java"
```java title="my_heap.java"
[class]{MaxHeap}-[func]{peek}
```
=== "C++"
```cpp title="my_heap.cpp"
[class]{MaxHeap}-[func]{peek}
```
=== "Python"
```python title="my_heap.py"
[class]{MaxHeap}-[func]{peek}
```
=== "Go"
```go title="my_heap.go"
[class]{maxHeap}-[func]{peek}
```
=== "JavaScript"
```javascript title="my_heap.js"
[class]{MaxHeap}-[func]{peek}
```
=== "TypeScript"
```typescript title="my_heap.ts"
[class]{MaxHeap}-[func]{peek}
```
=== "C"
```c title="my_heap.c"
[class]{maxHeap}-[func]{peek}
```
=== "C#"
```csharp title="my_heap.cs"
[class]{MaxHeap}-[func]{peek}
```
=== "Swift"
```swift title="my_heap.swift"
[class]{MaxHeap}-[func]{peek}
```
=== "Zig"
```zig title="my_heap.zig"
[class]{MaxHeap}-[func]{peek}
```
### 元素入堆
给定元素 `val` ,我们首先将其添加到堆底。添加之后,由于 val 可能大于堆中其他元素,堆的成立条件可能已被破坏。因此,**需要修复从插入节点到根节点的路径上的各个节点**,这个操作被称为「堆化 Heapify」。
考虑从入堆节点开始,**从底至顶执行堆化**。具体来说,我们比较插入节点与其父节点的值,如果插入节点更大,则将它们交换。然后继续执行此操作,从底至顶修复堆中的各个节点,直至越过根节点或遇到无需交换的节点时结束。
=== "<1>"
![元素入堆步骤](heap.assets/heap_push_step1.png)
=== "<2>"
![heap_push_step2](heap.assets/heap_push_step2.png)
=== "<3>"
![heap_push_step3](heap.assets/heap_push_step3.png)
=== "<4>"
![heap_push_step4](heap.assets/heap_push_step4.png)
=== "<5>"
![heap_push_step5](heap.assets/heap_push_step5.png)
=== "<6>"
![heap_push_step6](heap.assets/heap_push_step6.png)
设节点总数为 $n$ ,则树的高度为 $O(\log n)$ 。由此可知,堆化操作的循环轮数最多为 $O(\log n)$ **元素入堆操作的时间复杂度为 $O(\log n)$** 。
=== "Java"
```java title="my_heap.java"
[class]{MaxHeap}-[func]{push}
[class]{MaxHeap}-[func]{siftUp}
```
=== "C++"
```cpp title="my_heap.cpp"
[class]{MaxHeap}-[func]{push}
[class]{MaxHeap}-[func]{siftUp}
```
=== "Python"
```python title="my_heap.py"
[class]{MaxHeap}-[func]{push}
[class]{MaxHeap}-[func]{sift_up}
```
=== "Go"
```go title="my_heap.go"
[class]{maxHeap}-[func]{push}
[class]{maxHeap}-[func]{siftUp}
```
=== "JavaScript"
```javascript title="my_heap.js"
[class]{MaxHeap}-[func]{push}
[class]{MaxHeap}-[func]{#siftUp}
```
=== "TypeScript"
```typescript title="my_heap.ts"
[class]{MaxHeap}-[func]{push}
[class]{MaxHeap}-[func]{siftUp}
```
=== "C"
```c title="my_heap.c"
[class]{maxHeap}-[func]{push}
[class]{maxHeap}-[func]{siftUp}
```
=== "C#"
```csharp title="my_heap.cs"
[class]{MaxHeap}-[func]{push}
[class]{MaxHeap}-[func]{siftUp}
```
=== "Swift"
```swift title="my_heap.swift"
[class]{MaxHeap}-[func]{push}
[class]{MaxHeap}-[func]{siftUp}
```
=== "Zig"
```zig title="my_heap.zig"
[class]{MaxHeap}-[func]{push}
[class]{MaxHeap}-[func]{siftUp}
```
### 堆顶元素出堆
堆顶元素是二叉树的根节点,即列表首元素。如果我们直接从列表中删除首元素,那么二叉树中所有节点的索引都会发生变化,这将使得后续使用堆化修复变得困难。为了尽量减少元素索引的变动,我们采取以下操作步骤:
1. 交换堆顶元素与堆底元素(即交换根节点与最右叶节点);
2. 交换完成后,将堆底从列表中删除(注意,由于已经交换,实际上删除的是原来的堆顶元素);
3. 从根节点开始,**从顶至底执行堆化**
顾名思义,**从顶至底堆化的操作方向与从底至顶堆化相反**,我们将根节点的值与其两个子节点的值进行比较,将最大的子节点与根节点交换;然后循环执行此操作,直到越过叶节点或遇到无需交换的节点时结束。
=== "<1>"
![堆顶元素出堆步骤](heap.assets/heap_pop_step1.png)
=== "<2>"
![heap_pop_step2](heap.assets/heap_pop_step2.png)
=== "<3>"
![heap_pop_step3](heap.assets/heap_pop_step3.png)
=== "<4>"
![heap_pop_step4](heap.assets/heap_pop_step4.png)
=== "<5>"
![heap_pop_step5](heap.assets/heap_pop_step5.png)
=== "<6>"
![heap_pop_step6](heap.assets/heap_pop_step6.png)
=== "<7>"
![heap_pop_step7](heap.assets/heap_pop_step7.png)
=== "<8>"
![heap_pop_step8](heap.assets/heap_pop_step8.png)
=== "<9>"
![heap_pop_step9](heap.assets/heap_pop_step9.png)
=== "<10>"
![heap_pop_step10](heap.assets/heap_pop_step10.png)
与元素入堆操作相似,堆顶元素出堆操作的时间复杂度也为 $O(\log n)$ 。
=== "Java"
```java title="my_heap.java"
[class]{MaxHeap}-[func]{pop}
[class]{MaxHeap}-[func]{siftDown}
```
=== "C++"
```cpp title="my_heap.cpp"
[class]{MaxHeap}-[func]{pop}
[class]{MaxHeap}-[func]{siftDown}
```
=== "Python"
```python title="my_heap.py"
[class]{MaxHeap}-[func]{pop}
[class]{MaxHeap}-[func]{sift_down}
```
=== "Go"
```go title="my_heap.go"
[class]{maxHeap}-[func]{pop}
[class]{maxHeap}-[func]{siftDown}
```
=== "JavaScript"
```javascript title="my_heap.js"
[class]{MaxHeap}-[func]{pop}
[class]{MaxHeap}-[func]{#siftDown}
```
=== "TypeScript"
```typescript title="my_heap.ts"
[class]{MaxHeap}-[func]{pop}
[class]{MaxHeap}-[func]{siftDown}
```
=== "C"
```c title="my_heap.c"
[class]{maxHeap}-[func]{pop}
[class]{maxHeap}-[func]{siftDown}
```
=== "C#"
```csharp title="my_heap.cs"
[class]{MaxHeap}-[func]{pop}
[class]{MaxHeap}-[func]{siftDown}
```
=== "Swift"
```swift title="my_heap.swift"
[class]{MaxHeap}-[func]{pop}
[class]{MaxHeap}-[func]{siftDown}
```
=== "Zig"
```zig title="my_heap.zig"
[class]{MaxHeap}-[func]{pop}
[class]{MaxHeap}-[func]{siftDown}
```
## 堆常见应用
- **优先队列**:堆通常作为实现优先队列的首选数据结构,其入队和出队操作的时间复杂度均为 $O(\log n)$ ,而建队操作为 $O(n)$ ,这些操作都非常高效。
- **堆排序**:给定一组数据,我们可以用它们建立一个堆,然后不断地执行元素出堆操作,从而得到有序数据。当然,堆排序还有一种更优雅的实现,详见后续的堆排序章节。
- **获取最大的 $k$ 个元素**:这是一个经典的算法问题,同时也是一种典型应用,例如选择热度前 10 的新闻作为微博热搜,选取销量前 10 的商品等。