1 std::priority_queue 应用于自定义数据结构
当应用于自定义数据结构时,std::priority_queue 的灵活性和可定制性可以得到充分体现。下面将详细讲解 std::priority_queue 如何与自定义数据结构结合使用。
自定义数据结构
首先,需要定义一个自定义数据类型,这可以是一个类或者结构体。这个自定义数据类型将作为 std::priority_queue 的元素类型。例如:
struct MyData {
int id;
double value;
// 可能还有其他成员变量和方法
// 构造函数、析构函数等(如果需要的话)
};
然后,可以使用这个比较对象来初始化 std::priority_queue:
std::priority_queue<MyData, std::vector<MyData>, CompareMyData> pq;
在这个例子中,std::priority_queue 使用 std::vector<MyData> 作为底层容器,并使用 CompareMyData 作为比较对象。这意味着队列中的元素将按照 CompareMyData 定义的优先级进行排序。
自定义数据结构
除了自定义比较函数或对象外,还可以指定一个自定义的底层容器来存储 std::priority_queue 的元素。这允许完全控制优先队列的行为和性能。例如,可以使用 std::deque 或其他容器类型作为底层容器。但需要注意的是,不是所有的容器类型都适合作为优先队列的底层容器,因为优先队列需要底层容器支持某些特定的操作(如随机访问迭代器)。
下面是一个完整的示例,展示了如何使用 std::priority_queue 与自定义数据结构 MyData 和自定义比较对象 CompareMyData:
#include <iostream>
#include <queue>
#include <vector>
struct MyData {
int id;
double value;
MyData(int id, double value) : id(id), value(value) {}
};
struct CompareMyData {
bool operator()(const MyData& a, const MyData& b) const {
return a.value < b.value; // 小于号表示大顶堆
}
};
int main()
{
std::priority_queue<MyData, std::vector<MyData>, CompareMyData> pq;
pq.push(MyData(1, 3.0));
pq.push(MyData(2, 1.0));
pq.push(MyData(3, 2.0));
while (!pq.empty()) {
MyData topElement = pq.top();
pq.pop();
std::cout << "ID: " << topElement.id << ", Value: " << topElement.value << std::endl;
}
return 0;
}
上面代码的输出为:
ID: 1, Value: 3
ID: 3, Value: 2
ID: 2, Value: 1
这个示例创建了一个 std::priority_queue,其中包含了自定义数据类型 MyData 的对象,并使用自定义比较对象 CompareMyData 来定义元素的优先级。然后,向队列中添加了一些元素,并逐个取出并打印它们的 id 和 value。由于使用了大顶堆,所以打印出的元素将按照 value 的降序排列。
2 std::priority_queue 的主要应用场景
下面是 std::priority_queue 的一些主要应用场景:
(1)任务调度与优先级管理
当有一系列任务需要按照优先级执行时,可以使用 std::priority_queue。例如,在操作系统或任务调度系统中,高优先级的任务会优先于低优先级的任务执行。通过将任务对象插入优先队列,并根据其优先级进行排序,可以方便地获取和执行最高优先级的任务。
(2)图算法中的优化
在图算法中,如 Dijkstra 算法用于寻找最短路径,或 Prim 算法用于寻找最小生成树时,std::priority_queue 可以用来高效地选择当前未访问节点中距离最短的节点。在这些算法中,优先队列用于维护一个待处理的节点集合,并根据它们的某种度量(如距离)进行排序。
(3)事件驱动系统
在事件驱动的系统中,事件可能按照不同的优先级触发。使用 std::priority_queue 可以确保高优先级的事件优先得到处理。例如,在游戏开发中,重要的用户输入事件(如按键操作)可能具有比背景动画更新更高的优先级。
(4)实时系统
在实时系统中,对时间敏感的任务需要快速响应。std::priority_queue 可以帮助系统按照任务的紧急程度或截止时间进行排序,从而确保关键任务得到及时处理。
(5)资源分配
当有限的资源需要分配给多个请求时,可以使用 std::priority_queue 来根据请求的优先级进行资源分配。例如,在服务器处理客户端请求时,可以根据请求的紧急程度或付费级别来分配计算资源。
(6)自定义数据结构的排序
当需要对自定义数据结构进行排序时,std::priority_queue 可以结合自定义比较函数或对象使用。这使得它成为处理具有复杂比较逻辑的数据结构的强大工具。例如,在模拟系统中,可能需要根据多个属性(如速度、位置等)来确定对象的优先级。
2.1 std::priority_queue 应用于任务调度与优先级管理
std::priority_queue 在任务调度与优先级管理中的应用非常直接且有效。以下是一个简单的示例,展示了如何使用 std::priority_queue 来实现一个基本的任务调度系统。
首先,定义一个 Task 结构体,其中包含任务的标识符、优先级以及需要执行的操作(这里用简单的函数指针代替)。然后,创建一个 CompareTask 结构体来定义任务的比较逻辑,即优先级高的任务应该排在优先队列的前面。
#include <iostream>
#include <queue>
#include <functional>
// 任务类型
struct Task {
int id;
int priority;
std::function<void()> action; // 执行的任务操作
Task(int id, int priority, std::function<void()> action)
: id(id), priority(priority), action(action) {}
};
// 比较任务优先级的结构体
struct CompareTask {
bool operator()(const Task& a, const Task& b) const {
// 返回 true 如果 a 的优先级低于 b 的优先级,这样 a 会被放在队列后面
return a.priority < b.priority;
}
};
int main()
{
// 创建一个最大堆的优先队列,根据任务的优先级排序
std::priority_queue<Task, std::vector<Task>, CompareTask> taskQueue;
// 添加任务到队列中,每个任务都有一个唯一的 ID、优先级和要执行的操作
taskQueue.push(Task(1, 3, []() { std::cout << "Executing task 1 (priority 3)\n"; }));
taskQueue.push(Task(2, 1, []() { std::cout << "Executing task 2 (priority 1)\n"; }));
taskQueue.push(Task(3, 2, []() { std::cout << "Executing task 3 (priority 2)\n"; }));
// 执行任务,直到队列为空
while (!taskQueue.empty()) {
Task currentTask = taskQueue.top(); // 获取最高优先级的任务
taskQueue.pop(); // 从队列中移除该任务
currentTask.action(); // 执行任务的操作
}
return 0;
}
上面代码的输出为:
Executing task 1 (priority 3)
Executing task 3 (priority 2)
Executing task 2 (priority 1)
这个示例创建了一个 std::priority_queue,它使用 Task 结构体作为元素类型,并使用 CompareTask 结构体来定义任务的优先级比较逻辑。然后通过 std::function<void()> 来表示每个任务需要执行的操作,这使得任务可以包含任何我们想要执行的代码。
接下来向优先队列中添加了三个任务,每个任务都有不同的优先级。然后,使用一个循环来从队列中取出并执行任务,直到队列为空。由于这里使用了最大堆(即大顶堆),所以每次从队列中取出的都是当前优先级最高的任务。
运行这个程序,可以看到任务按照优先级的降序被执行。这就是 std::priority_queue 在任务调度与优先级管理中的一个简单应用。在实际应用中,任务可能包含更复杂的逻辑和操作,但这个示例提供了一个基本的框架和思路。
2.2 std::priority_queue 应用于 Dijkstra 算法
Dijkstra 算法是一个用于解决带权有向图中单源最短路径问题的算法。在 Dijkstra 算法中,std::priority_queue 是一个非常适合的数据结构,用于存储待处理的节点,并根据它们的当前最短距离进行排序。下面是一个使用 std::priority_queue 实现 Dijkstra 算法的示例:
#include <iostream>
#include <vector>
#include <queue>
#include <climits>
#include <cstring>
using namespace std;
// 用于存储边的结构体
struct Edge {
int dest;
int weight;
Edge(int d, int w) : dest(d), weight(w) {}
};
// Dijkstra算法实现
void dijkstra(vector<vector<Edge>>& graph, int src) {
int V = graph.size();
vector<int> dist(V, INT_MAX); // 存储从源点到各个顶点的最短距离
dist[src] = 0; // 源点到自身的距离为0
vector<bool> visited(V, false); // 标记节点是否被访问过
// 使用小顶堆,按照距离排序
priority_queue<pair<int, int>, vector<pair<int, int>>, greater<pair<int, int>>> pq;
pq.push({ 0, src }); // 初始时将源点入队,距离为0
while (!pq.empty()) {
// 取出当前距离最短的节点
int u = pq.top().second;
pq.pop();
// 如果该节点已经被访问过,则跳过
if (visited[u]) continue;
visited[u] = true;
// 遍历与当前节点相邻的节点
for (const Edge& edge : graph[u]) {
int v = edge.dest;
int weight = edge.weight;
// 如果发现一条更短的路径,则更新距离,并将该节点入队
if (dist[v] > dist[u] + weight) {
dist[v] = dist[u] + weight;
pq.push({ dist[v], v });
}
}
}
// 输出结果
cout << "Vertex \t\t Distance from Source" << endl;
for (int i = 0; i < V; i++)
cout << i << "\t\t" << dist[i] << endl;
}
int main()
{
// 初始化图
int V = 5; // 顶点数量
vector<vector<Edge>> graph(V);
graph[0].push_back(Edge(1, 9));
graph[0].push_back(Edge(2, 6));
graph[0].push_back(Edge(4, 5));
graph[1].push_back(Edge(3, 15));
graph[2].push_back(Edge(3, 11));
graph[2].push_back(Edge(1, 2));
graph[3].push_back(Edge(4, 6));
// 调用Dijkstra算法
dijkstra(graph, 0); // 从顶点0开始
return 0;
}
上面代码的输出为:
Vertex Distance from Source
0 0
1 8
2 6
3 17
4 5
这个示例首先定义了一个Edge结构体来存储图的边信息。然后,实现了 Dijkstra 算法。算法开始时,初始化所有顶点到源点的距离为无穷大(INT_MAX),除了源点到自身的距离为 0。这里使用一个 visited 数组来跟踪哪些节点已经被处理过。
priority_queue 用于存储待处理的节点对,每个节点对包含一个距离值和一个顶点索引。队列根据距离值进行排序,因此每次从队列中取出的都是当前距离最短的节点。
在算法的主体部分,不断从队列中取出距离最短的节点,并更新其相邻节点的距离。如果找到了一条更短的路径,就更新该节点的距离,并将其重新加入队列。
最后,输出每个顶点到源点的最短距离。
3 实现一个简单的 std::priority_queue
可以使用一个数组或动态数组(如 std::vector)作为底层存储,并手动实现堆的基本操作,如插入元素、删除顶部元素和调整堆结构。以下是一个简化版的 std::priority_queue 实现,仅支持最大堆(即顶部元素总是最大的)。
#include <iostream>
#include <vector>
#include <algorithm> // 用于 std::swap
template <typename T>
class SimplePriorityQueue {
private:
std::vector<T> elements;
// 调整堆结构,确保父节点总是大于或等于其子节点
void siftUp(int index) {
int parent = (index - 1) / 2;
while (index > 0 && elements[parent] < elements[index]) {
std::swap(elements[parent], elements[index]);
index = parent;
parent = (index - 1) / 2;
}
}
// 调整堆结构,确保父节点总是大于或等于其子节点
void siftDown(int index) {
int leftChild = 2 * index + 1;
int rightChild = 2 * index + 2;
int largest = index;
if (leftChild < elements.size() && elements[leftChild] > elements[largest]) {
largest = leftChild;
}
if (rightChild < elements.size() && elements[rightChild] > elements[largest]) {
largest = rightChild;
}
if (largest != index) {
std::swap(elements[index], elements[largest]);
siftDown(largest);
}
}
public:
// 插入元素
void push(const T& value) {
elements.push_back(value);
siftUp(elements.size() - 1);
}
// 删除顶部元素
void pop() {
if (elements.empty()) {
throw std::out_of_range("Priority queue is empty");
}
elements[0] = elements.back();
elements.pop_back();
siftDown(0);
}
// 返回顶部元素,但不删除
T top() const {
if (elements.empty()) {
throw std::out_of_range("Priority queue is empty");
}
return elements[0];
}
// 检查队列是否为空
bool empty() const {
return elements.empty();
}
// 获取队列大小
size_t size() const {
return elements.size();
}
};
int main()
{
SimplePriorityQueue<int> pq;
pq.push(3);
pq.push(1);
pq.push(4);
std::cout << "Top element is: " << pq.top() << std::endl; // 输出 4
pq.pop();
std::cout << "Top element after pop is: " << pq.top() << std::endl; // 输出 3
return 0;
}
上面代码的输出为:
Top element is: 4
Top element after pop is: 3
在这个实现中,SimplePriorityQueue 类使用 std::vector 来存储元素。push 方法用于向队列中添加元素,并在添加后通过 siftUp 方法调整堆的结构。pop 方法用于删除顶部元素,并通过 siftDown 方法重新调整堆的结构。top 方法返回顶部元素的值。empty 和 size 方法分别用于检查队列是否为空和获取队列的大小。