上一篇:(二)图的存储结构及图的构建、释放——精选万字长文
专栏:图论专栏
专栏导航:专栏导航
正文开始
从图中一点出发系统地访问图中的所有点,并且每个点都只访问一次,这种运算操作称为图的遍历。图的遍历通常使用两种方法——深度优先搜索和广度优先搜索,本文将着重介绍深度优先搜索。
深度优先搜索
介绍
深度优先搜索(Depth First Search,简称DFS),是相对广度优先搜索较为常用、实用的一种方法。它遍历的主要方法是在保证每个点都不重复遍历的情况下,沿着某一分支一直往前,直到该点没有可以继续向前的路径为止。例如说在图中与该点有边相连的点都是已访问过的,或者该点根本就没有与除了上一个点外的其他点之间有边。这种情况下就需要“回溯”操作,指状态和位置回到上一个访问点的位置,继续遍历上一个访问点的其他分支。当一个点没有上一个点的时候,就可以直接结束这一次的搜索。为了避免图中分为多部分的情况,我们还需要继续寻找没有便历到的点,继续搜索该点所在部分。
图解
假设有一个有向图,它的邻接表和邻接矩阵形式如下:
注:这个图的边数较少,是一个稀疏图,所以推荐使用邻接表。
G
(
a
)
=
[
5
2
4
3
无
无
无
]
G(a)= \begin{bmatrix} 5 & 2 \\ 4 & 3 \\ 无 \\ 无 \\ 无 \end{bmatrix}
G(a)=
54无无无23
G
(
a
)
=
[
0
1
0
0
1
0
0
1
1
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
]
G(a)= \begin{bmatrix} 0 & 1 & 0 & 0 & 1 \\ 0 & 0 & 1 & 1 & 0 \\ 0 & 0 & 0 & 0 & 0 \\ 0 & 0 & 0 & 0 & 0 \\ 0 & 0 & 0 & 0 & 0 \end{bmatrix}
G(a)=
0000010000010000100010000
文字表达方法是:
G
(
a
)
=
[
有从结点
1
到结点
2
、
5
的边
有从结点
2
到结点
3
、
4
的边
没有以结点
3
为起点的边
没有以结点
4
为起点的边
没有以结点
5
为起点的边
]
G(a)= \begin{bmatrix} 有从结点1到结点2、5的边 \\ 有从结点2到结点3、4的边 \\ 没有以结点3为起点的边 \\ 没有以结点4为起点的边 \\ 没有以结点5为起点的边 \end{bmatrix}
G(a)=
有从结点1到结点2、5的边有从结点2到结点3、4的边没有以结点3为起点的边没有以结点4为起点的边没有以结点5为起点的边
G
(
a
)
=
[
1
−
1
不通
1
−
2
不通
1
−
3
不通
1
−
4
不通
1
−
5
不通
2
−
1
不通
2
−
2
不通
2
−
3
不通
2
−
4
不通
2
−
5
不通
3
−
1
不通
3
−
2
不通
3
−
3
不通
3
−
4
不通
3
−
5
不通
4
−
1
不通
4
−
2
不通
4
−
3
不通
4
−
4
不通
4
−
5
不通
5
−
1
不通
5
−
2
不通
5
−
3
不通
5
−
4
不通
5
−
5
不通
]
G(a)= \begin{bmatrix} 1-1不通 & 1-2不通 & 1-3不通 & 1-4不通 & 1-5不通 \\ 2-1不通 & 2-2不通 & 2-3不通 & 2-4不通 & 2-5不通 \\ 3-1不通 & 3-2不通 & 3-3不通 & 3-4不通 & 3-5不通 \\ 4-1不通 & 4-2不通 & 4-3不通 & 4-4不通 & 4-5不通 \\ 5-1不通 & 5-2不通 & 5-3不通 & 5-4不通 & 5-5不通 \end{bmatrix}
G(a)=
1−1不通2−1不通3−1不通4−1不通5−1不通1−2不通2−2不通3−2不通4−2不通5−2不通1−3不通2−3不通3−3不通4−3不通5−3不通1−4不通2−4不通3−4不通4−4不通5−4不通1−5不通2−5不通3−5不通4−5不通5−5不通
注:下图中用A、B、C、D、E分别表示结点1、2、3、4、5。
注:下图红色线段代表边,箭头说明了边的方向,终点处会出现箭头。如果这条边是可以双向通过的,则起点和终点结点都会出现箭头指示。
注:下图用图案虚化表示该点已访问,用黄色空心圆圈标出当前访问到的结点
原图如下:
一开始,从结点1开始访问。
随后,结点1访问到了结点2。
紧接着,结点2访问到了结点3。
但是我们可以发现,并没有以结点3为起点的边,也就是我们已经走到死路了,所以应当从结点3退回到结点2。
以结点2为起点的边一共有两条,分别通往结点3和结点4。而结点3已经访问过了,所以现在应该访问结点4。
但是与结点2同样,也没有以结点4为起点的边,所以从结点4退回结点2。
此时,起点为结点2的两条边的终点结点3、4都已被访问。所以结点2无路可走,只得退回结点1.
以结点1为起点的边一共有两条,一条通往结点2,一条通往结点5。由于结点2已经访问过了,所以这里进行对结点5的访问。
而结点5没有通往其他节点的边,所以又是死路,因此从结点5退回结点1.
可以发现,此时所有的结点都访问完毕,所以遍历完成!!!
代码实现
代码输入
注:使用邻接表实现的代码,起点为同一个点的边,输入的顺序与实际遍历顺序相反!!!如果想要两者输出相同,可以自己手动把输入顺序反过来,或者自己手动实现链表尾插的功能也可以,这里不多做赘述。
5 4
1 2 1
1 5 1
2 3 1
2 4 1
代码输出
-> 1 -> 2 -> 3 -> 4 -> 5//邻接矩阵实现的输出
-> 1 -> 5 -> 2 -> 4 -> 3//邻接表实现的输出
代码
使用邻接矩阵实现的代码
#include <stdio.h> //包含函数scanf和printf
#include <string.h> //包含函数memset
#define N (100) //该程序最多只能输入100个点
int g[N + 1][N + 1],n,k,from,to,w ;
//边数组,点数,边数,边的起点,边的终点,权值
char visit[N + 1] ;
//用于判断顶点是否访问
//定义深度优先搜索函数,参数为当前遍历结点
void dfs(int now){
if(visit[now])return ;
//如果该结点已经遍历过,直接返回
visit[now] = true ;
//标记该结点已经遍历
printf("-> %d ",now) ;
//输出当前访问的点的信息
for(int i = 1;i <= n;++ i)
if(g[now][i])dfs(i) ;
//访问以该点为起点的边所能够到达的顶点
return ;
}
int main(){
scanf("%d",&n) ;//输入点数
memset(g,0,sizeof(g)) ;//全部清零
//memset(g,0x7f,sizeof(g)) ;//全部设为一个很大的数
//memset(g,0xaf,sizeof(g)) ;//全部设为一个很小的数
memset(visit,0,sizeof(visit)) ;//全部清零
scanf("%d",&k) ;//输入边数
while(k --){
scanf("%d%d%d",&from,&to,&w) ;//输入边连接的顶点的序号及边的权值
g[from][to] = w ; //若无输入权值,设为1即可
//g[to][from] = w ; //由于无向图的对称性,需要镜像操作
//如果是有向图,则不需要这句代码,
//所以这里注释掉了
}
for(int i = 1;i <= n;++ i)
dfs(i) ;
//遍历点i以及能通过点i访问的点
return 0 ;
}
使用STL库实现邻接表代码
#include <stdio.h> //包含printf和scanf
#include <forward_list> //包含forward_list
#include <vector> //包含vector
using std::forward_list ;
//声明要使用STL库中的forward_list
using std::vector ; //声明要使用STL库中的vector
struct Point ; //声明顶点结构体
//定义边结构体
typedef struct Edge{
int v,dis ; //表示这条边的终点结点编号和权值
} Edge ; //用typedef语句,使下次使用时不用加struct
//定义顶点结构体
typedef struct Point{
forward_list<Edge> e ; //用于存储起点为自身的边
} Point ; //用typedef语句,使下次使用时不用加struct
vector<Point> a ;
//用于存储顶点
int n,m,u,v,d ;
//点数、边数、起点、终点、权值
vector<bool> visit ;
//用于判断顶点是否访问
//定义深度优先搜索函数,参数为当前遍历结点
void dfs(int now){
if(visit[now])return ;
//如果该结点已经遍历过,直接返回
visit[now] = true ;
//标记该结点已经遍历
printf("-> %d ",now) ;
//输出当前访问的点的信息
for(forward_list<Edge>::iterator i = a[now].e.begin();i != a[now].e.end();++ i)
dfs((*i).v) ; //访问以该点为起点的边所能够到达的顶点
return ;
}
int main(){
scanf("%d%d",&n,&m) ;
//输入点数、边数
a.resize(n + 1) ;
//预留足够的内存
visit.resize(n + 1,false) ;
//预留足够的内存
for(int i = 1;i <= m;++ i){
scanf("%d%d%d",&u,&v,&d) ;
//输入点数、边数、权值
a[u].e.push_front({v,d}) ;
//增加这条边
}
for(int i = 1;i <= n;++ i)
dfs(i) ;
//遍历点i以及能通过点i访问的点
return 0 ;
}
用数组模拟链表实现邻接表
#include <stdio.h> //包含函数scanf和printf
#include <vector> //包含vector
using std::vector ; //声明要使用STL库中的vector
#define N (100) //最多只能有100个点
#define M (N * (N - 1)) //计算最多会有几条边
struct Edge{ //定义边结构体
int next,to,dis ; //下一个边结点,这条边的终点结点编号,
//这条边的权值
} ;
vector<Edge> edge ; //定义边动态数组edge,节省内存
int head[N + 5],n,m,u,v,d ;
//存储每个点的边链表的头节点,点数,边数,起点,终点,权值
vector<bool> visit ;
//用于标记顶点是否访问。
//存储顶点边链表头节点的数组,顶点数,边数,起点顶点,终点顶点,权值
void add_edge(int from,int to,int dis){
//参数:起点顶点,终点顶点,权值
edge.push_back({head[from],to,dis}) ;
//增加新结点,next指针指向原来的头节点
head[from] = edge.size() - 1 ;
//新结点成为新的头节点
return ;
}
//定义深度优先搜索函数,参数为当前遍历结点
void dfs(int now){
if(visit[now])return ;
//如果该结点已经遍历过,直接返回
visit[now] = true ;
//标记该结点已经遍历
printf("-> %d ",now) ;
//输出当前访问的点的信息
int p = head[now] ;
//用于遍历边
while(p){
dfs(edge[p].to) ;
//访问以该边到达的顶点
p = edge[p].next ;
//遍历下一条边
}
return ;
}
int main(){
scanf("%d%d",&n,&m) ;
//输入顶点数、边数
visit.resize(n + 1,false) ;
//预留足够的空间
edge.push_back({-1,-1,-1}) ;
//提前加入一个无用的边,仅仅用于让边的编号能从一开始
for(int i = 1;i <= m;++ i){
//输入边
scanf("%d%d%d",&u,&v,&d) ;
//输入起点顶点,终点顶点,权值
add_edge(u,v,d) ;
//增添边
}
for(int i = 1;i <= n;++ i)
dfs(i) ;
//遍历点i以及能通过点i访问的点
return 0 ;
}
用自己实现的单向链表实现邻接表
#include <stdio.h> //包含函数scanf和printf
#include <string.h> //包含函数memset
struct Point ; //声明顶点结构体
typedef struct Edge{ //定义边结构体
struct Point *v ; //这条边的终点节点
struct Edge *next ; //这条边的下一条边
int dis ; //这条边的权值
} Edge ; //使用typedef语句,使下次使用时不用加struct
typedef struct Point{ //定义顶点结构体
Edge *e ; //边链表的的头节点
} Point ; //使用typedef语句,使下次使用时不用加struct
Point *head ; //顶点指针,用于动态分配顶点,数量可以不确定
void addEdge(Point* from,Point* to,int dis) ;
//声明函数addEdge,用于增加一条由from顶点到to顶点,权值为dis的边
void del(Point* head,int from,int to) ;
//声明函数del,用于释放顶点数组和边链表使用的内存
int n,m,u,v,d ;
//点数,边数,边的起点、终点、权值。
char *visit ;
//用于判断顶点是否访问
//定义深度优先搜索函数,参数为当前遍历结点
void dfs(Point* now){
if(visit[now - head])return ;
//如果该结点已经遍历过,直接返回
visit[now - head] = true ;
//标记该结点已经遍历
printf("-> %d ",now - head) ;
//输出当前访问的点的信息
Edge* p = now -> e ;
//用于边的遍历
while(p){
dfs(p -> v) ;
//访问该边到达的顶点
p = p -> next ;
//遍历下一条边
}
return ;
}
int main(){
scanf("%d%d",&n,&m) ;
//输入点数与边数
visit = new char[n + 1] ;
//提前分配给标记数组足够的内存
memset(visit,0,n + 1) ;
//全部设为零,即未访问
head = new Point[n + 1] ;
//为head指针分配(n+1)个Point结构体的内存
//可以以head[i]的形式访问(i=0~n)
for(int i = 1;i <= n;++ i)
//由于只需要1~n,所以直接初始化1~n即可
head[i].e = 0 ;
//初始化s,e为零,避免野指针。
for(int i = 1;i <= m;++ i){
//初始化m条边
scanf("%d%d%d",&u,&v,&d) ;
//输入边的起点、终点的编号及边的权值
addEdge(head + u,head + v,d) ;
//给点u增添一条到点v、长度为d的边
}
for(int i = 1;i <= n;++ i)
dfs(head + i) ;
//遍历点i以及能通过点i访问的点
del(head,1,n) ;
//释放顶点数组和边链表使用的内存
return 0 ;
}
//作用:向点from的边链表中增加一条有向从from到to长度为dis的边。
void addEdge(Point* from,Point* to,int dis){
//参数:边的起点、终点顶点的指针及边的权值
Edge *ed = new Edge ;
//分配一个边结构体
(ed -> v) = to ;
//表示这条边的终点是to顶点
(ed -> dis) = dis ;
//表示这条边的权值是dis
(ed -> next) = (from -> e) ;
//因为只存储了链表的头结点,只能在链表头部插入结点,
//则将新结点的后继指针指向原来的头结点
(from -> e) = ed ;
//新结点成为头节点
return ;
}
//作用:释放顶点数组和边链表使用的内存
void del(Point* head,int from,int to){
//参数:顶点数组,
//使用到的第一个的顶点和最后一个顶点的编号
if(!head)return ;
//如果顶点数组为空,直接返回
while(from <= to){
if(!head[from].e){ ++ from ; continue ; }
//如果边链表为空,不进行操作
Edge *now = head[from].e,*next ;
//存储当前结点和当前结点的下一个节点
while(now){
//重复直到到达尾结点
next = now -> next ;
//先保存下一个结点
delete now ;
//释放当前结点
now = next ;
//准备释放下一个结点
}
head[from].e = 0 ;
//指针清零
++ from ;
//遍历下一个结点
}
delete[] head ;
//释放顶点数组内存
return ;
}
尾声
如果你认真看到了这里——恭喜你,通过一篇七千字好文透彻地学习了利用深度优先搜索实现图的遍历的四种方法!你的编程水平又提升了一个阶梯。
想要学习更多关于“图论”的知识,请移步:
专栏:图论专栏
专栏导航:专栏导航