数据结构:图的遍历
记录遍历状态
对于图结构来说,图的遍历和树的遍历有类似之处,树结构的遍历从根结点出发,图结构的遍历从某一结点出发。出发之后,按照某种手法无重复地访问所有的结点,这也是后续解决图的连通性、拓扑排序和关键路径的预备知识。
由于在图结构中,任意顶点都有可能与其他顶点相互邻接,因此如果没有对已走过的路径进行记录的话,很有可能会由于结点的重复访问而无法遍历所有顶点。因此我们需要一种手法记录访问过的顶点,一种直接而有效的手法是使用一个 visited[n] 数组,先将其每一个元素初始化为 0,当我访问了第 i 个顶点时,就将 visited[i] 的值赋值为 1,表示已经访问过。当我访问某一个顶点时,可以通过 visited 数组来确定我接下来是否要从这个顶点往下走。
DFS
深度优先搜索
深度优先搜索( Depth First Serarch )我们之前是接触过的,在迷宫问题(栈实现)和树结构的递归遍历法中,我们用其思想实现了一些功能,现在我们来详细谈一谈。所谓 DFS,我称之为视角放在路径的手法,思想是通过对某一条路径的顶点的挖掘,从而试探出一条可行的路径。当我使用递归或者通过修饰后的栈结构,可以实现回溯的效果,以获取全部的路径。
算法流程
对于一个连通图,DFS 的遍历过程为:
- 选择图中的某个顶点出发,并访问该顶点;
- 找出刚访问过的顶点的第一个未被访问的邻接点并访问;
- 以该顶点为新顶点,步骤 2 直至刚访问过的顶点没有未被访问的邻接点;
- 返回前一个访问过的且仍有未被访问的邻接点的顶点,找出该顶点的下一个未被访问的邻接点,并访问该顶点;
- 重复上述 2、3、4 步骤直至所有顶点访问完毕。
模拟遍历
例如上图的连通图,我们选择顶点 A 出发,访问该顶点:
选取 A 的邻接点 B,访问该结点:
选取 B 的邻接点 D,访问该结点:
选取 D 的邻接点 H,访问该结点:
选取 H 的邻接点 E,访问该结点:
由于 E 顶点的所有邻接点都遍历完了,因此需要回溯。回溯需要 4 次回到顶点 A,并访问下一个邻接点 C:
选取 C 的邻接点 F,访问该结点:
选取 F 的邻接点 G,访问该结点:
遍历完毕,顺序为 A->B->D->H->E->C->F->G。其深度优先生成树为:
代码实现
由于 DFS 需要涉及到回溯问题,因此我们想到使用递归来实现。
邻接矩阵 DFS
int visited[MAXV] = { 0 };
void DFS(MGraph g, int v) //v 表示当前所在的顶点
{
cout << v << " "; //输出顶点
visited[v] = 1; //标记已访问
for (int i = 1; i <= g.n; i++) //使用循环控制回溯
{
if (g.edges[v][i] == 1 && visited[i] == 0)
{
DFS(g, i); //当前顶点与 i 顶点邻接且未被访问,递归搜索
}
}
}
邻接表 DFS
int visited[MAXV] = { 0 };
void DFS(AdjGraph* G, int v) //从 v 顶点开始深度遍历
{
ArcNode* ptr;
cout << v << " "; //输出顶点
visited[v] = 1; //标记已访问
ptr = G->adjlist[v].firstarc;
while (ptr) //沿着邻接表搜索路径
{
if (visited[ptr->adjvex] == 0)
{
DFS(G, ptr->adjvex); //当前顶点与 i 顶点邻接且未被访问,递归搜索
}
ptr = ptr->nextarc; //继续访问邻接表
}
}
时间复杂度
当我们遍历一个连通图时,对图中每个顶点至多调用一次 DFS 函数,并且当这个顶点被访问过之后,由于被标志成已被访问,就不再从它出发进行搜索。因此,遍历图的过程实质上是对每个顶点查找其邻接点的过程,其时间复杂度则取决于使用的存储结构。当用邻接矩阵描述图结构时,查找每个顶点的邻接点的时间复杂度为 O(n2),其中 n 为图结构中的顶点数。当以邻接表做图的存储结构时,查找邻接点的时间复杂度为 O(e),e 为图结构的边数。由此当以邻接表做存储结构时,深度优先搜索遍历图的时间复杂度为 O(n + e)。
BFS
广度优先搜索
广度优先搜索( Breadth First Serarch )我们之前也有接触过,在迷宫问题(队列实现)和树结构的层序遍历法中,我们用其思想实现了一些功能,现在我们来详细谈一谈。所谓 BFS,我称之为视角放在整个图结构的手法,思想是通过从某个顶点向外扩散,从而囊括所有顶点的手法。当我借助队列结构时,可以实现该算法。
算法流程
对于一个连通图,DFS 的遍历过程为:
- 选择图中的某个顶点出发,并访问该顶点;
- 依次访问 v 的每个未曾访问过的邻接点 i;
- 分别从这些邻接点出发,依次访问它们的邻接点,并使“先被访问的顶点的邻接点”先于“后被访问的顶点的邻接点”被访问;
- 重复步骤 3,直至图中所有己被访问的顶点的邻接点全部访问到。
模拟遍历
例如上图的连通图,我们选择顶点 A 出发,访问该顶点:
访问顶点 A 的所有邻接点:
访问顶点 B 的所有邻接点:
访问顶点 C 的所有邻接点:
访问顶点 D 的所有邻接点:
接着访问剩下的顶点,由于所有的顶点都被访问过了,因此没有新顶点入队列。遍历顺序为:A->B->C->D->E->F->G,广度优先生成树为:
代码实现
由于 BFS 需要涉及到回溯问题,因此我们想到使用递归来实现。
广度优先搜索遍历图结构其实和树结构的层序遍历是一个玩意,尽可能先对横向进行搜索。设 x 和 y 是两个相继被访问过的顶点,若当前是以 X 为出发点进行搜索,则在访问 x 的所有未曾被访问过的邻接点之后,紧接着是以 y 为出发点进行横向搜索,并对搜索到的 y 的邻接点中尚未被访问的顶点进行访问。也就是说,先访问的顶点其邻接点亦先被访问,因此我们需要一个队列来辅助实现算法。
邻接矩阵 BFS
void BFS(MGraph g, int v)
{
int front, rear; //头指针与尾指针
int point[MAXV]; //构造队列结构(可以是非循环队列)
front = 0;
point[front] = v; //顶点 v 入队列
rear = visited[v] = 1; //标记顶点已访问
while (front != rear) //队列不为空,搜索继续
{
for (int i = 1; i <= g.n; i++) //遍历表头顶点的邻接点
{
if (g.edges[point[front]][i] == 1 && visited[i] == 0)
{
point[rear++] = i; //顶点 i 入队列
visited[i] = 1; //标记顶点已访问
}
}
cout << point[front++] << " "; //输出顶点
}
邻接表 BFS
void BFS(AdjGraph* G, int v) //顶点 v 开始广度遍历
{
int front, rear; //头指针与尾指针
int a_que[MAXV]; //构造队列结构(可以是非循环队列
ArcNode* ptr;
front = 0;
a_que[front] = v; //顶点 v 入队列
visited[v] = rear = 1; //标记顶点已访问
while (front != rear) //队列不为空,搜索继续
{
ptr = G->adjlist[a_que[front]].firstarc;
while (ptr) //遍历表头顶点的邻接点
{
if (visited[ptr->adjvex] == 0)
{
a_que[rear++] = ptr->adjvex; //顶点 i 入队列
visited[ptr->adjvex] = 1; //标记顶点已访问
}
ptr = ptr->nextarc;
}
cout << point[front++] << " "; //输出顶点
}
}
时间复杂度
对于 BFS,每个顶点至多进一次队列,因此遍历图的过程实质上是通过边找邻接点的过程。因此广度优先搜索遍历图的时间复杂度和深度优先搜索遍历相同,两种遍历方法的不同之处仅仅在于对顶点访问的顺序不同。
实例:六度空间
情景需求
输入样例
10 9
1 2
2 3
3 4
4 5
5 6
6 7
7 8
8 9
9 10
输出样例
1: 70.00%
2: 80.00%
3: 90.00%
4: 100.00%
5: 100.00%
6: 100.00%
7: 100.00%
8: 90.00%
9: 80.00%
10: 70.00%
情景解析
这道题表面上看好像不好懂,但在本质上是一个有限的图结构遍历,即我们可能在遍历到某些顶点的时候就需要提前结束了。基于这一点,我们反应过来用 BFS 做比较方便一点,因为 BFS 的流程可以抽象为一层一层地往外探测,这与我们的思路是符合的。
那么接下来我们读题,根据题意,我们需要找到关系网在 6 层以内的结点。那也就是说,我们需要去记录某个顶点,它对于初始顶点来说是第几层的关系网。为了方便理解,我展示的做法是去修改记录结点信息的数组的结构体为:
typedef struct
{
int level; //表示结点相对目标顶点的距离
int v;
}AVertex;
即多开一个成员来记录层次的信息。那么层次的信息怎么操作?首先我们先初始化为 0,接下来每进行一轮 BFS,添加入队列的顶点就从它的上一层继承层数,可以用这行代码实现:
a_que[rear].level = a_que[front].level + 1;
解决了层次的确定问题,剩下的就是我们喜闻乐见的建图和遍历的事情啦。
伪代码
代码实现
int getCount(AdjGraph* G, int v)
{
int visited[MAXV] = { 0 };
space a_que[MAXV] = { 0 };
int front, rear;
int count = 1;
ArcNode* ptr;
front = rear = 0;
a_que[rear].v = v; //目标顶点 v 入队列
a_que[rear++].level = 0; //初始化层数为 0
visited[v] = 1;
while (front != rear)
{
if (a_que[front].level == 6)
{
break; //表头顶点在第 6 层,结束 BFS
}
ptr = G->adjlist[a_que[front].v].firstarc;
while (ptr)
{
if (visited[ptr->adjvex] == 0)
{
a_que[rear].v = ptr->adjvex;
a_que[rear++].level = a_que[front].level + 1; //继承上一个顶点的层数
visited[ptr->adjvex] = 1;
count++;
}
ptr = ptr->nextarc;
}
front++;
}
return count;
}
扩充资料
实例:判断 DFS 序列合法性
左转我另一篇博客——PTA习题解析——判断DFS序列的合法性
参考资料
《大话数据结构》—— 程杰 著,清华大学出版社
《数据结构(C语言版|第二版)》—— 严蔚敏 李冬梅 吴伟民 编著,人民邮电出版社