目录
使用std::priority_queue实现Dijkstra算法
C++ 标准模板库(STL)是C++中一个功能丰富且高效的库,它提供了多种数据结构和算法。在STL中,适配器(Adapters)是一类特别有用的工具,能够修改容器、迭代器和函数的行为,使其适应特定的需求。本文将深入探讨STL中的适配器,涵盖其概念、底层原理、详细使用及经典示例。
适配器的概念
适配器是一种设计模式,它使一个接口与另一个接口兼容。STL中的适配器主要分为三类:
- 容器适配器(Container Adapters):修改标准容器的行为。
- 迭代器适配器(Iterator Adapters):修改迭代器的行为。
- 函数适配器(Function Adapters):修改函数对象或函数指针的行为。
容器适配器
容器适配器是在已有的序列容器基础上构建的,它们提供了一个修改后的接口来满足特定的需求。STL中提供了三种容器适配器:
std::stack
: 后进先出(LIFO)数据结构。std::queue
: 先进先出(FIFO)数据结构。std::priority_queue
: 基于优先级的队列。
std::stack
std::stack
基于deque(双端队列)或其他任何符合序列容器要求的容器(如vector和list)实现。它只允许在一端进行插入和删除操作。
#include <stack>
#include <iostream>
int main() {
std::stack<int> s;
s.push(10);
s.push(20);
s.push(30);
while (!s.empty()) {
std::cout << ' ' << s.top();
s.pop();
}
return 0;
}
在上述例子中,std::stack
对象s
使用默认的std::deque
作为底层容器。通过push
和pop
操作进行元素的插入和删除,top
操作可以访问栈顶元素。
std::queue
std::queue
同样基于deque或list实现。它允许在一端插入元素(push),在另一端删除元素(pop)。
#include <queue>
#include <iostream>
int main() {
std::queue<int> q;
q.push(10);
q.push(20);
q.push(30);
while (!q.empty()) {
std::cout << ' ' << q.front();
q.pop();
}
return 0;
}
上述代码展示了如何使用std::queue
。front
函数访问队列的第一个元素,而back
函数访问最后一个元素。
std::priority_queue
std::priority_queue
基于堆(通常是二叉堆)实现。它支持最高优先级的元素优先出队。
#include <queue>
#include <vector>
#include <iostream>
int main() {
std::priority_queue<int> pq;
pq.push(30);
pq.push(10);
pq.push(20);
while (!pq.empty()) {
std::cout << ' ' << pq.top();
pq.pop();
}
return 0;
}
在这个例子中,std::priority_queue
默认是一个最大堆(max-heap),最高优先级的元素(最大的元素)首先被访问。
迭代器适配器
迭代器适配器修改迭代器的行为,STL提供了三种常用的迭代器适配器:
std::reverse_iterator
:反向遍历容器。std::insert_iterator
:在特定位置插入元素。std::istream_iterator
和std::ostream_iterator
:用于流的输入和输出。
std::reverse_iterator
std::reverse_iterator
允许反向遍历容器。
#include <vector>
#include <iostream>
#include <algorithm>
int main() {
std::vector<int> v = {1, 2, 3, 4, 5};
std::reverse_iterator<std::vector<int>::iterator> rbegin(v.end());
std::reverse_iterator<std::vector<int>::iterator> rend(v.begin());
std::for_each(rbegin, rend, [](int x) {
std::cout << x << ' ';
});
return 0;
}
在这个例子中,反向迭代器rbegin
和rend
用于反向遍历v
中的元素。
std::insert_iterator
std::insert_iterator
用于在特定位置插入元素。
#include <vector>
#include <iterator>
#include <iostream>
#include <algorithm>
int main() {
std::vector<int> v = {1, 2, 3, 4, 5};
std::vector<int> v2;
std::copy(v.begin(), v.end(), std::inserter(v2, v2.begin()));
for (int x : v2) {
std::cout << x << ' ';
}
return 0;
}
在这个例子中,std::inserter
适配器用于将元素插入到v2
的开头。
函数适配器
函数适配器修改函数对象或指针的行为。C++11之后,标准库提供了一些新的工具,如std::bind
和lambda表达式,使得以前的函数适配器(如std::bind1st
和std::bind2nd
)变得不那么必要。
std::bind
std::bind
允许绑定函数参数,从而创建新的函数对象。
#include <functional>
#include <iostream>
void print(int x, int y) {
std::cout << x << ' ' << y << std::endl;
}
int main() {
auto bound_func = std::bind(print, 10, std::placeholders::_1);
bound_func(20); // 输出 10 20
return 0;
}
在这个例子中,std::bind
创建了一个新的函数对象bound_func
,它将print
函数的第一个参数固定为10,而第二个参数仍然是可变的。
Lambda表达式
Lambda表达式是创建匿名函数的一种简洁方法。
#include <vector>
#include <algorithm>
#include <iostream>
int main() {
std::vector<int> v = {1, 2, 3, 4, 5};
std::for_each(v.begin(), v.end(), [](int x) {
std::cout << x << ' ';
});
return 0;
}
在这个例子中,Lambda表达式用于在std::for_each
算法中定义一个内联的匿名函数。
适配器的底层原理
适配器的底层原理通常是通过组合(composition)和代理(delegation)来实现的。适配器类内部持有一个或多个底层对象,并将操作委托给这些底层对象。例如,std::stack
持有一个底层容器,并通过这个容器实现push
和pop
等操作。
适配器的详细使用
掌握适配器的使用需要理解其接口和底层实现,从而能够根据需求选择和定制适配器。
- 选择适配器: 根据需求选择合适的适配器。
- 理解接口: 仔细阅读适配器的文档,理解其提供的方法和行为。
- 定制适配器: 在必要时创建自定义适配器,满足特定需求。
经典示例
使用std::stack
实现DFS
深度优先搜索(DFS)通常使用栈实现。以下是一个使用std::stack
实现的DFS算法:
#include <iostream>
#include <vector>
#include <stack>
void dfs(int start, const std::vector<std::vector<int>>& graph) {
std::vector<bool> visited(graph.size(), false);
std::stack<int> s;
s.push(start);
while (!s.empty()) {
int node = s.top();
s.pop();
if (!visited[node]) {
std::cout << node << ' ';
visited[node] = true;
for (int neighbor : graph[node]) {
if (!visited[neighbor]) {
s.push(neighbor);
}
}
}
}
}
int main() {
std::vector<std::vector<int>> graph = {
{1, 2},
{0, 3, 4},
{0, 4},
{1},
{1, 2}
};
dfs(0, graph);
return 0;
}
在这个例子中,我们使用std::stack
来管理待访问的节点,实现了深度优先搜索算法。
使用std::priority_queue
实现Dijkstra算法
Dijkstra算法用于求解单源最短路径问题,通常使用优先级队列实现。以下是一个使用std::priority_queue
实现的Dijkstra算法:
#include <iostream>
#include <vector>
#include <queue>
struct Edge {
int to, weight;
};
using Graph = std::vector<std::vector<Edge>>;
std::vector<int> dijkstra(int start, const Graph& graph) {
std::vector<int> dist(graph.size(), INT_MAX);
dist[start] = 0;
using pii = std::pair<int, int>; // (distance, node)
std::priority_queue<pii, std::vector<pii>, std::greater<pii>> pq;
pq.push({0, start});
while (!pq.empty()) {
int d = pq.top().first;
int u = pq.top().second;
pq.pop();
if (d != dist[u]) continue;
for (const Edge& e : graph[u]) {
if (dist[u] + e.weight < dist[e.to]) {
dist[e.to] = dist[u] + e.weight;
pq.push({dist[e.to], e.to});
}
}
}
return dist;
}
int main() {
Graph graph = {
{{1, 2}, {2, 4}},
{{2, 1}},
{{3, 2}},
{}
};
std::vector<int> distances = dijkstra(0, graph);
for (int i = 0; i < distances.size(); ++i) {
std::cout << "Distance to node " << i << " is " << distances[i] << std::endl;
}
return 0;
}
在这个例子中,我们使用std::priority_queue
来管理待访问的节点及其距离,实现了Dijkstra算法。
总结
STL中的适配器为C++编程提供了极大的灵活性和便利性。理解和掌握容器适配器、迭代器适配器和函数适配器的使用,不仅能够帮助你写出更加高效、简洁的代码,还能增强你对STL底层机制的理解。这篇文章详细探讨了适配器的概念、底层原理、详细使用及经典示例,希望能够为读者更好地掌握C++ STL适配器提供帮助。