图的经典算法
完整版万字原文见史上最全详解图数据结构
一、图的遍历算法
1.void DFS(int startVertex);
2.void BFS(int startVertex);
3.void TopologicalSort();(两种实现方式)
1. DFS(深度优先搜索)
算法原理
是一种用于遍历或搜索图(包括树)中节点的算法。
其基本思想是沿着一个分支尽可能深地搜索,直到该分支无法继续扩展,再回溯到上一个分支,继续探索其他可能的路径
算法步骤
1. 选择一个起始节点,并访问它。
2. 对每个未被访问的邻接节点,递归地应用DFS。
3. 如果一个节点没有未被访问的邻接节点,则回溯到其父节点,继续进行其他分支的探索。
4. 直到所有节点都被访问或图的遍历完成。
代码实现
提要:
function<void(int)> dfsVisit = [&](int vertex)
1. 定义一个名为 dfsVisit 的递归 Lambda 函数,接受一个顶点作为参数。
function 是 C++ 标准库中的一个类模板,允许你存储可调用对象(如函数指针、lambda 表达式等)
void(int) 指定这个函数不返回任何值(void),并且接受一个 int 类型的参数
2. 这里存储的可调用对象 dfsVisit 是函数指针对象
使用了 lambda 表达式 [&](int vertex) 来构造一个 function<void(int)>类模板的函数指针对象。
lambda 表达式通过 [&] 以引用的方式捕获外部变量(如 visited 和 traversal)的引用,便于在递归中使用
void MyGraph::DFS(int startVertex)
{
vector<bool> visited(n, false); // 标记数组,记录顶点是否已被访问
vector<int> traversal; // 用于存储遍历过程中访问的顶点(路径)
function<void(int)> dfsVisit = [&](int vertex)
{
// 1. 选择一个起始节点,并访问它。
visited[vertex] = true;
traversal.push_back(vertex);
// 2. 对每个未被访问的邻接节点,递归地应用DFS。
for (const auto &edge : adjList[vertex])
if (!visited[edge.first])
dfsVisit(edge.first);
// 3. 如果一个节点没有未被访问的邻接节点,则回溯到其父节点,继续进行其他分支的探索。
};
dfsVisit(startVertex);
for (int vertex : traversal)
cout << vertex << " ";
cout << endl;
}
思考
这段代码的实现中用了 lamba 函数而没有用辅助函数,为什么?
比较 :使用 lamba 表达式和使用辅助函数
1. lambda 表达式
好处在于简化代码和提高可读性
使得递归函数 dfsVisit 可以直接在 DFS 方法内部定义,这样就能方便地捕获外部的状态(如 visited 和 traversal)而不需要额外的参数
2. 辅助函数
需要额外的参数 dfsVisit(startVertex, visited, traversal);
//使用辅助函数版的 dfs
void MyGraph::DFS(int startVertex)
{
vector<bool> visited(n, false); // 标记数组
vector<int> traversal; // 存储访问的顶点
// 需要调用独立的递归成员函数
dfsVisit(startVertex, visited, traversal);
// 输出遍历顺序
for (int vertex : traversal)
cout << vertex << " ";
cout << endl;
}
void MyGraph::dfsVisit(int vertex, vector<bool>& visited, vector<int>& traversal)
{
visited[vertex] = true;
traversal.push_back(vertex);
for (const auto& edge : adjList[vertex])
if (!visited[edge.first])
dfsVisit(edge.first, visited, traversal); // 递归调用
}
2. BFS(宽度优先搜索)
算法原理
是一种用于遍历或搜索图的算法
它以图的一个节点为起点,首先访问该节点的所有邻接节点,然后再访问这些邻接节点的邻接节点,以此类推。简单来说,BFS 是按层次逐层展开搜索的。
算法步骤
1. 初始化:
创建一个队列 q 用来存放待访问的节点。
创建一个布尔数组 visited 用来标记节点是否被访问过。
2. 访问节点:
从队列中取出一个节点,标记它为已访问,并处理它(如输出)。
3. 将该节点的所有未访问过的邻接节点加入队列中。
4. 重复2 3 步,直到队列为空,表示所有可达节点都已经被访问。
代码实现
// 从给定的起始顶点进行广度优先搜索(BFS)。
void MyGraph::BFS(int startVertex)
{
// 1. 初始化
vector<bool> visited(n, false);
queue<int> q;
// 2. 访问节点
visited[startVertex] = true;
q.push(startVertex);
while (!q.empty())
{
int currentVertex = q.front();
q.pop();
cout << currentVertex << " ";
// 3. 将该节点的所有未访问过的邻接节点加入队列中。
for (const auto &edge : adjList[currentVertex])
if (!visited[edge.first])
visited[edge.first] = true, q.push(edge.first);
}
cout << endl;
}
3. 拓扑排序(两种实现)
问题引入
算法原理
有向无环图(DAG)
(1)有向 ———> 按先后顺序排序( u -> v,u 在 v 之前出现)
(2)无环 ———> 需要包含所有节点
时间复杂度 O(E + V)
方法一:Kahn算法(基于入度)
核心思想 :
每次选择入度为 0 的点,然后
删除这个点(将这个点加入结果序列中),删除它的出边(将与这个点连接的所有点入度减 1)
对其排序的结果就是:2 -> 8 -> 0 -> 3 -> 7 -> 1 -> 5 -> 6 -> 9 -> 4 -> 11 -> 10 -> 12
算法步骤
1. 计算所有节点的入度。
2. 将所有入度为 0 的节点放入队列。
3. 从队列中取出一个节点,并将其加入拓扑排序结果中。
4. 对该节点的所有邻接节点,将其入度减 1,如果某个邻接节点的入度变为0,则将其加入队列。
5. 重复以上步骤,直到队列为空。
6. 如果最后拓扑排序中的节点数小于图中的节点数,则图中存在环。
代码实现
// 拓扑排序(Kahn 算法)
vector<int> topologicalSortKahn(int n, vector<vector<int>> &edges)
{
vector<int> inDegree(n, 0); // 入度数组
vector<vector<int>> adj(n); // 邻接表
vector<int> result; //储存排序得到的数据
// 1. 计算所有节点的入度。
for (const auto &edge : edges)
{
int u = edge[0], v = edge[1];
adj[u].push_back(v);
inDegree[v]++;
}
// 2. 将所有入度为 0 的节点放入队列。
queue<int> q;
for (int i = 0; i < n; ++i)
if (inDegree[i] == 0)
q.push(i);
while (!q.empty())
{
// 3. 从队列中取出一个节点,并将其加入拓扑排序结果中。
int node = q.front();
q.pop();
result.push_back(node);
// 4. 对该节点的所有邻接节点,将其入度减 1,如果某个邻接节点的入度变为 0,则将其加入队列。
for (int neighbor : adj[node])
{
inDegree[neighbor]--;
if (inDegree[neighbor] == 0)
q.push(neighbor);
}
}
// 6. 如果最后拓扑排序中的节点数小于图中的节点数,则图中存在环。
if (result.size() != n)
{
return {
};
}
return result;
}
思考
为什么如果最后拓扑排序中的节点数小于图中的节点数,则图中存在环?
拓扑排序按 先后顺序 排序 ——> 拓扑排序中的节点数小于图中的节点数 ———> 不是所有的节点都符合排序条件 ——> 有一些节点没有先后顺序 ————> 有一些节点形成了环
方法二:深度优先搜索(DFS)
核心思想 : 利用递归调用栈的特点,保证入栈按倒序,正确的拓扑排序即为出栈顺序
算法步骤
算法步骤:
1. 对每个未被访问的节点执行 DFS,标记该节点为访问中。
2. 如果访问到某个已经在 “ 访问中 ” 状态的节点,则说明图中存在环,无法进行拓扑排序。
3. DFS 完成后,标记节点为“已访问”,并将节点加入到结果栈(或者队列)中。
4. 最终结果是栈中节点的逆序。
代码实现
bool dfs(int node, vector<vector<int>> &adj, vector<int> &visited, stack<int> &result)
{
// 当前节点正在访问,图中有环
if (visited[node] == 1)
return false;
// 当前节点已经访问过
if (visited[node] == 2)
return true;
// 1. 对每个未被访问的节点执行 DFS,标记该节点为访问中。
visited[node] = 1;
for (int neighbor : adj[node])
// 2. 如果访问到某个已经在 “ 访问中 ” 状态的节点,则说明图中存在环,无法进行拓扑排序
if (!dfs(neighbor, adj, visited, result))
return false;
// 3. DFS 完成后,标记节点为“已访问”,并将节点加入到结果栈(或者队列)中。
visited[node] = 2; // 标记为已访问
result.push(node); // 将节点加入栈
return true;
}
vector<int> topologicalSortDFS(int n, vector<vector<int>> &edges)
{
vector<vector<int>> adj(n);
for (const auto &edge : edges)
adj[edge[0]].push_back(edge[1]);
vector<int> visited(n, 0); // 0: 未访问, 1: 正在访问, 2: 已访问
stack<int> result;
// 1. 对每个未被访问的节点执行 DFS,标记该节点为访问中。
for (int i = 0; i < n; ++i)
if (visited[i] == 0)
if (!dfs(i, adj, visited, result))
return {
}; // 如果存在环,返回空
vector<int> order;
while (!result.empty())
{
order.push_back(result.top());
result.pop();
}
return order;
}
总结
(1)Kahn算法(基于入度):通常更直观易懂,适合大规模图,对于边的增删操作更加高效,且容易处理图中的环。
(2)DFS算法:适合递归或栈的场景,能够通过递归的后序遍历得到拓扑排序,也适合用来检测环。
最短路径算法
1. Dijkstra算法(单源最短路径)(无负权边图)
算法原理
1. Dijkstra 算法通过 贪心策略 计算从一个源顶点到其他所有 顶点的最短路径。
2. 时间复杂度为 O(V^2)(未优化时)或 O((V + E) log V)(使用优先队列时)
3. 应用:适用于无负权边的图。
4. 核心思想
(1) 选定一个点,这个点满足两个条件:a.未被选过,b.距离最短
(2) 对于这个点的所有邻近点去尝试松弛
实现代码(未优化版本)
// 求最短路径
// Dijkstra 不断选择当前距离源节点最近的未处理节点来构建最短路径。
// 贪心算法
// 时间复杂度 O(V^2)
void MyGraph::Dijkstra(int startVertex)
{
//代表某个顶点是否被访问过,初始化所有的顶点为 false
vector<bool> visited(n, false);
//dis代表源点到其它点的最短距离,numeric_limits<int>::max()代表无穷
vector<int> distances(n, numeric_limits<int>::max());
//向量 prev 用来存储路径的前驱节点,用于之后路径重建,初始化为 -1
vector<int> prev(n, -1);
源点到源点的距离为 0
distances[startVertex] = 0;
//为什么只要寻找n-1个点呢?因为当剩下一个点的时候,这个点已经没有需要松弛的邻接点了
for (int i = 0; i < n - 1; i++)
{
//进入循环之后,一开始不知道哪个是没有被访问过且距离源点最短的
int now_minDistance = numeric_limits<int>::max();
int now_minVertex = -1;
//使用这个循环开始寻找没有被访问过且距离源点最短距离的点
for (int j = 0; j < n; j++)
if (!visited[j] && distances[j] < now_minDistance)
now_minDistance = distances[j], now_minVertex = j;
//标记当前节点已访问
visited[now_minVertex] = true;
//对这个距离源点最短距离的点的所有邻接点进行松弛
for (const auto &pair : adjList[now_minVertex])
{
// 松弛操作(Relaxation)
if (distances[now_minVertex] + pair.second < distances[pair.first])
{
distances[pair.first] = distances[now_minVertex] + pair.second, prev[pair.first] = now_minVertex;
}
}
}
for (int i = 0; i < n; i++)
if (i != startVertex)
cout << "vertex " << i << " distance from " << startVertex << " is " << distances[i] << endl;
}
实现代码(优化版本)
算法原理:
利用了优先队列(通常是最小堆)
将时间复杂度降低到 O((V + E) log V)。
void MyGraph::Dijkstra_withOptimize(int startVertex)
{
vector<bool> visited(n, false);
vector<int> distances(n, numeric_limits<int>::max());
vector<int> prev(n, -1);
priority_queue<pair<int, int>, vector<pair<int, int>>, greater<pair<int, int>>> pq;
pq.push({
0, startVertex});
distances[startVertex] = 0;
// for(int i = 0; i < n - 1; i++){
while (!pq.empty())