说在前面
在图论算法一中介绍了图的分类以及实现了邻接表与邻接矩阵的基本实现。
下面我将介绍图的遍历算法。
图的遍历
回顾一下,之前树的遍历算法中,有深度优先遍历和广度优先遍历。
所以我们先来介绍图的深度优先遍历。
1. 深度优先遍历
我将以下面这张图作为实例,演示图的深度优先遍历是怎么实现的。
首先这是一张稀疏图,我们用邻接表来实现。所谓的深度优先遍历,首先得把握两个点。第一点就是深度优先,就是从一个点开始,不停的向下试,直到试不下去为止。这个与树的深度优先是类似的。第二点就是图跟树不一样,对于树结构,从根开始,一直向下走,一定会有结束的时候,但是对于图来说,它可能会存在环,为此我们需要记录每个点是否被遍历过了。
下面,来看一下,对于这张图,从 0 开始进行深度优先遍历,是怎样实现的。
接着我们就要看,与 0 相邻的这些节点,首先看 1,1 我们没有遍历过,这里我们就将 1 加入序列。
然后,我们再看与 1 相邻的节点,为 0,但是 0 我们已经遍历过了。我们不管,但是 1 已经没有与之相邻的节点了,此时这条路就走完了。
之后,我们就退回到了 0,0 的下一个相邻的并且没有遍历过的节点是 2,然后我们将 2 加入序列。
之后,对于 2 来说,与之唯一相邻的节点 0,已经遍历过了;然后又退回到了 1,寻找下一个相邻且没有遍历过的节点 5,将 5 加入序列。
紧接着看与 5 相邻的节点,首先是 0,遍历过了,下一个是 3,没有遍历过,将其加入序列中。
之后再看与 3 相邻节点 4,没有遍历过,将 4 加入序列。
然后,看与 4 相邻节点,3, 5遍历过了。将 6 加入序列。
看与 6 相邻的节点 0 和 4,都遍历过了。就可以退回到 4,对于节点 4 来说,3,5,6都遍历过了,然后退回到 3,3 的相邻节点 4,5都遍历过了,退回到 5,5 的相邻节点也遍历过了,然后退回到 0,0 的全部相邻节点也遍历过了,那么到此,节点已经完全遍历完成。
使用图的深度优先遍历,可以很轻松的求出一张图的连通分量。
在上图中,连通分量为 3。那么我们怎么使用深度优先遍历来求出联通分量呢?
首先我们随便选择一个节点作为起始点,然后使用深度优先遍历遍历一遍便会遍历完上图中 1,2,3的某一个。然后再从未被遍历的节点中选择一个节点作为起始点,再进行一次深度优先遍历,又会遍历完图中1,2,3的某一个。最后再从未被遍历的节点中,选择一个节点作为起始点,再进行一次深度优先遍历,直到最后将整张图遍历完,那么总共进行了几次深度优先遍历,就有几个连通分量。
下面来实现一下图的深度优先遍历。
#include <iostream>
#include <assert.h>
using namespace std;
// 深度优先遍历
template<typename Graph>
class Component
{
private:
Graph &G;
bool *visited; //记录节点是否被访问过
int ccount; // 连通分量
void dfs( int v )
{
visited[v] = true;
//Graph::adjIterator adj(G, v); // 此时编译器并不知道adjIterator是一个类型还是一个成员变量
// 为了显示声明adjIterator是一个类性,加上 typename
typename Graph::adjIterator adj(G, v);
for(int i = adj.begin(); !adj.end(); i = adj.next())
{
if(!visited[i])
dfs(i);
}
}
public:
Component(Graph &graph) : G(graph)
{
visited = new bool[G.V()];
ccount = 0;
for(int i = 0; i < G.V(); i++)
{
visited[i] = false;
}
for(int i = 0; i < G.V(); i++)
{
if(!visited[i]) // 若没有被访问过,进行遍历
{
dfs(i);
ccount ++;
}
}
}
int count(){ return ccount; }
~Component()
{
delete[] visited;
}
};
上面既然求出了连通分量,那么也很容易求出两个节点是否是相邻的。我们只需要在上面的代码中添加一个 id 数组,来记录每个节点是否是连通的,若是连通的节点,那么它们的 id 的值是相同的。
#include <iostream>
#include <assert.h>
using namespace std;
// 深度优先遍历
template<typename Graph>
class Component
{
private:
Graph &G;
bool *visited; //记录节点是否被访问过
int ccount; // 连通分量
int *id; // 记录节点是否相邻,若两个节点相邻,那么它们的 id 是相同的。
void dfs( int v )
{
id[v] = ccount;
visited[v] = true;
//Graph::adjIterator adj(G, v); // 此时编译器并不知道adjIterator是一个类型还是一个成员变量
// 为了显示声明adjIterator是一个类性,加上 typename
typename Graph::adjIterator adj(G, v);
for(int i = adj.begin(); !adj.end(); i = adj.next())
{
if(!visited[i])
dfs(i);
}
}
public:
Component(Graph &graph) : G(graph)
{
visited = new bool[G.V()];
id = new int[G.V()];
ccount = 0;
for(int i = 0; i < G.V(); i++)
{
visited[i] = false;
id[i] = -1;
}
for(int i = 0; i < G.V(); i++)
{
if(!visited[i])
{
dfs(i);
ccount ++;
}
}
}
int count(){ return ccount; }
// 判断两个节点是否是相连接的
bool isConnected(int v, int w)
{
assert( v >= 0 && v < G.V() );
assert( w >= 0 && w < G.V() );
return id[v] == id[w];
}
~Component()
{
delete[] visited;
delete[] id;
}
};
其实在深度优先遍历过程中,我们形成了一条条的路径,那么我们怎么获取两点之间的路径呢?
其实,我们都可以在遍历过程中通过红色线条来找到。但是,我们使用深度优先的方式,并不能保证它是一条最短的路径。其实,我们要做的就是,就是在遍历的过程中,存储这条路径。
下面来看代码。
#include <iostream>
#include <vector>
#include <stack>
using namespace std;
template<typename Graph>
class Path
{
private:
Graph &G;
int s; // 起始节点
bool *visited;
int *from; // 记录某个节点来自哪一个节点
void dfs( int v )
{
visited[v] = true;
typename Graph::adjIterator adj(G, v);
for(int i = adj.begin(); !adj.end(); i = adj.next())
{
if(!visited[i])
{
from[i] = v;
dfs(i);
}
}
}
public:
Path(Graph &graph, int s) : G(graph)
{
// 算法初始化
assert( s >= 0 && s < G.V() );
visited = new bool[G.V()];
from = new int[G.V()];
for(int i = 0; i < G.V(); i++)
{
visited[i] = false;
from[i] = -1;
}
this->s = s;
// 寻路算法
dfs(s);
}
~Path()
{
delete[] visited;
delete[] from;
}
// 从节点 w 是否有路径
bool hasPath(int w)
{
assert( w >= 0 && w < G.V() );
return visited[w];
}
void path(int w, vector<int> &vec)
{
stack<int> s;
int p = w;
while(p != -1)
{
s.push(p);
p = from[p];
}
vec.clear();
while( !s.empty() )
{
vec.push_back( s.top() );
s.pop();
}
}
void showPath(int w)
{
vector<int> vec;
path( w, vec );
for(int i = 0; i < vec.size(); i++)
{
cout << vec[i];
if(i == vec.size() - 1)
cout << endl;
else
cout << " -> ";
}
}
};
【测试代码】
#include <iostream>
#include <ctime>
#include "DenseGraph.h"
#include "SparseGraph.h"
#include "ReadGraph.h"
#include "Component.h"
#include "Path.h"
using namespace std;
void testDFS()
{
// 测试路径
string filename = "testG2.txt";
SparseGraph g1(7, false);
ReadGraph<SparseGraph> readGraph1( g1, filename );
g1.show();
Path<SparseGraph> dfs(g1, 0);
cout << "DFS:" ;
dfs.showPath(6);
}
int main()
{
testDFS();
return 0;
}
【运行结果】
时间复杂度分析:
- 稀疏图(邻接表):O(V + E)
- 稠密图(邻接矩阵):O(V^2)
其实,深度优先遍历还可以有其他的应用。比如:检测图中是否有环。
2. 广度优先遍历
上面介绍完了图的深度优先遍历,下面来看看图的广度优先遍历。
我们知道,树的广度优先遍历是用队列来实现的。相同的,图的广度优先遍历也借助队列来实现。
下面,我将使用这样一张图来介绍树的广度优先遍历。
我们以节点 0 作为开始,进行广度优先遍历。首先,我们将 0 放入队列。
我们每次遍历都将队首元素取出来作为遍历的对象,现在我们队列中只有 0 一个元素,将 0 取出来。相当于遍历了 0 这个节点。
之后需要将 0 这个节点的所有和它相邻的节点,如果还未被加入过队列中,就将它加入进来。所以我们现在需要将 1,2,5,6 这 4 个节点加入到队列中。
之后,我们将队首元素 1 取出,表示 1 被遍历了,对于 1,与之相邻的节点只有 0,但是 0 已经被加入过队列了表示被遍历过了,所以不管。
下面,继续将 2 节点从队列中取出作为遍历的对象,那么对于节点 2,与之相邻的为节点 0,不用管。
我们继续取出节点 5,然后将与之相邻的且未被加入到的队列的节点 3 和 4加入队列。
然后将队首元素 6 作为遍历对象,取出,4 已经在队列中,不管。然后继续取出队首元素 3,4 和 5 不用管。然后将队首 4 进行遍历,3,5,6均被遍历过了,不管。
此时,队列为空,我们这次的广度优先遍历就已经完成了。
其实,这个广度优先遍历的过程,对于这张图而言,相当于是以距离我们遍历的起始节点 0的距离为顺序进行遍历的。
首先 0 这个节点,它和它自己的距离就是 0,所以它是第一个被遍历的。之后所有与0相邻的节点都被推了进去,这些节点距离 0 的距离就是 1,下一步,我们在遍历 5 这个节点的时候,将 3,4节点推进队列进行后续的遍历。而 3,4这两个节点距离 0 的距离都是 2。
这样我就可以使用这样一种性质,求出了无权图的最短路径。
下面,我们就用代码来实现一下。
#include <iostream>
#include <vector>
#include <stack>
#include <queue>
using namespace std;
template<typename Graph>
class ShortestPath
{
private:
Graph &G;
int s;
bool *visited;
int *from;
int *ord; // 从起始节点 s 到每一个节点的距离
public:
ShortestPath(Graph &graph, int s) : G(graph)
{
// 算法初始化
assert( s >= 0 && s < graph.V() );
visited = new bool[graph.V()];
from = new int[graph.V()];
ord = new int[graph.V()];
for(int i = 0; i < graph.V(); i++)
{
visited[i] = false;
from[i] = -1;
ord[i] = -1;
}
this->s = s;
queue<int> q;
//无向图的最短路径算法
q.push( s );
visited[s] = true;
ord[s] = 0;
while( !q.empty() )
{
int v = q.front();
q.pop();
typename Graph::adjIterator adj(G, v);
for(int i = adj.begin(); !adj.end(); i = adj.next())
{
if(!visited[i])
{
q.push(i);
visited[i] = true;
from[i] = v;
ord[i] = ord[v] + 1;
}
}
}
}
bool hasPath(int w)
{
assert( w >= 0 && w < G.V());
return visited[w];
}
void Path(int w, vector<int> &vec)
{
assert( w >= 0 && w < G.V() );
stack<int> s;
int p = w;
while( p != -1)
{
s.push(p);
p = from[p];
}
vec.clear();
while( !s.empty() )
{
vec.push_back( s.top() );
s.pop();
}
}
void showPath(int w)
{
assert( w >= 0 && w < G.V() );
vector<int> vec;
Path(w, vec);
for(int i = 0; i < vec.size(); i++)
{
cout << vec[i];
if(i == vec.size() - 1)
cout << endl;
else
cout << " -> ";
}
}
int length(int w)
{
assert( w >= 0 && w < G.V());
return ord[w];
}
~ShortestPath()
{
delete[] visited;
delete[] from;
delete[] ord;
}
};
【测试代码】
#include <iostream>
#include <ctime>
#include "DenseGraph.h"
#include "SparseGraph.h"
#include "ReadGraph.h"
#include "Component.h"
#include "Path.h"
#include "ShortestPath.h"
using namespace std;
void testDFS()
{
// 测试路径
string filename = "testG2.txt";
SparseGraph g1(7, false);
ReadGraph<SparseGraph> readGraph1( g1, filename );
g1.show();
Path<SparseGraph> dfs(g1, 0);
cout << "DFS:" ;
dfs.showPath(6);
}
void testBFS()
{
string filename = "testG2.txt";
SparseGraph g1(7, false);
ReadGraph<SparseGraph> readGraph1( g1, filename );
ShortestPath<SparseGraph> bfs(g1, 0);
cout << "BFS:";
bfs.showPath(6);
}
int main()
{
//testG();
testDFS();
testBFS();
return 0;
}
时间复杂度分析:
- 稀疏图(邻接表):O(V + E)
- 稠密图(邻接矩阵):O(V^2)