48.c++标准库里优先队列是怎么实现的?
🔬 c++标准库里优先队列是怎么实现的?
📋 总结
📖 内容概览
本文将详细介绍C++标准库中priority_queue(优先队列)的底层实现原理,包括堆的概念、建堆算法、堆操作,以及优先队列的各种使用方式。通过深入理解优先队列的实现机制,帮助开发者在实际项目中正确高效地使用这一数据结构。
📚 优先队列的基本概念
1.1 什么是优先队列
优先队列(Priority Queue)是一种特殊的队列,它的特点是:
- 元素按照优先级进行排序
- 每次取出的元素都是当前队列中优先级最高的元素
- 插入元素时会自动调整位置,保持优先级顺序
在C++ STL中,
priority_queue默认是最大堆(Max Heap),即每次取出的是最大元素。
1.2 优先队列的应用场景
优先队列常用于以下场景:
- 任务调度系统(按优先级处理任务)
- 图算法(如Dijkstra最短路径、Prim最小生成树)
- 排序算法(堆排序)
- 数据流中的中位数查找
- 霍夫曼编码 🎯 优先队列的底层实现
2.1 堆的概念
C++ STL中的优先队列基于堆(Heap)数据结构实现。堆是一种特殊的完全二叉树,具有以下性质:
- 最大堆:每个节点的值都大于或等于其子节点的值
- 最小堆:每个节点的值都小于或等于其子节点的值 堆通常用数组(vector)来表示,利用完全二叉树的性质进行索引:
- 对于索引为
i的节点:- 左子节点索引:
2i + 1 - 右子节点索引:
2i + 2 - 父节点索引:
(i - 1) / 2
- 左子节点索引:
2.2 优先队列的定义
在C++ STL中,priority_queue的定义如下:
template < class T, class Container = std::vector<T>, class Compare = std::less<typename Container::value_type>> class priority_queue;- T:存储的元素类型
- Container:底层容器类型,默认为
std::vector<T> - Compare:比较函数对象,默认为
std::less<>(最大堆) 🔧 堆的核心操作
3.1 下滤操作(Sift Down)
下滤操作用于维护堆的性质,当根节点被替换后,需要将新的根节点向下调整到合适位置。
void sift_down(vector<int>& heap, int start, int end) { int parent = start; int child = 2 * parent + 1; // 左子节点
while (child <= end) { // 选择左右子节点中较大的那个 if (child + 1 <= end && heap[child] < heap[child + 1]) { child++; }
// 如果父节点大于等于子节点,堆性质已满足 if (heap[parent] >= heap[child]) { return; } else { // 交换父节点和子节点 swap(heap[parent], heap[child]); // 继续向下调整 parent = child; child = 2 * parent + 1; } }}3.2 上滤操作(Sift Up)
上滤操作用于在插入新元素时,将新元素向上调整到合适位置,以维护堆的性质。
void sift_up(vector<int>& heap, int start) { int child = start; int parent = (child - 1) / 2; // 父节点
while (child > 0 && heap[child] > heap[parent]) { // 交换子节点和父节点 swap(heap[child], heap[parent]); // 继续向上调整 child = parent; parent = (child - 1) / 2; }}3.3 建堆算法
建堆算法使用Floyd算法,从最后一个非叶子节点开始,自下而上进行下滤操作。
void build_heap(vector<int>& heap) { int n = heap.size(); // 从最后一个非叶子节点开始,向前遍历 for (int i = n / 2 - 1; i >= 0; --i) { sift_down(heap, i, n - 1); }}建堆的时间复杂度为O(n),优于逐个插入元素的O(n log n)。 💡 优先队列的基本操作
4.1 优先队列的创建
#include <queue>#include <vector>#include <functional>
// 1. 默认最大堆std::priority_queue<int> max_heap;
// 2. 显式指定最大堆std::priority_queue<int, std::vector<int>, std::less<int>> max_heap2;
// 3. 最小堆std::priority_queue<int, std::vector<int>, std::greater<int>> min_heap;4.2 常用成员函数
| 成员函数 | 功能 | 时间复杂度 |
|---|---|---|
push(const T& value) | 插入元素 | O(log n) |
pop() | 删除堆顶元素 | O(log n) |
top() | 返回堆顶元素 | O(1) |
empty() | 检查是否为空 | O(1) |
size() | 返回元素个数 | O(1) |
4.3 优先队列的使用示例
#include <iostream>#include <queue>
int main() { // 创建最大堆 std::priority_queue<int> pq;
// 插入元素 pq.push(3); pq.push(1); pq.push(4); pq.push(5);
// 输出堆顶元素 std::cout << "堆顶元素: " << pq.top() << std::endl; // 输出: 5
// 弹出堆顶元素 pq.pop(); std::cout << "弹出后堆顶元素: " << pq.top() << std::endl; // 输出: 4
// 遍历所有元素(会破坏堆结构) std::cout << "所有元素: "; while (!pq.empty()) { std::cout << pq.top() << " "; pq.pop(); } // 输出: 4 3 1
return 0;}🚀 优先队列的高级使用
5.1 存储pair类型
优先队列可以存储pair类型,默认按照pair的第一个元素进行比较,第一个元素相同时比较第二个元素。
#include <iostream>#include <queue>#include <utility>#include <string>
int main() { // 最大堆(默认) std::priority_queue<std::pair<int, std::string>> pq;
pq.push({3, "apple"}); pq.push({1, "banana"}); pq.push({4, "cherry"}); pq.push({1, "date"});
// 遍历所有元素 while (!pq.empty()) { auto [priority, name] = pq.top(); std::cout << "优先级: " << priority << ", 名称: " << name << std::endl; pq.pop(); }
return 0;}输出结果:
优先级: 4, 名称: cherry优先级: 3, 名称: apple优先级: 1, 名称: date优先级: 1, 名称: banana5.2 自定义比较函数
5.2.1 重载operator<
对于自定义类型,可以通过重载operator<来定义比较规则:
#include <iostream>#include <queue>#include <string>
struct Task { int priority; std::string name;
// 重载operator<,定义最大堆 bool operator<(const Task& other) const { return priority < other.priority; } // 若要定义最小堆,返回 priority > other.priority};
int main() { std::priority_queue<Task> pq; pq.push({3, "Task A"}); pq.push({1, "Task B"}); pq.push({5, "Task C"});
std::cout << "任务: " << pq.top().name << ", 优先级: " << pq.top().priority << std::endl; // 输出: 任务: Task C, 优先级: 5
return 0;}5.2.2 使用函数对象
#include <iostream>#include <queue>#include <string>
struct Task { int priority; std::string name;};
// 自定义比较函数对象(最小堆)struct CompareTask { bool operator()(const Task& t1, const Task& t2) { return t1.priority > t2.priority; }};
int main() { std::priority_queue<Task, std::vector<Task>, CompareTask> pq; pq.push({3, "Task A"}); pq.push({1, "Task B"}); pq.push({5, "Task C"});
std::cout << "任务: " << pq.top().name << ", 优先级: " << pq.top().priority << std::endl; // 输出: 任务: Task B, 优先级: 1
return 0;}5.2.3 使用lambda表达式
C++11及以上支持使用lambda表达式作为比较函数:
#include <iostream>#include <queue>
int main() { // 定义lambda比较函数(最小堆) auto cmp = [](int a, int b) { return a > b; };
// 使用decltype获取lambda的类型 std::priority_queue<int, std::vector<int>, decltype(cmp)> pq(cmp);
pq.push(3); pq.push(1); pq.push(4);
// 输出所有元素 std::cout << "所有元素: "; while (!pq.empty()) { std::cout << pq.top() << " "; pq.pop(); } // 输出: 1 3 4
return 0;}🔍 优先队列的实现原理深度解析
6.1 push操作的实现
当调用push()插入元素时,优先队列的实现逻辑如下:
- 将新元素添加到底层容器的末尾
- 对新元素执行上滤操作,使其上升到合适位置
void push(const value_type& val) { // 1. 添加到末尾 c.push_back(val); // 2. 上滤操作 sift_up(c.size() - 1, val);}6.2 pop操作的实现
当调用pop()删除堆顶元素时,优先队列的实现逻辑如下:
- 将堆顶元素与底层容器的最后一个元素交换
- 删除最后一个元素(原堆顶元素)
- 对新的堆顶元素执行下滤操作,使其下降到合适位置
void pop() { // 1. 检查是否为空 if (c.empty()) return; // 2. 交换堆顶和最后一个元素 std::swap(c.front(), c.back()); // 3. 删除最后一个元素 c.pop_back(); // 4. 下滤操作 if (!c.empty()) { sift_down(0, c.front()); }}6.3 top操作的实现
top()操作非常简单,直接返回底层容器的第一个元素:
const_reference top() const { return c.front();}⏱️ 优先队列的性能分析
7.1 时间复杂度
| 操作 | 时间复杂度 | 说明 |
|---|---|---|
| 插入(push) | O(log n) | 需要上滤操作 |
| 删除(pop) | O(log n) | 需要下滤操作 |
| 访问堆顶(top) | O(1) | 直接返回第一个元素 |
| 建堆 | O(n) | Floyd算法 |
7.2 空间复杂度
优先队列的空间复杂度为O(n),其中n是元素个数,主要用于存储底层容器。
7.3 与其他容器的比较
| 容器 | 插入复杂度 | 删除复杂度 | 访问最大/最小元素 | 顺序 |
|---|---|---|---|---|
priority_queue | O(log n) | O(log n) | O(1) | 无序(仅堆顶有序) |
set | O(log n) | O(log n) | O(log n) | 有序 |
unordered_set | O(1) 平均 | O(1) 平均 | O(n) | 无序 |
vector | O(1) 尾部 | O(n) | O(n) | 有序/无序 |
| 🌟 优先队列的最佳实践 |
8.1 选择合适的比较方式
- 默认情况下,优先队列是最大堆
- 使用
std::greater<>可以创建最小堆 - 对于自定义类型,根据需要重载
operator<或提供比较函数
8.2 避免频繁的push和pop操作
- 频繁的push和pop操作会导致多次堆调整,影响性能
- 如果需要批量处理元素,考虑先构建堆,再进行操作
8.3 注意内存使用
- 优先队列的底层容器默认是
vector,会动态扩容 - 对于大量元素,可以使用
reserve()预先分配空间
8.4 自定义类型的注意事项
- 确保比较函数是严格弱序(Strict Weak Ordering)
- 比较函数应该是const成员函数或函数对象
- 避免在比较函数中修改对象状态 📋 总结
9.1 优先队列的核心要点
- 底层实现:基于堆数据结构,默认为最大堆
- 存储结构:使用vector作为底层容器,利用完全二叉树的性质
- 核心操作:上滤(插入)和下滤(删除)
- 时间复杂度:插入和删除均为O(log n),访问堆顶为O(1)
- 灵活性:支持自定义比较函数,可以实现最大堆或最小堆
9.2 优先队列的使用建议
- 当需要频繁访问最大/最小元素时,优先使用优先队列
- 当需要有序遍历所有元素时,考虑使用set或sort后的vector
- 当元素数量较大时,注意内存使用和预先分配空间
- 对于自定义类型,确保比较函数正确实现
9.3 优先队列与堆排序的关系
优先队列的实现原理与堆排序密切相关:
- 堆排序的第一步是建堆,与优先队列的初始化类似
- 堆排序的第二步是反复取出堆顶元素,与优先队列的pop操作类似
- 优先队列可以看作是堆排序的动态版本,支持动态插入和删除元素 通过深入理解优先队列的实现原理和使用方法,可以在实际项目中正确选择和高效使用这一重要的数据结构,提高程序的性能和可读性。