方法签名 | 描述 |
---|---|
构造函数 | |
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 下沉到正确位置 |
c++
#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。
cppif (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
的意思是
cpp
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
,那就变成
cpp
2*k + 1 < n ⇒ 2*k + 2 ≤ n
这样只有当右孩子的下标 ≤ n-1 时才被认为存在,反而错过了"右孩子下标恰好等于 n"这一合法情况,反而舍弃了数组最后一个元素做比较/交换。所以正确的边界检查就是用
cpp
j < n // 保证 j+1 <= n
而不是 j+1 < n
。
cpp
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))
。
cpp
// 在 MaxPQ 类中,只展示关键部分
private:
std::vector<Key> pq; // 1-based,pq[0] 不存放数据
int n; // 元素个数
Key sentinel; // 小于任何合法 Key 的值
// 下沉:不再检查 j < n
void sink(int k) {
// 先把哨兵放到尾后
pq[n+1] = sentinel;
// 只检查左孩子是否存在
while (2*k <= n) {
int j = 2*k;
// 直接比较,无需 j < n
if (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)的浪费。
cpp
// ------------------------------
// 优化版 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 动态中位数查找
题目:设计一个数据类型,支持在对数时间内插入元素,常数时间内找到中位数并在对数时间内删除中位数。提示:用一个面向最大元素的堆再用一个面向最小元素的堆。
解答:
cpp
#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() + 1
void 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);
else
minHeap.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]
的次数;在堆排序主流程里统计总比较次数。
cpp
#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-based
for (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);
// Floyd
auto 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()。
解答:
cpp
#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 的位置,若不在队列中则为 -1
vector<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,键为 key
void 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()方法。
解答:
cpp
#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 的位置,空时为 -1
vector<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,键为 key
void 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;
}