Skip to content

第9章 priority_queue 容器详解 ​

priority_queue(优先队列)是 STL 中的容器适配器,底层默认基于 vector + 堆(heap)实现,会自动将元素按优先级排序,保证堆顶元素始终是优先级最高的元素,遵循"堆顶优先"原则。


🎯 本章学习目标 ​

学完本章,你将能够:

  1. 理解优先队列的核心思想(按优先级出队)
  2. 熟练使用 priority_queue 的基本操作(push、pop、top)
  3. 掌握大顶堆和小顶堆的定义与使用
  4. 理解优先队列的底层实现(二叉堆)
  5. 掌握自定义比较函数的写法
  6. 能用优先队列解决 TopK 等经典问题
  7. 理解优先队列与普通队列、set 的区别
  8. 根据场景选择合适的优先队列配置

🗺️ 本章知识图谱 ​

priority_queue 优先队列
├── 核心特性
│   ├── 按优先级排序
│   ├── 容器适配器
│   ├── 仅堆顶操作
│   └── 无迭代器
├── 基本用法
│   ├── 定义与头文件
│   ├── push(入队)
│   ├── pop(出队)
│   ├── top(访问堆顶)
│   ├── empty(判空)
│   └── size(大小)
├── 堆的类型
│   ├── 大顶堆(默认)
│   ├── 小顶堆(greater)
│   └── 自定义比较
├── 底层原理
│   ├── 二叉堆结构
│   ├── 上滤(插入)
│   ├── 下滤(删除)
│   └── 建堆过程
├── 经典应用
│   ├── TopK 问题
│   ├── 任务调度
│   ├── 合并 K 个有序链表
│   └── 哈夫曼编码
├── 注意事项
│   ├── 不可遍历
│   ├── 不能修改元素
│   └── 空堆访问 top 崩溃
└── 适用场景与选型

一、priority_queue 核心特性 ​

🍎 生活比喻:priority_queue 就像医院的急诊室——不是先来的先看,而是病情最严重(优先级最高)的病人先看。每个病人来的时候,护士会根据病情安排到合适的位置,医生每次都先处理最紧急的那个。

  1. 堆结构:底层默认是大顶堆(最大值优先),可自定义排序规则;
  2. 自动排序:插入元素时自动调整堆结构,保证优先级有序;
  3. 受限访问:只能访问**堆顶(top)**元素,不能访问其他元素;
  4. 高效操作:插入、删除堆顶效率为 O(log n);
  5. 无迭代器:不支持遍历,无法访问非堆顶元素。

🔥 高频考点:优先队列的底层是堆、插入删除 O(log n)、默认大顶堆,这些是选择题的高频考点。


二、priority_queue 基础用法 ​

使用 priority_queue 需包含 <queue> 头文件(和 queue 同一个头文件)。

cpp
#include <iostream>
#include <queue>    // 优先队列头文件
#include <vector>
using namespace std;

int main() {
    // 1. 默认大顶堆(最大值优先)
    priority_queue<int> pq;
    
    // 2. 插入元素(自动调整堆结构)
    pq.push(30);
    pq.push(10);
    pq.push(50);
    pq.push(20);
    
    // 3. 访问堆顶(优先级最高的元素)
    cout << "堆顶元素:" << pq.top() << endl;  // 输出 50
    
    // 4. 删除堆顶元素(自动调整堆结构)
    pq.pop();
    cout << "删除后堆顶:" << pq.top() << endl; // 输出 30
    
    // 5. 遍历输出(只能逐个取堆顶,会清空队列)
    cout << "依次出队:";
    while (!pq.empty()) {
        cout << pq.top() << " ";
        pq.pop();
    }
    // 输出:30 20 10
    cout << endl;
    
    // 6. 常用操作
    cout << "队列大小:" << pq.size() << endl;
    cout << "是否为空:" << (pq.empty() ? "是" : "否") << endl;
    
    return 0;
}

操作汇总表 ​

操作语法说明时间复杂度
插入push(元素)插入元素并自动调整堆O(log n)
删除堆顶pop()删除优先级最高的元素,无返回值O(log n)
访问堆顶top()返回堆顶元素(不删除),需确保非空O(1)
判空empty()为空返回 true,否则返回 falseO(1)
获取大小size()返回元素个数O(1)
清空无 clear()需循环 pop()O(n log n)

💡 小技巧:priority_queue 没有 clear() 方法,清空的方法是 while (!pq.empty()) pq.pop();,或者用技巧 priority_queue<int>().swap(pq);。


三、大顶堆与小顶堆 ​

1. 大顶堆(默认) ​

默认情况下,priority_queue 是大顶堆,值越大优先级越高,堆顶是最大值。

cpp
priority_queue<int> pq;  // 默认大顶堆
pq.push(3);
pq.push(1);
pq.push(5);
pq.push(2);
// 堆顶是 5,依次出队顺序:5 3 2 1

2. 小顶堆 ​

如果需要值越小优先级越高(小顶堆),定义时需要指定底层容器和比较函数:

cpp
// 小顶堆定义方式:priority_queue<类型, 底层容器, 比较函数>
priority_queue<int, vector<int>, greater<int>> pq_min;
pq_min.push(3);
pq_min.push(1);
pq_min.push(5);
// 堆顶是 1,依次出队顺序:1 3 5

⚠️ 易错点:小顶堆的定义语法比较复杂,很多同学记不住。记住格式:priority_queue<T, vector<T>, greater<T>>,三个参数分别是:元素类型、底层容器、比较方式。

3. 完整对比示例 ​

cpp
#include <iostream>
#include <queue>
#include <vector>
using namespace std;

int main() {
    priority_queue<int> max_heap;          // 大顶堆
    priority_queue<int, vector<int>, greater<int>> min_heap;  // 小顶堆
    
    int arr[] = {3, 1, 4, 1, 5, 9, 2, 6};
    for (int x : arr) {
        max_heap.push(x);
        min_heap.push(x);
    }
    
    cout << "大顶堆出队(从大到小):";
    while (!max_heap.empty()) {
        cout << max_heap.top() << " ";
        max_heap.pop();
    }
    cout << endl;  // 输出:9 6 5 4 3 2 1 1
    
    cout << "小顶堆出队(从小到大):";
    while (!min_heap.empty()) {
        cout << min_heap.top() << " ";
        min_heap.pop();
    }
    cout << endl;  // 输出:1 1 2 3 4 5 6 9
    
    return 0;
}

🔥 高频考点:默认大顶堆、小顶堆需要用 greater,这是选择题和填空题的常考点。


四、底层原理:二叉堆(进阶) ​

priority_queue 的底层是二叉堆(binary heap),这是一种完全二叉树,用数组存储。

1. 大顶堆的性质 ​

  • 是一棵完全二叉树;
  • 每个节点的值都大于等于它的左右子节点;
  • 根节点(堆顶)是整个堆中的最大值。

2. 数组表示 ​

因为是完全二叉树,可以用数组来存储:

  • 父节点下标为 i,左子节点下标为 2*i+1,右子节点下标为 2*i+2;
  • 子节点下标为 i,父节点下标为 (i-1)/2。

3. 插入操作(上滤) ​

  1. 把新元素放到数组末尾(堆的最后一个位置);
  2. 与父节点比较,如果比父节点大(大顶堆),就和父节点交换;
  3. 重复第 2 步,直到父节点更大或到达堆顶。

时间复杂度:O(log n)

4. 删除堆顶操作(下滤) ​

  1. 把堆顶元素保存起来(作为返回值);
  2. 把最后一个元素移到堆顶;
  3. 与左右子节点中较大的那个比较,如果比子节点小,就交换;
  4. 重复第 3 步,直到子节点都更小或到达叶子节点。

时间复杂度:O(log n)

🍎 生活比喻:插入就像新员工入职——先坐到最底层的位置,然后发现自己比上级厉害,就跟上级交换位置,一直升到合适的位置。删除堆顶就像CEO 离职——把最底层的员工拉到 CEO 位置,然后发现他管不了下属,就和更厉害的下属交换,一直降到合适的位置。


五、自定义比较(进阶) ​

除了默认的大顶堆和小顶堆,priority_queue 还支持自定义比较规则,用于复杂类型(如结构体、pair)。

示例:自定义结构体的比较 ​

cpp
#include <iostream>
#include <queue>
#include <string>
using namespace std;

// 学生结构体:姓名 + 分数
struct Student {
    string name;
    int score;
    Student(string n, int s) : name(n), score(s) {}
};

// 自定义比较:分数高的优先级高(大顶堆)
struct cmp_score {
    bool operator()(const Student& a, const Student& b) {
        return a.score < b.score;  // 分数高的在前面
    }
};

int main() {
    priority_queue<Student, vector<Student>, cmp_score> pq;
    pq.push(Student("张三", 85));
    pq.push(Student("李四", 95));
    pq.push(Student("王五", 90));
    
    while (!pq.empty()) {
        cout << pq.top().name << ":" << pq.top().score << "分" << endl;
        pq.pop();
    }
    // 输出:
    // 李四:95分
    // 王五:90分
    // 张三:85分
    
    return 0;
}

⚠️ 易错点:priority_queue 的比较函数写法和 sort 正好相反!sort 中 return a < b 是升序,但 priority_queue 中 return a < b 是大顶堆(大的在前)。记忆口诀:priority_queue 的比较函数定义的是"谁的优先级更低"。


六、与其他容器对比 ​

特性priority_queuesetqueue
顺序按优先级按 key 排序按插入顺序(FIFO)
插入O(log n)O(log n)O(1)
删除O(log n)(删堆顶)O(log n)O(1)(删队头)
访问只能访问堆顶可查找任意元素只能访问头尾
遍历不支持支持不支持
底层二叉堆红黑树deque
适用场景TopK、任务调度去重、查找排队、BFS

七、常见误区与注意事项 ​

⚠️ 易错点 1:认为 priority_queue 是 queue 的一种。虽然名字里有 queue,但它和普通 queue 的出队规则完全不同。普通 queue 是 FIFO,priority_queue 是按优先级出队。

⚠️ 易错点 2:小顶堆的写法记不住。记住模板:priority_queue<T, vector<T>, greater<T>>,三个参数缺一不可。

⚠️ 易错点 3:以为可以遍历。priority_queue 没有迭代器,不能遍历所有元素,只能通过 top() 访问堆顶,或者依次弹出。

⚠️ 易错点 4:空堆调用 top()/pop()。和 stack、queue 一样,空的优先队列调用 top() 或 pop() 会导致未定义行为(程序崩溃)。

⚠️ 易错点 5:可以修改元素。priority_queue 的元素不能直接修改(因为会破坏堆结构),如果需要修改,只能先弹出旧元素,再插入新元素。

⚠️ 易错点 6:比较函数和 sort 一样。priority_queue 的比较函数逻辑和 sort 是反的!less<T> 对应大顶堆(大的在前),greater<T> 对应小顶堆(小的在前)。


八、经典应用场景 ​

1. TopK 问题(算法题高频) ​

求数组中第 K 大的元素,或前 K 大的元素。这是优先队列最经典的应用。

**思路:**维护一个大小为 K 的小顶堆,遍历数组,如果当前元素比堆顶大,就弹出堆顶,插入当前元素。遍历结束后,堆顶就是第 K 大的元素。

cpp
// 求第 K 大的元素
int findKthLargest(vector<int>& nums, int k) {
    priority_queue<int, vector<int>, greater<int>> min_heap;
    for (int x : nums) {
        min_heap.push(x);
        if (min_heap.size() > k) {
            min_heap.pop();
        }
    }
    return min_heap.top();
}

🔥 高频考点:TopK 问题是面试和竞赛的必考题,必须掌握用小顶堆的解法。

2. 任务调度 ​

按优先级处理任务,优先级高的任务先执行。

3. 合并 K 个有序链表 ​

用小顶堆维护 K 个链表的当前节点,每次取出最小的节点加入结果。

4. 哈夫曼编码 ​

用优先队列每次取出两个权值最小的节点合并。


九、🔖 本章小结 ​

priority_queue 是一种按优先级排序的容器适配器,底层是二叉堆,插入删除都是 O(log n)。默认是大顶堆,用 greater<T> 可以变成小顶堆。它最经典的应用是 TopK 问题,也是任务调度、合并有序链表等问题的高效解法。使用时需注意:只能访问堆顶、不可遍历、空堆不能调用 top/pop。


十、📝 实战练习 ​

练习题 1(选择题) ​

题目: priority_queue 的底层数据结构是?

A. 红黑树 B. 二叉堆 C. 双向链表 D. 哈希表

🔑 点击查看答案与解析

答案: B

**解析:**priority_queue 底层是二叉堆(默认大顶堆),通常用 vector 作为底层容器。A 是 set/map 的底层结构,C 是 list 的底层结构,D 是 unordered_set/unordered_map 的底层结构。


练习题 2(判断题) ​

题目: priority_queue 默认是小顶堆(值越小优先级越高)( )

🔑 点击查看答案与解析

答案: 错误

**解析:**priority_queue 默认是大顶堆,值越大优先级越高,堆顶是最大值。如果需要小顶堆,要显式指定 greater<T>,如 priority_queue<int, vector<int>, greater<int>>。


练习题 3(选择题) ​

题目: priority_queue 的 push 操作时间复杂度是?

A. O(1) B. O(log n) C. O(n) D. O(n log n)

🔑 点击查看答案与解析

答案: B

**解析:**priority_queue 底层是二叉堆,插入元素需要进行"上滤"操作,最坏情况下要从堆底一直交换到堆顶,时间复杂度是 O(log n)。pop 操作也是 O(log n)。


练习题 4(程序阅读题) ​

题目: 阅读以下代码,写出输出结果:

cpp
#include <iostream>
#include <queue>
#include <vector>
using namespace std;

int main() {
    priority_queue<int, vector<int>, greater<int>> pq;
    pq.push(5);
    pq.push(2);
    pq.push(8);
    pq.push(1);
    cout << pq.top() << " ";
    pq.pop();
    cout << pq.top() << endl;
    return 0;
}
🔑 点击查看答案与解析

答案: 1 2

**解析:**这是一个小顶堆(因为用了 greater<int>),元素越小优先级越高。

  • 插入 5、2、8、1 后,堆顶是最小的 1
  • top() 输出 1
  • pop() 删除 1 后,新的堆顶是 2
  • top() 输出 2

最终输出:1 2


练习题 5(选择题) ​

题目: 以下关于 priority_queue 的说法,错误的是?

A. priority_queue 支持随机访问任意元素 B. priority_queue 的底层默认是 vector C. priority_queue 没有迭代器,不能遍历 D. priority_queue 的 top() 返回堆顶元素

🔑 点击查看答案与解析

答案: A

解析:

  • A 错误:priority_queue 只能访问堆顶元素,不能随机访问其他元素
  • B 正确:priority_queue 默认底层容器是 vector
  • C 正确:priority_queue 没有迭代器,不能遍历
  • D 正确:top() 返回优先级最高的堆顶元素

练习题 6(编程题) ​

题目: 输入 n 个整数,输出其中前 K 大的数(从大到小输出)。

**输入格式:**第一行两个整数 n 和 k,第二行 n 个整数 **输出格式:**一行,前 K 大的数,从大到小排列

样例输入:

8 3
3 1 4 1 5 9 2 6

样例输出:

9 6 5
🔑 点击查看答案与解析

参考代码(方法一:大顶堆,全部入队后取前 K 个):

cpp
#include <iostream>
#include <queue>
using namespace std;

int main() {
    int n, k;
    cin >> n >> k;
    priority_queue<int> pq;  // 大顶堆
    
    for (int i = 0; i < n; i++) {
        int x;
        cin >> x;
        pq.push(x);
    }
    
    // 依次取出前 K 个
    for (int i = 0; i < k; i++) {
        if (i > 0) cout << " ";
        cout << pq.top();
        pq.pop();
    }
    cout << endl;
    
    return 0;
}

参考代码(方法二:小顶堆维护 TopK,空间更优):

cpp
#include <iostream>
#include <queue>
#include <vector>
#include <algorithm>
using namespace std;

int main() {
    int n, k;
    cin >> n >> k;
    priority_queue<int, vector<int>, greater<int>> min_heap;  // 小顶堆
    
    for (int i = 0; i < n; i++) {
        int x;
        cin >> x;
        min_heap.push(x);
        if (min_heap.size() > k) {
            min_heap.pop();  // 堆中超过 K 个,弹出最小的
        }
    }
    
    // 小顶堆里存的是前 K 大的,需要倒序输出
    vector<int> result;
    while (!min_heap.empty()) {
        result.push_back(min_heap.top());
        min_heap.pop();
    }
    reverse(result.begin(), result.end());
    
    for (int i = 0; i < result.size(); i++) {
        if (i > 0) cout << " ";
        cout << result[i];
    }
    cout << endl;
    
    return 0;
}

解析:

  • 方法一:最简单,全部放入大顶堆,然后取前 K 个。时间 O(n log n),空间 O(n)。
  • 方法二:更优,用小顶堆维护大小为 K 的窗口。时间 O(n log k),空间 O(k)。当 n 很大而 k 很小时,方法二优势明显。

这就是经典的 TopK 问题,方法二是面试中的标准最优解。


练习题 7(编程题) ​

题目: 使用优先队列对一个数组进行排序(从小到大)。

**输入格式:**第一行一个整数 n,第二行 n 个整数 **输出格式:**一行,排序后的整数

样例输入:

5
3 1 4 1 5

样例输出:

1 1 3 4 5
🔑 点击查看答案与解析

参考代码:

cpp
#include <iostream>
#include <queue>
#include <vector>
using namespace std;

int main() {
    int n;
    cin >> n;
    // 用小顶堆,出来就是从小到大
    priority_queue<int, vector<int>, greater<int>> min_heap;
    
    for (int i = 0; i < n; i++) {
        int x;
        cin >> x;
        min_heap.push(x);
    }
    
    // 依次出队输出
    while (!min_heap.empty()) {
        cout << min_heap.top();
        min_heap.pop();
        if (!min_heap.empty()) cout << " ";
    }
    cout << endl;
    
    return 0;
}

**解析:**这就是堆排序(heap sort)的基本思想。把所有元素放入小顶堆,然后依次弹出,就得到了从小到大排序的结果。时间复杂度 O(n log n),和 sort 函数一样。

不过实际编程中直接用 sort 更方便,这道题主要是帮助理解优先队列的原理。


十一、📊 考点总结表 ​

知识点重要程度出题形式易错点
底层是二叉堆⭐⭐⭐⭐选择题与红黑树混淆
默认大顶堆⭐⭐⭐⭐⭐选择题、判断题以为默认是小顶堆
小顶堆写法(greater)⭐⭐⭐⭐⭐选择题、编程题语法记不住
push/pop 时间 O(log n)⭐⭐⭐⭐选择题以为是 O(1)
只能访问堆顶⭐⭐⭐选择题、判断题以为可以随机访问
无迭代器、不可遍历⭐⭐⭐判断题试图遍历
TopK 问题⭐⭐⭐⭐⭐编程题不会用小顶堆维护
比较函数与 sort 相反⭐⭐⭐选择题、编程题搞反大小顶堆
空堆调用 top/pop 崩溃⭐⭐⭐判断题忘记判空

⭐ 必背速记卡 ​

┌──────────────────────────────────────┐
│  priority_queue 必背速记卡            │
├──────────────────────────────────────┤
│  1. 优先队列,底层是二叉堆             │
│  2. 默认大顶堆,值越大优先级越高       │
│  3. 小顶堆写法:greater<T> + vector<T> │
│  4. push/pop 时间复杂度 O(log n)      │
│  5. top() 访问堆顶,O(1)              │
│  6. 只能访问堆顶,不能遍历             │
│  7. 没有迭代器,没有 clear()          │
│  8. 空堆调用 top/pop 会崩溃           │
│  9. 经典应用:TopK、任务调度           │
│  10. 比较函数和 sort 逻辑相反         │
└──────────────────────────────────────┘

百炼成钢,融会贯通