📌 专栏说明:本专栏「从零开始学C++」面向完全零基础的同学,每篇文章聚焦一个核心概念,用最通俗的语言 + 完整可运行的代码帮你把知识点吃透。
C++相关专题链接🔗:
本文主题 :priority_queue(优先级队列)的基本使用与模拟实现 + 仿函数(函数对象)。
作者:小小de风呀
🧭 前言
在上一篇中,我们已经学习了 stack(栈)和 queue(队列)。今天来认识一个功能更强大的"特殊队列"------ priority_queue(优先级队列)。
普通队列是"先进先出",而优先级队列则是"按优先级出队"------每次出队的永远是当前优先级最高(或最低)的那个元素。
此外,为了能灵活地自定义"谁的优先级更高",我们还会介绍一个非常实用的 C++ 特性------仿函数(Functor)。
学会这两个知识点,你处理排序、调度、算法竞赛题目的能力将大幅提升!
一、priority_queue 基本使用
简介与作用
priority_queue 是 C++ STL 中的优先级队列 容器适配器,定义在头文件 <queue> 中。
它的底层是一个堆(Heap)结构,默认是大堆(最大堆) ,即堆顶永远是所有元素中最大的那个。
常用操作一览:
| 操作 | 说明 |
|---|---|
push(val) |
插入元素 |
pop() |
删除堆顶元素(最大值) |
top() |
访问堆顶元素(只读) |
empty() |
判断是否为空 |
size() |
返回元素个数 |
模板参数(三个):
priority_queue<Type, Container, Compare>
Type:元素类型Container:底层容器,默认vector<Type>Compare:比较方式,默认less<Type>(大堆)
💡 大堆 vs 小堆:
- 默认
less<T>→ 大堆,堆顶是最大值(最大优先)- 改用
greater<T>→ 小堆,堆顶是最小值(最小优先)
代码示例
示例一:默认大堆的基本用法
cpp
#include <iostream>
#include <queue>
using namespace std;
int main() {
priority_queue<int> pq; // 默认大堆
pq.push(3);
pq.push(1);
pq.push(7);
pq.push(5);
pq.push(2);
cout << "依次弹出(从大到小):";
while (!pq.empty()) {
cout << pq.top() << " "; // 查看堆顶
pq.pop(); // 弹出堆顶
}
cout << endl;
return 0;
}
输出结果:
依次弹出(从大到小):7 5 3 2 1
✅ 无论插入顺序如何,每次弹出的都是当前最大值。
示例二:使用 greater 构建小堆
cpp
#include <iostream>
#include <queue>
#include <functional> // greater 在此头文件中
using namespace std;
int main() {
// 小堆:堆顶是最小值
priority_queue<int, vector<int>, greater<int>> minPQ;
minPQ.push(3);
minPQ.push(1);
minPQ.push(7);
minPQ.push(5);
minPQ.push(2);
cout << "依次弹出(从小到大):";
while (!minPQ.empty()) {
cout << minPQ.top() << " ";
minPQ.pop();
}
cout << endl;
return 0;
}
输出结果:
依次弹出(从小到大):1 2 3 5 7
示例三:存放自定义结构体(按分数排序)
当元素是自定义类型时,需要告诉 priority_queue 怎么比较大小。
cpp
#include <iostream>
#include <queue>
#include <string>
using namespace std;
struct Student {
string name;
int score;
// 重载 < 运算符,让 priority_queue 知道如何比较
// 分数高的优先级更高(大堆逻辑)
bool operator<(const Student& other) const {
return score < other.score; // 分数小的"小",所以分数大的排前面
}
};
int main() {
priority_queue<Student> pq;
pq.push({"小明", 88});
pq.push({"小红", 95});
pq.push({"小刚", 76});
pq.push({"小美", 100});
cout << "按分数从高到低出队:" << endl;
while (!pq.empty()) {
Student s = pq.top();
pq.pop();
cout << s.name << " " << s.score << " 分" << endl;
}
return 0;
}
输出结果:
按分数从高到低出队:
小美 100 分
小红 95 分
小明 88 分
小刚 76 分
⚠️ 经典 Bug:误用 top() 后未 pop(),陷入死循环
cpp
// ❌ 错误写法
while (!pq.empty()) {
cout << pq.top() << endl;
// 忘记写 pq.pop(),堆顶永远不变 → 死循环!
}
// ✅ 正确写法
while (!pq.empty()) {
cout << pq.top() << endl;
pq.pop(); // 必须弹出,才能访问下一个元素
}
二、priority_queue 的底层原理:堆
简介与作用
要真正理解 priority_queue,就必须了解它的底层数据结构------堆(Heap)。
堆是一棵满足特定规则的完全二叉树:
- 大堆(最大堆):每个节点的值 ≥ 其所有子节点的值。根节点是最大值。
- 小堆(最小堆):每个节点的值 ≤ 其所有子节点的值。根节点是最小值。
堆通常用数组 来存储,对于下标为 i 的节点:
- 左子节点:
2*i + 1 - 右子节点:
2*i + 2 - 父节点:
(i - 1) / 2
核心操作:
- 向上调整(AdjustUp):插入元素后,将新元素与父节点比较,若违反堆性质则交换,直到满足为止。
- 向下调整(AdjustDown):删除堆顶后,将最后一个元素移到堆顶,再向下比较交换,直到满足为止。
💡 这就是为什么
push和pop的时间复杂度都是 O(log n)------每次调整最多走树的高度步。
代码示例
示例四:可视化大堆的插入过程
cpp
#include <iostream>
#include <vector>
using namespace std;
// 向上调整(大堆)
void adjustUp(vector<int>& heap, int child) {
while (child > 0) {
int parent = (child - 1) / 2;
if (heap[parent] < heap[child]) { // 父节点比子节点小,违反大堆性质
swap(heap[parent], heap[child]);
child = parent;
} else {
break;
}
}
}
// 插入元素并打印堆当前状态
void pushAndPrint(vector<int>& heap, int val) {
heap.push_back(val);
adjustUp(heap, heap.size() - 1);
cout << "插入 " << val << " 后,堆数组:[";
for (int i = 0; i < (int)heap.size(); i++) {
cout << heap[i];
if (i != (int)heap.size() - 1) cout << ", ";
}
cout << "] → 堆顶(最大值)= " << heap[0] << endl;
}
int main() {
vector<int> heap; // 用数组模拟堆
cout << "====== 大堆插入过程演示 ======" << endl;
pushAndPrint(heap, 3);
pushAndPrint(heap, 1);
pushAndPrint(heap, 7);
pushAndPrint(heap, 5);
pushAndPrint(heap, 2);
cout << endl;
cout << "最终堆数组:[";
for (int i = 0; i < (int)heap.size(); i++) {
cout << heap[i];
if (i != (int)heap.size() - 1) cout << ", ";
}
cout << "]" << endl;
// 验证堆的父子关系
cout << endl;
cout << "====== 验证父子节点关系 ======" << endl;
for (int i = 1; i < (int)heap.size(); i++) {
int parent = (i - 1) / 2;
cout << "节点[" << i << "]=" << heap[i]
<< " 父节点[" << parent << "]=" << heap[parent]
<< (heap[parent] >= heap[i] ? " ✓ 满足大堆" : " ✗ 违反大堆!")
<< endl;
}
return 0;
}
输出结果:
====== 大堆插入过程演示 ======
插入 3 后,堆数组:[3] → 堆顶(最大值)= 3
插入 1 后,堆数组:[3, 1] → 堆顶(最大值)= 3
插入 7 后,堆数组:[7, 1, 3] → 堆顶(最大值)= 7
插入 5 后,堆数组:[7, 5, 3, 1] → 堆顶(最大值)= 7
插入 2 后,堆数组:[7, 5, 3, 1, 2] → 堆顶(最大值)= 7
最终堆数组:[7, 5, 3, 1, 2]
====== 验证父子节点关系 ======
节点[1]=5 父节点[0]=7 ✓ 满足大堆
节点[2]=3 父节点[0]=7 ✓ 满足大堆
节点[3]=1 父节点[1]=5 ✓ 满足大堆
节点[4]=2 父节点[1]=5 ✓ 满足大堆
✅ 结合图示和代码输出可以清晰地看到:每次插入后,
adjustUp都会自动把元素"冒泡"到正确的位置,始终保证堆顶是最大值。最终数组[7, 5, 3, 1, 2]与图示完全一致!
三、手动模拟实现 priority_queue
简介与作用
在学会使用 priority_queue 之后,我们来"造轮子"------自己动手实现一个简版的优先级队列。
这样做的好处:
- 深刻理解堆的工作原理,不再是黑箱。
- 掌握向上调整、向下调整等核心算法。
- 为后续学习算法竞赛、操作系统调度等打基础。
我们实现的版本支持:push、pop、top、empty、size,并且通过仿函数模板参数支持自定义比较规则(大堆/小堆均可),与 STL 的设计思路一致。
代码示例
示例五:完整手写 priority_queue(支持大堆/小堆切换)
cpp
#include <iostream>
#include <vector>
#include <functional> // less, greater
using namespace std;
template<class T, class Container = vector<T>, class Compare = less<T>>
class MyPriorityQueue {
private:
Container _con; // 底层容器,用数组(vector)模拟堆
Compare _comp; // 比较器(仿函数)
// 向上调整:插入新元素后维护堆性质
void adjustUp(int child) {
int parent = (child - 1) / 2;
while (child > 0) {
// _comp(a, b):若 a 的优先级"低于"b,则为 true
// 大堆:less,当 parent < child 时,parent 优先级更低,需要交换
if (_comp(_con[parent], _con[child])) {
swap(_con[parent], _con[child]);
child = parent;
parent = (child - 1) / 2;
} else {
break;
}
}
}
// 向下调整:删除堆顶后维护堆性质
void adjustDown(int parent) {
int size = _con.size();
int child = 2 * parent + 1; // 左子节点
while (child < size) {
// 找左右子节点中优先级更高的那个
if (child + 1 < size && _comp(_con[child], _con[child + 1])) {
child++; // 右子节点优先级更高
}
// 如果子节点优先级高于父节点,则交换
if (_comp(_con[parent], _con[child])) {
swap(_con[parent], _con[child]);
parent = child;
child = 2 * parent + 1;
} else {
break;
}
}
}
public:
// 插入元素
void push(const T& val) {
_con.push_back(val);
adjustUp(_con.size() - 1); // 新元素在末尾,向上调整
}
// 删除堆顶元素
void pop() {
if (empty()) return;
swap(_con[0], _con[_con.size() - 1]); // 堆顶与末尾交换
_con.pop_back(); // 删除末尾(原堆顶)
adjustDown(0); // 新堆顶向下调整
}
// 访问堆顶
const T& top() const {
return _con[0];
}
bool empty() const {
return _con.empty();
}
size_t size() const {
return _con.size();
}
};
int main() {
cout << "====== 大堆测试 ======" << endl;
MyPriorityQueue<int> maxPQ; // 默认 less<int>,大堆
maxPQ.push(3);
maxPQ.push(1);
maxPQ.push(7);
maxPQ.push(5);
maxPQ.push(2);
cout << "依次弹出:";
while (!maxPQ.empty()) {
cout << maxPQ.top() << " ";
maxPQ.pop();
}
cout << endl;
cout << "====== 小堆测试 ======" << endl;
MyPriorityQueue<int, vector<int>, greater<int>> minPQ; // greater<int>,小堆
minPQ.push(3);
minPQ.push(1);
minPQ.push(7);
minPQ.push(5);
minPQ.push(2);
cout << "依次弹出:";
while (!minPQ.empty()) {
cout << minPQ.top() << " ";
minPQ.pop();
}
cout << endl;
return 0;
}
输出结果:
====== 大堆测试 ======
依次弹出:7 5 3 2 1
====== 小堆测试 ======
依次弹出:1 2 3 5 7
✅ 自己实现的
MyPriorityQueue与 STL 的priority_queue行为完全一致!
⚠️ 经典 Bug:向下调整时忘记检查右子节点是否存在
cpp
// ❌ 错误写法:直接比较 child 和 child+1,没有判断 child+1 是否越界
if (_comp(_con[child], _con[child + 1])) { // 当 child 是最后一个节点时,child+1 越界!
child++;
}
// ✅ 正确写法:先判断 child+1 < size
if (child + 1 < size && _comp(_con[child], _con[child + 1])) {
child++;
}
四、仿函数(Functor)
简介与作用
仿函数(Functor) ,也叫函数对象 ,是一种重载了 operator() 的类。重载之后,这个类的对象就可以像函数一样被调用。
听起来有点绕,看一个例子就清楚了:
cpp
MyFunctor f;
f(3, 5); // 这里 f 不是函数,而是一个对象,但它"长得像函数调用"
为什么需要仿函数?
- 可以携带状态:普通函数没有"记忆",而仿函数是对象,可以有成员变量记录状态。
- 灵活传递比较规则 :STL 容器(如
priority_queue、sort)通过模板参数接收仿函数,实现自定义排序。 - 比函数指针更高效:仿函数调用可以被编译器内联优化,性能更好。
💡 你已经见过仿函数了!前面用到的
less<int>和greater<int>就是 STL 提供的标准仿函数。
代码示例
示例六:手写一个最简单的仿函数
cpp
#include <iostream>
using namespace std;
// 定义一个仿函数类
class Add {
public:
int operator()(int a, int b) const {
return a + b;
}
};
int main() {
Add add; // 创建仿函数对象
cout << add(3, 5) << endl; // 像函数一样调用,输出 8
cout << add(10, 20) << endl; // 输出 30
// 也可以直接用匿名对象调用
cout << Add()(100, 200) << endl; // 输出 300
return 0;
}
输出结果:
8
30
300
示例七:带状态的仿函数(记录调用次数)
cpp
#include <iostream>
using namespace std;
class Counter {
private:
int count = 0; // 记录状态(调用次数)
public:
void operator()() {
count++;
cout << "第 " << count << " 次被调用" << endl;
}
int getCount() const {
return count;
}
};
int main() {
Counter c;
c(); // 第 1 次
c(); // 第 2 次
c(); // 第 3 次
cout << "总共调用了 " << c.getCount() << " 次" << endl;
return 0;
}
输出结果:
第 1 次被调用
第 2 次被调用
第 3 次被调用
总共调用了 3 次
✅ 普通函数做不到"记录自己被调用了多少次",但仿函数可以!
示例八:自定义仿函数用于 sort 排序
cpp
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
// 自定义比较仿函数:按绝对值从小到大排序
class AbsCmp {
public:
bool operator()(int a, int b) const {
return abs(a) < abs(b);
}
};
int main() {
vector<int> v = {-5, 3, -1, 8, -2, 6};
// 使用自定义仿函数排序
sort(v.begin(), v.end(), AbsCmp());
cout << "按绝对值从小到大排序:";
for (int x : v) {
cout << x << " ";
}
cout << endl;
return 0;
}
输出结果:
按绝对值从小到大排序:-1 -2 3 -5 6 8
示例九:自定义仿函数用于 priority_queue
cpp
#include <iostream>
#include <queue>
#include <string>
using namespace std;
struct Task {
string name;
int priority; // 数字越大,优先级越高
};
// 自定义比较仿函数
struct TaskCmp {
bool operator()(const Task& a, const Task& b) const {
// 返回 true 表示 a 的优先级"低于"b(a 排在后面)
return a.priority < b.priority;
}
};
int main() {
priority_queue<Task, vector<Task>, TaskCmp> taskQueue;
taskQueue.push({"写作业", 2});
taskQueue.push({"打游戏", 1});
taskQueue.push({"吃饭", 5});
taskQueue.push({"睡觉", 4});
taskQueue.push({"写代码", 3});
cout << "任务按优先级执行顺序:" << endl;
while (!taskQueue.empty()) {
Task t = taskQueue.top();
taskQueue.pop();
cout << "优先级 " << t.priority << " → " << t.name << endl;
}
return 0;
}
输出结果:
任务按优先级执行顺序:
优先级 5 → 吃饭
优先级 4 → 睡觉
优先级 3 → 写代码
优先级 2 → 写作业
优先级 1 → 打游戏
示例十:STL 内置仿函数一览
C++ 在 <functional> 中提供了很多开箱即用的仿函数:
cpp
#include <iostream>
#include <functional>
using namespace std;
int main() {
// 算术仿函数
cout << plus<int>()(3, 5) << endl; // 3+5 = 8
cout << minus<int>()(10, 3) << endl; // 10-3 = 7
cout << multiplies<int>()(4, 5) << endl; // 4*5 = 20
cout << divides<int>()(10, 2) << endl; // 10/2 = 5
cout << modulus<int>()(10, 3) << endl; // 10%3 = 1
cout << negate<int>()(5) << endl; // -5
cout << "---" << endl;
// 比较仿函数
cout << less<int>()(3, 5) << endl; // 3 < 5 → 1 (true)
cout << greater<int>()(3, 5) << endl; // 3 > 5 → 0 (false)
cout << less_equal<int>()(5, 5) << endl; // 5 <= 5 → 1 (true)
cout << greater_equal<int>()(3, 5) << endl; // 3 >= 5 → 0 (false)
cout << equal_to<int>()(5, 5) << endl; // 5 == 5 → 1 (true)
cout << not_equal_to<int>()(3, 5) << endl; // 3 != 5 → 1 (true)
return 0;
}
输出结果:
cpp
8
7
20
5
1
-5
---
1
0
1
0
1
1
五、综合练习:用 priority_queue 求 TopK 问题
简介与作用
TopK 问题 是一道经典的算法题:从 N 个数中找出最大(或最小)的 K 个数。
用 priority_queue 解决 TopK 非常优雅:
- 求前 K 大:维护一个大小为 K 的小堆,遍历完所有数后,堆中剩下的就是最大的 K 个。
- 求前 K 小:维护一个大小为 K 的大堆,遍历完所有数后,堆中剩下的就是最小的 K 个。
代码示例
示例十一:求数组中前 K 个最大的数
cpp
#include <iostream>
#include <queue>
#include <vector>
using namespace std;
// 返回数组中最大的 k 个数
vector<int> topK(vector<int>& nums, int k) {
// 使用小堆,维护一个大小为 k 的窗口
priority_queue<int, vector<int>, greater<int>> minHeap;
for (int num : nums) {
minHeap.push(num);
if ((int)minHeap.size() > k) {
minHeap.pop(); // 堆顶是最小值,超出 k 个时弹出最小值
}
}
// 收集结果
vector<int> result;
while (!minHeap.empty()) {
result.push_back(minHeap.top());
minHeap.pop();
}
return result;
}
int main() {
vector<int> nums = {3, 1, 7, 5, 2, 9, 4, 8, 6};
int k = 4;
vector<int> res = topK(nums, k);
cout << "前 " << k << " 个最大的数:";
for (int x : res) {
cout << x << " ";
}
cout << endl;
return 0;
}
输出结果:
前 4 个最大的数:6 7 8 9
💡 用小堆(size = K)解决"最大 K 个",是非常经典的技巧,时间复杂度 O(N·log K),空间复杂度 O(K),效率远优于先排序再取前 K 个。
📝 总结
| 知识点 | 核心要点 |
|---|---|
| priority_queue 默认行为 | 大堆,堆顶是最大值,头文件 <queue> |
| 切换为小堆 | 第三个模板参数改为 greater<T> |
| 三个模板参数 | <元素类型, 底层容器, 比较器> |
| 底层结构 | 完全二叉树(用数组存储),大/小堆 |
| push 时间复杂度 | O(log n),执行向上调整 |
| pop 时间复杂度 | O(log n),执行向下调整 |
| 仿函数定义 | 重载了 operator() 的类 |
| 仿函数优势 | 可携带状态、灵活传参、编译器可内联优化 |
| STL 内置仿函数 | less、greater、plus、minus 等,头文件 <functional> |
| TopK 经典用法 | 最大 K 个用小堆,最小 K 个用大堆 |
🚀 下期预告
掌握了 priority_queue 和仿函数之后,下一篇,我们将重点讲解 C++ 中至关重要的面向对象特性------继承,深入拆解继承的核心概念、语法规则、分类特性以及实际开发中的应用场景,助力大家进一步掌握 C++ 面向对象的编程精髓。
de风小提醒: 堆调整算法是数据结构中的重中之重,建议大家亲手写 3 遍以上,直到能闭着眼写出来为止!面试时这是最常考的手写算法之一。
如果本篇博客对你有帮助,欢迎点赞👍、收藏⭐、评论💬,你的支持是我创作的最大动力!
