This commit is contained in:
krahets
2023-08-17 05:12:05 +08:00
parent f0826da7f6
commit 97c532b228
67 changed files with 1481 additions and 1066 deletions
+2 -2
View File
@@ -171,7 +171,7 @@ comments: true
_maxHeap = nums;
// 堆化除叶节点以外的其他所有节点
for (int i = _parent(size() - 1); i >= 0; i--) {
_siftDown(i);
siftDown(i);
}
}
```
@@ -204,7 +204,7 @@ comments: true
![完美二叉树的各层节点数量](build_heap.assets/heapify_operations_count.png)
<p align="center"> Fig. 完美二叉树的各层节点数量 </p>
<p align="center"> 图:完美二叉树的各层节点数量 </p>
因此,我们可以将各层的“节点数量 $\times$ 节点高度”求和,**从而得到所有节点的堆化迭代次数的总和**。
+40 -6
View File
@@ -11,7 +11,7 @@ comments: true
![小顶堆与大顶堆](heap.assets/min_heap_and_max_heap.png)
<p align="center"> Fig. 小顶堆与大顶堆 </p>
<p align="center"> 图:小顶堆与大顶堆 </p>
堆作为完全二叉树的一个特例,具有以下特性:
@@ -333,7 +333,7 @@ comments: true
![堆的表示与存储](heap.assets/representation_of_heap.png)
<p align="center"> Fig. 堆的表示与存储 </p>
<p align="center"> 图:堆的表示与存储 </p>
我们可以将索引映射公式封装成函数,方便后续使用。
@@ -708,6 +708,8 @@ comments: true
=== "<9>"
![heap_push_step9](heap.assets/heap_push_step9.png)
<p align="center"> 图:元素入堆步骤 </p>
设节点总数为 $n$ ,则树的高度为 $O(\log n)$ 。由此可知,堆化操作的循环轮数最多为 $O(\log n)$ ,**元素入堆操作的时间复杂度为 $O(\log n)$** 。
=== "Java"
@@ -994,10 +996,24 @@ comments: true
// 添加节点
_maxHeap.add(val);
// 从底至顶堆化
_siftUp(size() - 1);
siftUp(size() - 1);
}
[class]{MaxHeap}-[func]{siftUp}
/* 从节点 i 开始,从底至顶堆化 */
void siftUp(int i) {
while (true) {
// 获取节点 i 的父节点
int p = _parent(i);
// 当“越过根节点”或“节点无需修复”时,结束堆化
if (p < 0 || _maxHeap[i] <= _maxHeap[p]) {
break;
}
// 交换两节点
_swap(i, p);
// 循环向上堆化
i = p;
}
}
```
=== "Rust"
@@ -1072,6 +1088,8 @@ comments: true
=== "<10>"
![heap_pop_step10](heap.assets/heap_pop_step10.png)
<p align="center"> 图:堆顶元素出堆步骤 </p>
与元素入堆操作相似,堆顶元素出堆操作的时间复杂度也为 $O(\log n)$ 。
=== "Java"
@@ -1480,12 +1498,28 @@ comments: true
// 删除节点
int val = _maxHeap.removeLast();
// 从顶至底堆化
_siftDown(0);
siftDown(0);
// 返回堆顶元素
return val;
}
[class]{MaxHeap}-[func]{siftDown}
/* 从节点 i 开始,从顶至底堆化 */
void siftDown(int i) {
while (true) {
// 判断节点 i, l, r 中值最大的节点,记为 ma
int l = _left(i);
int r = _right(i);
int 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(i, ma);
// 循环向下堆化
i = ma;
}
}
```
=== "Rust"
+19 -4
View File
@@ -18,11 +18,11 @@ comments: true
![遍历寻找最大的 k 个元素](top_k.assets/top_k_traversal.png)
<p align="center"> Fig. 遍历寻找最大的 k 个元素 </p>
<p align="center"> 图:遍历寻找最大的 k 个元素 </p>
!!! tip
当 $k = n$ 时,我们可以得到从大到小的序列,等价于「选择排序」算法。
当 $k = n$ 时,我们可以得到从大到小的序列,等价于「选择排序」算法。
## 8.3.2. &nbsp; 方法二:排序
@@ -32,7 +32,7 @@ comments: true
![排序寻找最大的 k 个元素](top_k.assets/top_k_sorting.png)
<p align="center"> Fig. 排序寻找最大的 k 个元素 </p>
<p align="center"> 图:排序寻找最大的 k 个元素 </p>
## 8.3.3. &nbsp; 方法三:堆
@@ -70,6 +70,8 @@ comments: true
=== "<9>"
![top_k_heap_step9](top_k.assets/top_k_heap_step9.png)
<p align="center"> 图:基于堆寻找最大的 k 个元素 </p>
总共执行了 $n$ 轮入堆和出堆,堆的最大长度为 $k$ ,因此时间复杂度为 $O(n \log k)$ 。该方法的效率很高,当 $k$ 较小时,时间复杂度趋向 $O(n)$ ;当 $k$ 较大时,时间复杂度不会超过 $O(n \log n)$ 。
另外,该方法适用于动态数据流的使用场景。在不断加入数据时,我们可以持续维护堆内的元素,从而实现最大 $k$ 个元素的动态更新。
@@ -227,7 +229,20 @@ comments: true
=== "Dart"
```dart title="top_k.dart"
[class]{}-[func]{top_k_heap}
/* 基于堆查找数组中最大的 k 个元素 */
MinHeap topKHeap(List<int> nums, int k) {
// 将数组的前 k 个元素入堆
MinHeap heap = MinHeap(nums.sublist(0, k));
// 从第 k+1 个元素开始,保持堆的长度为 k
for (int i = k; i < nums.length; i++) {
// 若当前元素大于堆顶元素,则将堆顶元素出堆、当前元素入堆
if (nums[i] > heap.peek()) {
heap.pop();
heap.push(nums[i]);
}
}
return heap;
}
```
=== "Rust"