48.c++标准库里优先队列是怎么实现的?

2026-01-23
2596 字 · 13 分钟

🔬 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, 名称: banana

5.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()插入元素时,优先队列的实现逻辑如下:

  1. 将新元素添加到底层容器的末尾
  2. 对新元素执行上滤操作,使其上升到合适位置
void push(const value_type& val) {
// 1. 添加到末尾
c.push_back(val);
// 2. 上滤操作
sift_up(c.size() - 1, val);
}

6.2 pop操作的实现

当调用pop()删除堆顶元素时,优先队列的实现逻辑如下:

  1. 将堆顶元素与底层容器的最后一个元素交换
  2. 删除最后一个元素(原堆顶元素)
  3. 对新的堆顶元素执行下滤操作,使其下降到合适位置
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_queueO(log n)O(log n)O(1)无序(仅堆顶有序)
setO(log n)O(log n)O(log n)有序
unordered_setO(1) 平均O(1) 平均O(n)无序
vectorO(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 优先队列的核心要点

  1. 底层实现:基于堆数据结构,默认为最大堆
  2. 存储结构:使用vector作为底层容器,利用完全二叉树的性质
  3. 核心操作:上滤(插入)和下滤(删除)
  4. 时间复杂度:插入和删除均为O(log n),访问堆顶为O(1)
  5. 灵活性:支持自定义比较函数,可以实现最大堆或最小堆

9.2 优先队列的使用建议

  • 当需要频繁访问最大/最小元素时,优先使用优先队列
  • 当需要有序遍历所有元素时,考虑使用set或sort后的vector
  • 当元素数量较大时,注意内存使用和预先分配空间
  • 对于自定义类型,确保比较函数正确实现

9.3 优先队列与堆排序的关系

优先队列的实现原理与堆排序密切相关:

  • 堆排序的第一步是建堆,与优先队列的初始化类似
  • 堆排序的第二步是反复取出堆顶元素,与优先队列的pop操作类似
  • 优先队列可以看作是堆排序的动态版本,支持动态插入和删除元素 通过深入理解优先队列的实现原理和使用方法,可以在实际项目中正确选择和高效使用这一重要的数据结构,提高程序的性能和可读性。

Thanks for reading!

48.c++标准库里优先队列是怎么实现的?

2026-01-23
2596 字 · 13 分钟

已复制链接

评论区

目录