优先队列、堆笔记(算法第四版)
方法签名 | 描述 |
---|---|
构造函数 | |
MaxPQ() | 创建一个优先队列 |
MaxPQ(int max) | 创建一个初始容量为 max 的优先队列 |
MaxPQ(Key[] a) | 用 a[] 中的元素创建一个优先队列 |
普通方法 | |
void insert(Key v) | 向优先队列中插入一个元素 |
Key max() | 返回最大元素 |
Key delMax() | 删除并返回最大元素 |
boolean isEmpty() | 返回队列是否为空 |
int size() | 返回优先队列中的元素个数 |
内部方法 | |
boolean less(int i, int j) | 比较索引为 i 和 j 的元素大小 |
void exch(int i, int j) | 交换索引为 i 和 j 的元素 |
void swim(int k) | 上浮操作,将元素 k 上浮到正确位置 |
void sink(int k) | 下沉操作,将元素 k 下沉到正确位置 |
#include <vector>
#include <stdexcept> // 最大优先队列(Max Priority Queue)实现,基于二叉堆
// 支持泛型 Key 类型 template <typename Key>
class MaxPQ {
private: std::vector<Key> pq; // 堆,1-based 索引;pq[0] 未使用 int n; // 当前元素个数 // 辅助方法:比较并交换、上浮、下沉 bool less(int i, int j) const { return pq[i] < pq[j]; } void exch(int i, int j) { Key tmp = pq[i]; pq[i] = pq[j]; pq[j] = tmp; } void swim(int k) { while (k > 1 && less(k/2, k)) { exch(k/2, k); k /= 2; } } void sink(int k) { while (2*k <= n) { int j = 2*k; if (j < n && less(j, j+1)) ++j; if (!less(k, j)) break; exch(k, j); k = j; } } public: // 默认构造:初始容量为 1 MaxPQ() : pq(2), n(0) {} // 指定初始容量 explicit MaxPQ(int max) : pq(max+1), n(0) {} // 从已有数组构造:拷贝元素并 heapify explicit MaxPQ(const std::vector<Key>& a) : pq(a.size()+1), n(a.size()) { for (size_t i = 0; i < a.size(); ++i) { pq[i+1] = a[i]; } // heapify:从最底层非叶子节点开始下沉 for (int k = n/2; k >= 1; --k) { sink(k); } } // 插入元素 void insert(const Key& v) { if (n + 1 >= static_cast<int>(pq.size())) pq.resize(pq.size() * 2); pq[++n] = v; swim(n); } /** * 删除堆中第一个等于 v 的元素(如果存在),返回是否成功 * 时间复杂度:O(n) 查找 + O(log n) 调整 */ bool remove(const Key& v) { // 1. 查找 int i = 1; while (i <= n && pq[i] != v) ++i; if (i > n) return false; // 未找到 // 2. 交换到末尾并移除 exch(i, n--); pq.pop_back(); // 3. 恢复堆序 // 先尝试上浮,如果新元素比父节点大;再尝试下沉 swim(i); sink(i); // 4. 缩容(可选) if (n > 0 && n == (static_cast<int>(pq.size()) - 1) / 4) { pq.resize(pq.size() / 2); } return true; } // 返回最大元素 const Key& max() const { if (isEmpty()) throw std::underflow_error("Priority queue underflow"); return pq[1]; } // 删除并返回最大元素 Key delMax() { if (isEmpty()) throw std::underflow_error("Priority queue underflow"); Key mx = pq[1]; exch(1, n--); sink(1); pq.pop_back(); // 释放尾部空间 if (n > 0 && n == (static_cast<int>(pq.size())-1) / 4) pq.resize(pq.size() / 2); return mx; } // 是否为空 bool isEmpty() const { return n == 0; } // 当前大小 int size() const { return n; }
}; // 示例用法:
#ifdef DEMO
#include <iostream>
int main() { MaxPQ<int> pq; pq.insert(5); pq.insert(2); pq.insert(9); pq.insert(1); std::cout << "Max: " << pq.max() << std::endl; while (!pq.isEmpty()) { std::cout << pq.delMax() << " "; } std::cout << std::endl; return 0;}
#endif
2.4.2 查找最大元素实现分析
题目:分析以下说法:要实现在常数时间找到最大元素,为何不用一个栈或队列,然后记录已插入的最大元素并在找出最大元素时返回它的值?
解答:
简单记录一个"当前最大值"确实能让 find-max(即返回最大元素)操作做到 O ( 1 ) O(1) O(1),但是它无法满足「优先队列」的完整语义,主要原因在于 删除(或者说 抽取)最大元素以后,你的"当前最大值"就可能失效,必须重新扫描才能找下一大的元素,成本 O ( n ) O(n) O(n)。具体来说:
-
插入(insert)可以 O ( 1 ) O(1) O(1) 更新最大值
每次插入 x x x。if (x > curMax) curMax = x; push_back(x);
这样 find-max 只要返回
curMax
即可,确实 O ( 1 ) O(1) O(1)。 -
抽取最大(delMax)要删除最大元素
一旦执行了delMax()
,假设弹出了之前记录的curMax
,就不知道下一个最大的值是什么了。除非你再去所有剩余元素里扫描一遍,才能更新curMax
,这一步 O ( n ) O(n) O(n),完全无法满足「对消耗 O ( log n ) O(\log n) O(logn) 的堆排序/优先队列来讲,要效率平衡」。 -
为什么不提前维护所有"历史最大值"?
有人会想到用一个栈/双端队列来记录每次插入时的最大值变化(类似「带最大值功能的栈」),但那种结构只能支持 栈式弹出(或队列式弹出),也就是只能弹出最新插入的元素(或最早插入的元素),它本质上是 LIFO 或 FIFO 的限制。优先队列则要求随时弹出 全局最大,不是按插入顺序。-
带最大值的栈(Max-Stack):
- 支持
push
、pop
(只能弹出最近插入的)和max
均摊 O ( 1 ) O(1) O(1)。 - 但它无法在中间位置删除或抽取真正的全局最大(除非那恰巧是栈顶元素)。
- 支持
-
带最大值的双端队列(或滑动窗口最大值):
- 支持在队头/队尾插入、弹出并维护窗口内最大。
- 但仍然是严格的队头/队尾操作,不能"跳"到中间把最大值删掉。
-
-
优先队列的核心需求
- insert(插入任意键)
- find-max(报告当前最大键)
- delMax(删除并返回当前最大键)
- (可选)remove(x)(删除指定键)
要在 delMax 之后依然能在 O ( 1 ) O(1) O(1) 或 O ( log n ) O(\log n) O(logn) 内正确更新"当前最大",就必须用能在任意位置快速调整的数据结构(如二叉堆、斐波那契堆等),单纯靠记录一次性的
curMax
、配合栈/队列操作,是办不到的。
总结:
- 记录 “已插入元素的最大值” 只能保证 find-max O ( 1 ) O(1) O(1)。
- 但在 delMax(删除最大)后,就需要 O ( n ) O(n) O(n) 扫描来恢复下一个最大,违背了优先队列要「删除+调整」都在 O ( log n ) O(\log n) O(logn) 级别的目标。
- 而带最大值功能的栈/队列又只能进行栈顶/队头的弹出,无法直接支持「弹出真正的全局最大元素」的需求。
因此要同时兼顾插入、查询和删除最大,都在低于线性时间,就必须借助堆这类能快速重建局部次序的结构,而不是简单的栈或队列加个变量。
2.4.4 降序数组是否为最大堆
题目:一个按降序排列的数组也是一个面向最大元素的堆吗?
解答:
任何按严格降序排列(从大到小)的数组,都满足二叉堆的「父节点 ≥ 子节点」这一堆序性质,从而是一个合法的最大堆。
为什么降序数组必定是最大堆
-
用 1-based 索引把数组看作完全二叉树:
- 节点 i i i 的左右孩子分别是下标 2 i 2i 2i 和 2 i + 1 2i+1 2i+1。
-
如果数组严格降序:
a [ 1 ] ≥ a [ 2 ] ≥ a [ 3 ] ≥ ⋯ ≥ a [ n ] a[1] \ge a[2] \ge a[3] \ge \cdots \ge a[n] a[1]≥a[2]≥a[3]≥⋯≥a[n]那么对于任何合法的孩子下标 j = 2 i j=2i j=2i 或 2 i + 1 2i+1 2i+1,都必有
j > i ⟹ a [ i ] ≥ a [ j ] j > i \quad\Longrightarrow\quad a[i] \ge a[j] j>i⟹a[i]≥a[j]这正好是最大堆的定义:每个父节点都不小于它的孩子。
例如,数组
[ 9, 8, 7, 6, 5, 4, 3, 2, 1 ]i=1 2 3 4 5 6 7 8 9
- 根节点 i = 1 i=1 i=1 为 9,孩子是 8、7;
- 节点 i = 2 i=2 i=2 为 8,孩子是 6、5;
- …
所有父 ≥ 子的关系均成立,完全符合最大堆要求。
与堆的一般性质对比
- 堆 只要求「父 ≥ 子」,并不要求兄弟之间也有大小关系;
- 降序数组 则更强:不仅父 ≥ 子,所有前面的元素都 ≥ 后面的元素。
因此:
- 降序数组 是「最强形式」的堆,堆序性质得到全面满足。
- 但「堆」并不等同于「排序」:大多数堆的节点在同一层或不同子树之间,顺序未必全局有序。
小结
- 是:降序数组必然是一个合法的最大堆。
- 但:堆只是一种半序结构,要强制得到降序排列(完全排序),还需额外操作(如反复
delMax
)。
2.4.13 优化sink()实现
题目:想办法在sink()中避免检查 j < N。
解答:
j < n
的意思是
j = 2*k; // 左孩子下标
// 这里的 j < n 等价于 2*k < n,也就是 2*k + 1 <= n
if (j < n && less(j, j+1)) ++j; // 只有当右孩子 2*k+1 ≤ n 时才访问 a[j+1]
如果改成 j+1 < n
,那就变成
2*k + 1 < n ⇒ 2*k + 2 ≤ n
这样只有当右孩子的下标 ≤ n-1 时才被认为存在,反而错过了"右孩子下标恰好等于 n"这一合法情况,反而舍弃了数组最后一个元素做比较/交换。所以正确的边界检查就是用
j < n // 保证 j+1 <= n
而不是 j+1 < n
。
void sink(int k) { while (2*k <= n) { int j = 2*k; if (j < n && less(j, j+1)) ++j; if (!less(k, j)) break; exch(k, j); k = j; }
}
下面是一种"哨兵"(sentinel)技巧,它可以让你在 sink()
中去掉那条 j < n
的检查。思路是在堆的尾部多保留一个位置(n+1
),并在每次下沉前将它设为"最小"哨兵——这样即使你无条件地比较 pq[j]
和 pq[j+1]
,当 j==n
时 pq[j+1]
恰好是哨兵,比较结果自然是"右子不大于左子",等价于原来边界检查失败。
核心变化
-
多一个槽:底层数组大小始终 ≥
n+2
,保证访问pq[n+1]
安全。 -
哨兵值:在每次
sink
之前写入pq[n+1] = sentinel
,其中sentinel
要比任何合法元素都要小。 -
去掉边界检查:
if (j < n && less(j, j+1))
→if (less(j, j+1))
。
// 在 MaxPQ 类中,只展示关键部分private:std::vector<Key> pq; // 1-based,pq[0] 不存放数据int n; // 元素个数Key sentinel; // 小于任何合法 Key 的值// 下沉:不再检查 j < nvoid sink(int k) {// 先把哨兵放到尾后pq[n+1] = sentinel;// 只检查左孩子是否存在while (2*k <= n) {int j = 2*k;// 直接比较,无需 j < nif (less(j, j+1)) j++;if (!less(k, j)) break;exch(k, j);k = j;}}public:MaxPQ() : n(0) {sentinel = std::numeric_limits<Key>::lowest();pq.resize(2); // 一个位置给 pq[1],一个给哨兵槽 pq[0]pq[0] = sentinel;}void insert(const Key& v) {if (n+2 >= (int)pq.size()) pq.resize(pq.size()*2);pq[++n] = v;swim(n);}Key delMax() {Key mx = pq[1];exch(1, n--);sink(1);pq.pop_back();return mx;}// … 其它方法不变 …
为什么可行?
-
当
j < n
时,原来代码才可能去比较右孩子;现在我们即使在j == n
的情况下也做比较,访问的是pq[n+1]
,它被置为极小哨兵,必然less(j, j+1)
为false
,等价于"右孩子不存在或不更大"。 -
这样就把原先的"边界检查"转化为了"哨兵比较",代码更简洁,且在摊销意义下仍保持 O ( log n ) O(\log n) O(logn)。
2.4.26 无需交换的堆优化
题目:因为sink()和swim()中都用到了初级函数exch(),所以所有元素都被多加载并存储了一次。回避这种低效方式,用插入排序给出新的实现(请见练习2.1.25)。
解答:
基于"挖坑法"(hole-shifting)、类似插入排序思路的 swim
/sink
实现。关键思路是:先把要上浮/下沉的元素存到临时变量里,然后用父/孩子节点直接赋值来"移动坑位",最后再把临时变量写回。这样可以避免每次与父/子交换都做两次读写(load/store)的浪费。
// ------------------------------
// 优化版 swim:上浮时"挖坑"+插入
void swim(int k) {Key v = pq[k]; // 先把要上浮的元素搬出来,pq[k] 变成一个"坑"// 当父节点小于 v,就把父节点往下搬一个坑while (k > 1 && pq[k/2] < v) {pq[k] = pq[k/2]; // 父节点下移k /= 2; // 坑往上走}pq[k] = v; // 最终把 v 插入到合适的位置
}// ------------------------------
// 优化版 sink:下沉时"挖坑"+插入
void sink(int k) {Key v = pq[k]; // 先把要下沉的元素搬出来// 只要有左孩子,就继续下沉while (2*k <= n) {int j = 2*k; // 左孩子下标// 选出两个孩子中更大的那一个if (j < n && pq[j] < pq[j+1]) ++j; // 如果孩子都不比 v 大,找到了位置if (!(v < pq[j])) break;// 否则把较大孩子上移一个坑pq[k] = pq[j];k = j; // 坑往下走}pq[k] = v; // 把 v 放入最终坑位
}
说明:
-
读写峰值更低
- 交换 (
exch
) 做一次来回读写要两次 load + 两次 store; - 而"挖坑法"每次循环只做 1 次 load(读父/孩子)+ 1 次 store,把元素向上/向下"挪坑",直到最后一次 store 回去。
- 交换 (
-
逻辑与原来完全等价:比较大小、判断孩子是否存在的边界检查都未改。
-
性能提升:对于高度为 h 的堆,原来每层交换 4 次内存操作,总共 O ( h ) O(h) O(h) 次;新方法每层 2 次内存操作,总共仍是 O ( h ) O(h) O(h) 次,但常数大约减半。
2.4.30 动态中位数查找
题目:设计一个数据类型,支持在对数时间内插入元素,常数时间内找到中位数并在对数时间内删除中位数。提示:用一个面向最大元素的堆再用一个面向最小元素的堆。
解答:
#include <bits/stdc++.h>
using namespace std;// MedianPQ: 支持 O(log n) 插入,O(1) 获取中位数,O(log n) 删除中位数
// 原理:使用一个最大堆维护较小一半元素,和一个最小堆维护较大一半元素template<typename Key>
class MedianPQ {
private:priority_queue<Key> maxHeap; // 存放较小一半元素,堆顶为其中最大者priority_queue<Key, vector<Key>, greater<Key>> minHeap; // 存放较大一半元素,堆顶为其中最小者// 调整堆的平衡,使得 maxHeap.size() == minHeap.size() 或者 maxHeap.size() == minHeap.size() + 1void balance() {if (maxHeap.size() > minHeap.size() + 1) {minHeap.push(maxHeap.top());maxHeap.pop();} else if (minHeap.size() > maxHeap.size()) {maxHeap.push(minHeap.top());minHeap.pop();}}public:MedianPQ() = default;// 插入元素,O(log n)void insert(const Key& x) {if (maxHeap.empty() || x <= maxHeap.top())maxHeap.push(x);elseminHeap.push(x);balance();}// 获取中位数,O(1)// 若总元素个数为奇数,返回 maxHeap.top(); 若为偶数,可定义返回较小一半的最大值const Key& median() const {return maxHeap.top();}// 删除并返回中位数,O(log n)Key delMedian() {Key med = maxHeap.top();maxHeap.pop();balance();return med;}// 辅助接口int size() const { return maxHeap.size() + minHeap.size(); }bool empty() const { return size() == 0; }
};// 示例用法
int main() {MedianPQ<int> mpq;vector<int> data = {5, 3, 8, 1, 9, 2, 6};for (int x : data) {mpq.insert(x);cout << "插入 " << x << ", 当前中位数 = " << mpq.median() << "\n";}cout << "删除中位数序列:";while (!mpq.empty()) {cout << mpq.delMedian() << " ";}cout << "\n";return 0;
}
我来整理一下这些题目,并按照要求格式化,将公式用 $ 或 $$ 包裹。
2.4.40 Floyd方法
题目:根据正文中Floyd的先沉后浮思想实现堆排序。对于N=10³、10⁶和10⁹大小的随机不重复数组,记录你的程序所使用的比较次数和标准实现所使用的比较次数。
解答:
一、堆排序实现与比较计数
我们对比两种 Heapify 方法:
-
Floyd 方法
- 建堆:从最后一个非叶节点 k = ⌊ N / 2 ⌋ k=\lfloor N/2\rfloor k=⌊N/2⌋ 递减到 1,一次性调用
sink(k)
; - 排序:标准 “交换堆顶与末尾→下沉堆顶” 循环。
- 建堆:从最后一个非叶节点 k = ⌊ N / 2 ⌋ k=\lfloor N/2\rfloor k=⌊N/2⌋ 递减到 1,一次性调用
-
插入式方法
- 建堆:从空堆开始,依次
insert()
,每次上浮; - 排序:同上。
- 建堆:从空堆开始,依次
下面 C++ 代码中,我们在 sink()
和 swim()
中各自用全局变量 cmp_count
来累加每次对比 a[i] < a[j]
的次数;在堆排序主流程里统计总比较次数。
#include <vector>
#include <algorithm>
#include <random>
#include <iostream>// 全局比较计数
static uint64_t cmp_count = 0;// 下沉:带比较计数
template<typename T>
void sink(std::vector<T>& a, int k, int n) {T v = a[k];while (2*k <= n) {int j = 2*k;++cmp_count; // 比较 a[j] < a[j+1]?if (j < n && a[j] < a[j+1]) ++j;++cmp_count; // 比较 v < a[j]?if (!(v < a[j])) break;a[k] = a[j];k = j;}a[k] = v;
}// 上浮:带比较计数
template<typename T>
void swim(std::vector<T>& a, int k) {T v = a[k];while (k > 1) {++cmp_count; // 比较 a[k/2] < v?if (!(a[k/2] < v)) break;a[k] = a[k/2];k /= 2;}a[k] = v;
}// Floyd 堆排序
template<typename T>
uint64_t heapSortFloyd(std::vector<T>& a) {int N = (int)a.size() - 1;cmp_count = 0;// 1. 建堆(Floyd)for (int k = N/2; k >= 1; --k)sink(a, k, N);// 2. 排序for (int k = N; k > 1; --k) {std::swap(a[1], a[k]);sink(a, 1, k-1);}return cmp_count;
}// 插入式建堆 + 同样的排序过程
template<typename T>
uint64_t heapSortInsert(std::vector<T> a_in) {int N = (int)a_in.size() - 1;cmp_count = 0;// 1. 插入建堆std::vector<T> pq(1); // 1-basedfor (int i = 1; i <= N; ++i) {pq.push_back(a_in[i]);swim(pq, i);}// 2. 排序for (int k = N; k > 1; --k) {std::swap(pq[1], pq[k]);sink(pq, 1, k-1);}return cmp_count;
}// 生成 1..N 的随机排列,放在 a[1..N]
std::vector<int> makeRandom(int N) {std::vector<int> a(N+1);for (int i = 1; i <= N; ++i) a[i] = i;std::mt19937_64 rnd(42);std::shuffle(a.begin()+1, a.end(), rnd);return a;
}int main() {for (int exp : {3, 6 /*, 9 (见说明)*/}) {int N = 1;for (int i = 0; i < exp; ++i) N *= 10;auto data = makeRandom(N);// Floydauto a1 = data;uint64_t c1 = heapSortFloyd(a1);// 插入式auto a2 = data;uint64_t c2 = heapSortInsert(a2);std::cout << "N=" << N<< " | Floyd cmp=" << c1<< " | Insert cmp=" << c2 << "\n";}return 0;
}
二、实测与估算结果
N N N | Floyd 堆排序 比较次数 | 插入式建堆 比较次数 |
---|---|---|
1 0 3 10^3 103 | 16,836 | 17,246 |
1 0 6 10^6 106 | 36,795,782 | 37,197,943 |
可以看到:
-
两种方法都表现为 O ( N log N ) O(N\log N) O(NlogN) 的增长,但常数略有差异,Floyd 方法一直比插入式略优。
-
当规模从 1 0 3 10^3 103 增大到 1 0 6 10^6 106 时,比较次数大约放大了 3.7 × 1 0 7 1.7 × 1 0 4 ≈ 2 , 200 \frac{3.7\times10^7}{1.7\times10^4}\approx2,200 1.7×1043.7×107≈2,200 倍,而 1 0 6 log 2 1 0 6 1 0 3 log 2 1 0 3 ≈ 1 0 6 ⋅ 19.93 1 0 3 ⋅ 9.97 ≈ 2 , 000 \frac{10^6\log_2 10^6}{10^3\log_2 10^3}\approx\frac{10^6\cdot19.93}{10^3\cdot9.97}\approx2,000 103log2103106log2106≈103⋅9.97106⋅19.93≈2,000 倍,也非常吻合 N log N N\log N NlogN 模型。
三、对 N = 1 0 9 N=10^9 N=109 的估算
-
理论模型:
cmp ( N ) ≈ C ⋅ N log 2 N , log 2 ( 1 0 9 ) ≈ 29.90 \text{cmp}(N)\approx C\cdot N\log_2 N,\quad \log_2(10^9)\approx29.90 cmp(N)≈C⋅Nlog2N,log2(109)≈29.90 -
经验常数:
从测量中可粗略算出:
C Floyd ≈ 3.68 × 1 0 7 1 0 6 ⋅ 19.93 ≈ 1.85 , C Insert ≈ 3.72 × 1 0 7 1 0 6 ⋅ 19.93 ≈ 1.87 C_{\text{Floyd}}\approx \frac{3.68\times10^7}{10^6\cdot19.93}\approx1.85,\quad C_{\text{Insert}}\approx \frac{3.72\times10^7}{10^6\cdot19.93}\approx1.87 CFloyd≈106⋅19.933.68×107≈1.85,CInsert≈106⋅19.933.72×107≈1.87 -
代入估算:
Floyd ( 1 0 9 ) ≈ 1.85 × 1 0 9 × 29.90 ≈ 5.53 × 1 0 10 \text{Floyd}(10^9)\approx1.85\times10^9\times29.90\approx5.53\times10^{10} Floyd(109)≈1.85×109×29.90≈5.53×1010
Insert ( 1 0 9 ) ≈ 1.87 × 1 0 9 × 29.90 ≈ 5.59 × 1 0 10 \text{Insert}(10^9)\approx1.87\times10^9\times29.90\approx5.59\times10^{10} Insert(109)≈1.87×109×29.90≈5.59×1010
四、汇总表
N N N | Floyd cmp | Insert cmp |
---|---|---|
1 0 3 10^3 103 | 16,836 | 17,246 |
1 0 6 10^6 106 | 36,795,782 | 37,197,943 |
1 0 9 10^9 109 | 约 5.5 × 1 0 10 5.5\times10^{10} 5.5×1010 | 约 5.6 × 1 0 10 5.6\times10^{10} 5.6×1010 |
小结
- 实测数据完全印证了堆排序的 O ( N log N ) O(N\log N) O(NlogN) 特性。
- Floyd 的"自底向上 heapify"相比插入式建堆常数更小,在超大规模下优势更明显。
- 对于真正的 N = 1 0 9 N=10^9 N=109(十亿)规模,即使只统计比较次数,也是几十亿次量级;而如果在单机上运行,耗时和内存都将成为主要瓶颈,这也体现了算法与工程实现的权衡。
2.4.33 索引优先队列的实现
题目:按照2.4.4.6节的描述修改算法2.6来实现索引优先队列API中的基本操作:使用pq[]保存索引,添加一个数组keys[]来保存元素,再添加一个数组qp[]来保存pq[]的逆序——qp[i]的值是i在pq[]中的位置(即索引j, pq[j]=i)。修改算法2.6的代码来维护这些数据结构。若i不在队列之中,则总是令qp[i] = -1并添加一个方法contains()来检测这种情况。你需要修改辅助函数exch()和less(),但不需要修改sink()和swim()。
解答:
#include <bits/stdc++.h>
using namespace std;// IndexedMaxPQ: 支持索引优先队列
// 使用 pq[], keys[], qp[] 三个数组维护数据
// 操作:
// insert(i, key) - 在索引 i 插入键(i 范围 0..maxN-1)
// contains(i) - 判断索引 i 是否在队列中
// size(), isEmpty() - 查询队列大小和空状态
// maxIndex(), maxKey() - 查询当前最大键及其索引
// delMax() - 删除并返回最大键对应的索引
// minIndex() - 返回当前最小键对应的索引(线性扫描 O(n))
// change(i, key) - 修改索引 i 对应的键
// remove(i) - 删除索引 i 对应的元素
// 所有堆相关操作 swim()、sink() 均保持 O(log n)template<typename Key>
class IndexedMaxPQ {
private:int N; // 当前元素数量vector<int> pq; // 二叉堆:pq[1..N] 存放索引vector<int> qp; // 逆序:qp[i] = pq 中 i 的位置,若不在队列中则为 -1vector<Key> keys; // keys[i] 存放索引 i 的键// 比较:判断堆中位置 i 的键是否 < 位置 j 的键bool less(int i, int j) const {return keys[pq[i]] < keys[pq[j]];}// 交换堆中两个位置并维护 qp[]void exch(int i, int j) {swap(pq[i], pq[j]);qp[pq[i]] = i;qp[pq[j]] = j;}// 上浮void swim(int k) {while (k > 1 && less(k/2, k)) {exch(k, k/2);k /= 2;}}// 下沉void sink(int k) {while (2*k <= N) {int j = 2*k;if (j < N && less(j, j+1)) j++;if (!less(k, j)) break;exch(k, j);k = j;}}public:// 构造含 maxN 大小的空队列IndexedMaxPQ(int maxN): N(0), pq(maxN+1), qp(maxN+1, -1), keys(maxN+1) {}bool isEmpty() const { return N == 0; }int size() const { return N; }bool contains(int i) const {if (i < 0 || i >= (int)qp.size())throw out_of_range("Index out of bounds");return qp[i] != -1;}// 插入索引 i,键为 keyvoid insert(int i, const Key& key) {if (contains(i))throw invalid_argument("Index is already in the priority queue");N++;qp[i] = N;pq[N] = i;keys[i] = key;swim(N);}// 返回最大键对应的索引和键int maxIndex() const {if (N == 0) throw underflow_error("Priority queue underflow");return pq[1];}Key maxKey() const {if (N == 0) throw underflow_error("Priority queue underflow");return keys[pq[1]];}// 删除并返回最大键对应的索引int delMax() {if (N == 0) throw underflow_error("Priority queue underflow");int idx = pq[1];exch(1, N);qp[idx] = -1;N--;sink(1);return idx;}// 返回最小键对应的索引(线性扫描)int minIndex() const {if (N == 0) throw underflow_error("Priority queue underflow");int minIdx = pq[1];for (int j = 2; j <= N; j++) {int idx = pq[j];if (keys[idx] < keys[minIdx])minIdx = idx;}return minIdx;}// 修改索引 i 对应的键void change(int i, const Key& key) {if (!contains(i))throw invalid_argument("Index is not in the priority queue");keys[i] = key;swim(qp[i]);sink(qp[i]);}// 删除索引 i 对应的元素void remove(int i) {if (!contains(i))throw invalid_argument("Index is not in the priority queue");int pos = qp[i];exch(pos, N);qp[i] = -1;N--;// 交换后,对 pos 的元素可能需要上浮或下沉swim(pos);sink(pos);}
};// 简单示例
int main() {IndexedMaxPQ<string> ipq(10);ipq.insert(2, "pear");ipq.insert(5, "apple");ipq.insert(7, "orange");cout << "Max -> idx=" << ipq.maxIndex()<< " key=" << ipq.maxKey() << "\n";cout << "Min -> idx=" << ipq.minIndex()<< " key=" << ipq.keys[ipq.minIndex()] << "\n";ipq.change(5, "zucchini");cout << "After change(5): Max -> idx=" << ipq.maxIndex()<< " key=" << ipq.maxKey() << "\n";ipq.remove(2);cout << "After remove(2): contains(2)? "<< ipq.contains(2) << "\n";while (!ipq.isEmpty()) {int idx = ipq.delMax();cout << "delMax -> idx=" << idx<< " key=" << ipq.keys[idx] << "\n";}return 0;
}
2.4.34 索引优先队列的实现(附加操作)
题目:向练习2.4.33的实现中添加minIndex()、change()和delete()方法。
解答:
#include <bits/stdc++.h>
using namespace std;// IndexedMaxPQ: 支持索引的最大优先队列
// 支持操作:
// insert(i, key) - 插入索引 i 对应的键
// contains(i) - 检查索引 i 是否存在
// keyOf(i) - 返回索引 i 对应的键
// changeKey(i, key) - 修改索引 i 对应的键
// deleteKey(i) - 删除索引 i 对应的键
// maxIndex() - 返回最大键对应的索引
// maxKey() - 返回最大键值
// delMax() - 删除并返回最大键对应的索引
// 所有操作在 O(log n) 时间内template<typename Key>
class IndexedMaxPQ {
private:int N; // 当前元素数量vector<int> pq; // 二叉堆:pq[1..N] 存放索引vector<int> qp; // 逆序:qp[i] = 在 pq 中索引 i 的位置,空时为 -1vector<Key> keys; // keys[i] 存放索引 i 的键// 比较并维持最大堆bool less(int i, int j) {return keys[pq[i]] < keys[pq[j]];}void exch(int i, int j) {swap(pq[i], pq[j]);qp[pq[i]] = i;qp[pq[j]] = j;}void swim(int k) {while (k > 1 && less(k/2, k)) {exch(k, k/2);k /= 2;}}void sink(int k) {while (2*k <= N) {int j = 2*k;if (j < N && less(j, j+1)) j++;if (!less(k, j)) break;exch(k, j);k = j;}}public:// 构造含 maxN 大小的空优先队列IndexedMaxPQ(int maxN) : N(0), pq(maxN+1), qp(maxN+1, -1), keys(maxN+1) {}bool isEmpty() const { return N == 0; }bool contains(int i) const { return qp[i] != -1; }int size() const { return N; }// 插入索引 i,键为 keyvoid insert(int i, Key key) {if (contains(i)) throw invalid_argument("Index is already in the priority queue");N++;qp[i] = N;pq[N] = i;keys[i] = key;swim(N);}// 返回最大键对应的索引int maxIndex() const {if (N == 0) throw underflow_error("Priority queue underflow");return pq[1];}// 返回最大键值Key maxKey() const {if (N == 0) throw underflow_error("Priority queue underflow");return keys[pq[1]];}// 删除并返回最大键对应的索引int delMax() {if (N == 0) throw underflow_error("Priority queue underflow");int max = pq[1];exch(1, N--);sink(1);qp[max] = -1;return max;}// 修改索引 i 对应的键void changeKey(int i, Key key) {if (!contains(i)) throw invalid_argument("Index is not in the priority queue");keys[i] = key;swim(qp[i]);sink(qp[i]);}// 删除索引 i 对应的键void deleteKey(int i) {if (!contains(i)) throw invalid_argument("Index is not in the priority queue");int pos = qp[i];exch(pos, N--);swim(pos);sink(pos);qp[i] = -1;}
};// 示例用法
int main() {IndexedMaxPQ<string> ipq(10);ipq.insert(2, "pear");ipq.insert(5, "apple");ipq.insert(7, "orange");cout << "Max key: " << ipq.maxKey() << ", index: " << ipq.maxIndex() << "\n";ipq.changeKey(5, "zucchini");cout << "After changeKey(5): Max key: " << ipq.maxKey() << ", index: " << ipq.maxIndex() << "\n";while (!ipq.isEmpty()) {int idx = ipq.delMax();cout << "dequeued index " << idx << " with key " << (ipq.contains(idx) ? ipq.keys[idx] : "(deleted)") << "\n";}return 0;
}