当前位置: 首页 > news >正文

优先队列、堆笔记(算法第四版)

方法签名描述
构造函数
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)。具体来说:

  1. 插入(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)

  2. 抽取最大(delMax)要删除最大元素
    一旦执行了 delMax(),假设弹出了之前记录的 curMax,就不知道下一个最大的值是什么了。除非你再去所有剩余元素里扫描一遍,才能更新 curMax,这一步 O ( n ) O(n) O(n),完全无法满足「对消耗 O ( log ⁡ n ) O(\log n) O(logn) 的堆排序/优先队列来讲,要效率平衡」。

  3. 为什么不提前维护所有"历史最大值"?
    有人会想到用一个栈/双端队列来记录每次插入时的最大值变化(类似「带最大值功能的栈」),但那种结构只能支持 栈式弹出(或队列式弹出),也就是只能弹出最新插入的元素(或最早插入的元素),它本质上是 LIFOFIFO 的限制。优先队列则要求随时弹出 全局最大,不是按插入顺序。

    • 带最大值的栈(Max-Stack):

      • 支持 pushpop(只能弹出最近插入的)和 max 均摊 O ( 1 ) O(1) O(1)
      • 但它无法在中间位置删除或抽取真正的全局最大(除非那恰巧是栈顶元素)。
    • 带最大值的双端队列(或滑动窗口最大值):

      • 支持在队头/队尾插入、弹出并维护窗口内最大。
      • 但仍然是严格的队头/队尾操作,不能"跳"到中间把最大值删掉。
  4. 优先队列的核心需求

    • 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>ia[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==npq[j+1] 恰好是哨兵,比较结果自然是"右子不大于左子",等价于原来边界检查失败。

核心变化

  1. 多一个槽:底层数组大小始终 ≥ n+2,保证访问 pq[n+1] 安全。

  2. 哨兵值:在每次 sink 之前写入 pq[n+1] = sentinel,其中 sentinel 要比任何合法元素都要小。

  3. 去掉边界检查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 放入最终坑位
}

说明:

  1. 读写峰值更低

    • 交换 (exch) 做一次来回读写要两次 load + 两次 store;
    • 而"挖坑法"每次循环只做 1 次 load(读父/孩子)+ 1 次 store,把元素向上/向下"挪坑",直到最后一次 store 回去。
  2. 逻辑与原来完全等价:比较大小、判断孩子是否存在的边界检查都未改。

  3. 性能提升:对于高度为 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 方法:

  1. Floyd 方法

    • 建堆:从最后一个非叶节点 k = ⌊ N / 2 ⌋ k=\lfloor N/2\rfloor k=N/2 递减到 1,一次性调用 sink(k)
    • 排序:标准 “交换堆顶与末尾→下沉堆顶” 循环。
  2. 插入式方法

    • 建堆:从空堆开始,依次 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;
}
二、实测与估算结果

image.png

N N NFloyd 堆排序 比较次数插入式建堆 比较次数
1 0 3 10^3 10316,83617,246
1 0 6 10^6 10636,795,78237,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×1072,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 103log2103106log21061039.9710619.932,000 倍,也非常吻合 N log ⁡ N N\log N NlogN 模型。

三、对 N = 1 0 9 N=10^9 N=109 的估算
  1. 理论模型
    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)CNlog2N,log2(109)29.90

  2. 经验常数
    从测量中可粗略算出:
    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 CFloyd10619.933.68×1071.85,CInsert10619.933.72×1071.87

  3. 代入估算
    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.905.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.905.59×1010

四、汇总表
N N NFloyd cmpInsert cmp
1 0 3 10^3 10316,83617,246
1 0 6 10^6 10636,795,78237,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;
}

相关文章:

  • Android完整开发环境搭建/Studio安装/NDK/本地Gradle下载配置/创建AVD/运行一个Android项目/常用插件
  • APP、游戏、网站被黑客攻击了怎么解决?
  • 机器学习之三:归纳学习
  • 通俗易懂一文讲透什么是 MCP?
  • EMQX​​ 默认安装后常用端口及其功能
  • Opnelayers:向某个方向平移指定的距离
  • C++初阶-模板初阶
  • 业务中台与数据中台:企业数字化转型的核心引擎
  • 【源码】【Java并发】【ConcurrentHashMap】适合中学体质的ConcurrentHashMap
  • 全球城市范围30米分辨率土地覆盖数据(1985-2020)
  • MCP协议:AI生态的统一标准
  • ppt章节页怎么做好看?ppt章节页模板
  • 京东商品详情数据爬取难度分析与解决方案
  • 线上线程池的调优与监控 - Java架构师面试实战
  • C++ 基础内容入门
  • 服务器ubuntu镜像磁盘空间怎么管理
  • Java学习--HashMap
  • Nacos简介—4.Nacos架构和原理二
  • Rabbit MQ的基础认识
  • Support for password authentication was removed on August 13, 2021
  • 古籍新书·2025年春季|中国土司制度史料集成
  • A股三大股指收跌:地产股领跌,银行股再度走强
  • 全国电影工作会:聚焦扩大电影国际交流合作,提升全球影响力
  • 五一期间上海景观照明开启重大活动模式,外滩不展演光影秀
  • 广汽集团一季度净亏损7.3亿元,同比转亏,总销量下滑9%
  • 中越海警2025年第一次北部湾联合巡逻圆满结束