
在 C++ 的 STL 容器中,priority_queue(优先级队列)是基于堆(Heap)结构实现的自适应容器适配器,它能保证队列队首元素始终是当前队列中优先级最高 / 最低的元素,无需手动维护堆结构,是处理「优先级调度、TOP-K 问题、贪心算法」等场景的核心工具。
深度剖析 C++ priority_queue
一、priority_queue 核心认知
1. 定义与本质
priority_queue是容器适配器(而非底层容器),STL 提供的「容器适配器」是基于底层容器(如vector、deque)封装的接口,屏蔽底层实现细节,只暴露符合需求的上层接口。它的核心特性:
- 有序性:队首元素永远是当前优先级极值(最大或最小);
- 底层结构:默认使用大顶堆(max-heap),底层依赖
vector存储数据;- 访问限制:只能访问队首元素(
top()),无法随机访问或遍历中间元素;- 操作效率:插入、删除堆顶元素的时间复杂度为 O(log n),访问堆顶为 O (1)。
2. 与普通队列(queue)的区别
普通队列遵循 FIFO(先进先出) 规则,而
priority_queue完全打破顺序限制,只按优先级排序:
特性 queue(普通队列) priority_queue(优先级队列) 出队规则 先进先出 优先级最高 / 最低先出 元素顺序 插入顺序 动态维护优先级堆序 访问方式 队首(front)、队尾(back) 仅队首(top) 底层实现 deque/list 堆(heap)+ 底层容器(默认 vector) 3. 底层堆原理
priority_queue底层是完全二叉堆(Complete Binary Heap),分为两种:
- 大顶堆:父节点值 ≥ 子节点值,堆顶是最大值(默认);
- 小顶堆:父节点值 ≤ 子节点值,堆顶是最小值。
堆的存储:用连续数组(vector) 模拟完全二叉树,索引规则:
- 父节点索引:
i- 左子节点索引:
2*i + 1- 右子节点索引:
2*i + 2STL 提供
make_heap、push_heap、pop_heap算法,priority_queue直接封装这些算法,无需手动调用。
二、priority_queue 基础语法
1. 头文件
priority_queue定义在<queue>头文件中,使用前必须包含:
#include <queue>2. 模板原型
C++ 中
priority_queue的完整模板声明:
template <class T, class Container = vector<T>, class Compare = less<T>> class priority_queue;3. 核心成员函数
函数 功能说明 push(val)插入元素 val,自动调整堆结构 pop()删除堆顶元素(优先级最高 / 最低),自动调整堆 top()返回堆顶元素的引用(只读,不删除) empty()判断队列是否为空,空返回 true size()返回队列中元素的个数 emplace(args)原地构造元素,比 push 更高效(C++11) swap(pq)交换两个优先级队列的内容
代码实现
1. 比较器设计
template<class T> class Less { public: bool operator()(const T& x, const T& y) const { return x < y; // 用于构建最大堆 } }; template<class T> class Greater { public: bool operator()(const T& x, const T& y) const { return x > y; } }两种策略:
Less:创建最大堆(默认)
Greater:创建最小堆
2. 优先队列类模板
template<class T, class Container=std::vector<T>, class Compare=Less<T>> class priority_queue { // 实现细节... };模板参数说明:
T:元素类型
Container:底层容器,默认std::vector<T>
Compare:比较器类型,默认Less<T>
三、核心算法详解
二叉堆使用数组(或向量)存储,具有以下父子节点关系:
父节点索引:
parent = (child - 1) / 2左子节点:
left_child = parent * 2 + 1右子节点:
right_child = parent * 2 + 21. 向上调整(adjust_up)
void adjust_up(int child) { Compare com; int parent = (child - 1) / 2; while (child > 0) { if (com(_con[parent], _con[child])) // 父节点优先级更低 { swap(_con[parent], _con[child]); child = parent; parent = (child - 1) / 2; } else { break; // 堆性质已满足 } } }算法流程:
比较子节点与其父节点的优先级
如果子节点优先级更高,交换它们
继续向上比较,直到根节点或满足堆性质
时间复杂度:O(log n)
2. 向下调整(adjust_down)
void adjust_down(int parent) { Compare com; size_t child = parent * 2 + 1; // 左子节点 while (child < _con.size()) { // 选择优先级更高的子节点 if (child + 1 < _con.size() && com(_con[child], _con[child + 1])) ++child; // 右子节点优先级更高 // 比较父节点和优先级更高的子节点 if (com(_con[parent], _con[child])) { swap(_con[child], _con[parent]); parent = child; child = parent * 2 + 1; } else { break; // 堆性质已满足 } } }算法流程:
找到父节点的两个子节点
选择优先级更高的子节点
比较父节点与该子节点的优先级
如果子节点优先级更高,交换它们
继续向下调整,直到叶子节点或满足堆性质
时间复杂度:O(log n)
四、主要操作实现
1. 插入操作(push)
void push(const T& x) { _con.push_back(x); // 步骤1:放入末尾 adjust_up(_con.size() - 1); // 步骤2:向上调整 }2. 删除操作(pop)
void pop() { if (empty()) return; // 边界检查 swap(_con[0], _con[_con.size() - 1]); // 步骤1:交换首尾 _con.pop_back(); // 步骤2:删除尾部 if (!empty()) adjust_down(0); // 步骤3:向下调整 }3. 构建堆(Heapify)
priority_queue(InputIterator first, InputIterator last) :_con(first, last) { // 从最后一个非叶子节点开始向下调整 for (int i = (_con.size() - 2) / 2; i >= 0; i--) { adjust_down(i); } }算法分析:
最后一个非叶子节点索引:
(size - 2) / 2自底向上构建堆
时间复杂度:O(n),比逐个插入的O(n log n)更高效
五、设计模式与优化
1. 策略模式的应用
通过模板参数
Compare实现策略模式,用户可自定义比较逻辑:
// 最大堆(默认) wxx::priority_queue<int> max_heap; // 最小堆 wxx::priority_queue<int, std::vector<int>, Greater<int>> min_heap; // 自定义比较器 struct CompareByLength { bool operator()(const string& a, const string& b) const { return a.length() < b.length(); // 按长度构建最大堆 } }; wxx::priority_queue<string, vector<string>, CompareByLength> str_heap;2. 容器适配器模式
优先队列是典型的容器适配器:
不直接管理内存
依赖于底层容器(默认
std::vector)提供统一的接口抽象
3. 迭代器支持
通过迭代器构造函数,支持从各种容器初始化:
std::vector<int> vec = {3, 1, 4, 1, 5}; wxx::priority_queue<int> pq(vec.begin(), vec.end());
六、实际应用场景
1. 任务调度系统
struct Task { int priority; string name; // 重载<运算符,优先级数值小的先执行 bool operator<(const Task& other) const { return priority > other.priority; // 最小堆 } }; wxx::priority_queue<Task> task_queue;2. 求Top K问题
// 求数组中最大的k个元素 vector<int> findTopK(const vector<int>& nums, int k) { wxx::priority_queue<int, vector<int>, Greater<int>> min_heap; for (int num : nums) { if (min_heap.size() < k) { min_heap.push(num); } else if (num > min_heap.top()) { min_heap.pop(); min_heap.push(num); } } vector<int> result; while (!min_heap.empty()) { result.push_back(min_heap.top()); min_heap.pop(); } return result; }3. 合并K个有序链表
struct ListNode { int val; ListNode* next; bool operator<(const ListNode* other) const { return val > other->val; // 最小堆 } }; ListNode* mergeKLists(vector<ListNode*>& lists) { wxx::priority_queue<ListNode*, vector<ListNode*>, function<bool(ListNode*, ListNode*)>> pq([](ListNode* a, ListNode* b) { return a->val > b->val; }); // 合并逻辑... }
仿函数
1. 仿函数是什么?
是一个对象,里面可以有成员变量 + 重载 operator ()。
struct Cmp { bool operator()(int a, int b) { return a > b; } };特点:
- 是对象
- 可以存数据(状态)
- 可以被模板识别
- 编译器能直接内联,0 开销
2. 函数指针是什么?
就是一个变量,存的是函数的内存地址。
bool cmp(int a, int b) { return a > b; } // 函数指针 bool (*fp)(int, int) = cmp;特点:
- 只是个地址
- 不携带任何数据
- 无法保存状态
- 编译器很难优化
功能 函数指针 仿函数 本质 函数地址 对象 能否做模板参数 ❌ 不能 ✅ 可以 能否保存状态(成员变量) ❌ 不能 ✅ 可以 效率 低(函数调用) 极高(可内联) 多态能力 弱 强 STL 支持 差 完美支持
