图的遍历

从图中某一顶点出发访遍图中其余顶点,且使每一个顶点仅被访问一次,这一过程就叫做图的遍历

通常情况下图的遍历方式有两种:深度优先遍历(Depth First Search)和广度优先遍历(Breadth First Search)


深度优先遍历(DFS)

        从起点出发,沿着一条路径走到头(没有未被访问的节点了),再回溯到上一个顶点,然后选择其他路径继续深入,直到遍历完所有顶点。

        实现方式:栈 / 递归(此处代码均使用递归实现)

仅遍历图中所有节点的代码如下:

#include <iostream>
#include <vector>
using namespace std;

//邻接边表节点结构 
struct Edge{
	int end;
	int weight;
};

vector<bool> isVisited;//标记对应下标的节点是否被访问过 

void dfs(vector<vector<Edge>> &G,int x){
	//当前节点已访问过,返回 
	if(isVisited[x]){
		return;
	}
	
	isVisited[x]=true;
	
	cout<<"当前访问节点:"<<x<<endl;
	//遍历当前顶点的所有邻接顶点 
	for(int i=0;i<G[x].size();i++){
		int en = G[x][i].end;
		
		if(!isVisited[en]){
			dfs(G,en);
		}
	}
}

int main(){
	int n;//顶点数
	int m;//边数
	
	cin>>n;
	cin>>m;
	
	isVisited.resize(n+1,false);
	vector<vector<Edge>> G(n+1);//邻接边表存图
	
	//输入边
	for(int i=0;i<m;i++){
		int st,en,wei;//起点 终点 权值
		
		cin>>st>>en>>wei;
		G[st].push_back({en,wei}); 
	} 
	
	cout<<endl;
	cout<<"DFS遍历结果如下:"<<endl;
	//遍历每个节点 
	for(int i=1;i<=n;i++){
		if(!isVisited[i]){
			dfs(G,i);
		}
	}
	
	return 0;
}

在做题时,DFS经常被用于处理走迷宫等需要找出符合条件的路径的问题,此处借助力扣的一道题200. 岛屿数量来帮助理解。

题干是这样的:

给你一个由 '1'(陆地)和 '0'(水)组成的的二维网格,请你计算网格中岛屿的数量。

岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。

此外,你可以假设该网格的四条边均被水包围。

读题中信息可知,我们可以将二维网格放置在二维数组构成的图中,从图的左上角开始向右下角遍历(因为这样遍历符合下标递增顺序),定义ans存储岛屿数量,每当遍历到一处岛屿时就将ans+1,当遍历完整个图(下标遍历到了最大值)后输出ans即可

算法实现伪代码如下:

vector<vector<bool>> visited;//标记对应下标位置是否被访问过
int dir[4][2] = { { -1, 0 }, { 1, 0 }, { 0, -1 }, { 0, 1 } };//方向数组

void dfs(vector<vector<char>>& grid, int row, int col) {
	//若当前位置已经访问过,则直接返回
	if (visited[row][col]) {
		return;
	}
	//若当前位置是海洋,则直接返回
	if (grid[row][col] == '0') {
		return;
	}

	int n = grid.size();//行边界
	int m = grid[0].size();//列边界

	visited[row][col] = true;//标记当前位置已访问
	//访问当前位置四周的单元格
	for (int i = 0; i < 4; i++) {
		int dx = row + dir[i][0];
		int dy = col + dir[i][1];

		if (dx >= 0 && dx < n && dy >= 0 && dy < m && !visited[dx][dy]) {
			dfs(grid, dx, dy);
		}
	}
}

int numIslands(vector<vector<char>>& grid) {
	int n = grid.size();//行
	int m = grid[0].size();//列

	//初始化visited数组全为false
	visited.resize(n, vector<bool>(m));

	int ans = 0;
	for (int i = 0; i < n; i++) {
		for (int j = 0; j < m; j++) {
			//若当前位置是岛屿且未被访问过,ans++
			if (grid[i][j] == '1' && !visited[i][j]) {
				dfs(grid, i, j);
				ans++;
			}
		}
	}

	return ans;
}

根据这道题,我们可以总结出一个适用于迷宫找路径等问题的dfs模版来,大致如下:

void dfs(图,当前顶点数据/顶点的坐标){
	边界判断(是否超出范围,是否已访问过,是否为题目所需的位置...) 
	
	标记当前位置已访问
	
	遍历当前位置旁边的格子(大部分情况是上下左右四个方向){
		
		 若当前格子满足条件,则对该格子进行dfs(不超出范围){
		 	dfs(图,当前位置的行坐标,当前位置的列坐标)
		 } 
	}
	
	回溯,改变该位置状态为未访问(特定题目可省略)
}

在不同的情景下所需dfs的形式可能是不同的,但其逻辑是不变的,所以需要在掌握dfs的逻辑后根据不同情景适当改变dfs的形式


广度优先遍历(BFS)

       从起点出发,先访问所有直接相连的邻点,再访问所有邻点的邻点,逐层扩散,直到遍历完所有的顶点。

        实现方式:队列

仅遍历图中所有节点的代码如下:

#include <iostream>
#include <vector>
#include <queue>
#include <algorithm>
using namespace std;

//邻接边表节点结构
struct Edge{
	int end;//终点
	int weight;//权值 
}; 

vector<bool> isVisited;//标记对应下标的节点是否被访问过 

void bfs(vector<vector<Edge>> &G,int x){
	queue<int> q;
	
	//起点入队 
	q.push(x);
	isVisited[x]=true;//标记该顶点为已访问状态 
	
	while(!q.empty()){
		//取出队首元素并将其出队列 
		int s = q.front();
		q.pop();
		
		cout<<"当前访问顶点:"<<s<<endl;
		
		//遍历以当前顶点为起点的边的所有终点 
		for(int i=0;i<G[s].size();i++){
			//保存终点 
			Edge e = G[s][i];
			int en = e.end;
			
			//若该终点未被访问过,将其入队并标记状态为true 
			if(!isVisited[en]){
				isVisited[en]=true;
				q.push(en);
			}
		}
	}
}

int main(){
	int n,m;//顶点数 边数
	cin>>n>>m; 
	
	vector<vector<Edge>> G(n+1);
	
	for(int i=0;i<m;i++){
		int st,en,wei;
		cin>>st>>en>>wei;
		
		G[st].push_back({en,wei});
	}
	
	isVisited.resize(n+1,false);
	
	
	cout<<endl;
	cout<<"BFS遍历结果如下:"<<endl;
	//遍历每个节点 
	for(int i=1;i<=n;i++){
		if(!isVisited[i]){
			bfs(G,i);
		}
	}
	
	return 0;
}

同DFS一样,BFS也经常在做题时遇到,且被用于解决求最短路径等问题,此处我们还是借助 200. 岛屿数量来帮助理解。

BFS算法实现伪代码如下:

vector<vector<bool>> visited;//标记对应下标位置是否被访问过
int dir[4][2] = { { -1, 0 }, { 1, 0 }, { 0, -1 }, { 0, 1 } };//方向数组

void bfs(vector<vector<char>>& grid, int row, int col) {
	int n = grid.size();//行边界
	int m = grid[0].size();//列边界

	queue<pair<int,int>> q;//核心队列
	q.push({ row, col });//当前位置入队列
	visited[row][col] = true;//标记当前位置已访问

	//访问当前位置四周的单元格
	while (!q.empty()) {
		pair<int, int> s = q.front();//取出队首元素
		q.pop();//队首元素出队

		//遍历当前位置的四个方向
		for (int i = 0; i < 4; i++) {
			int dx = s.first + dir[i][0];
			int dy = s.second + dir[i][1];

			if (dx >= 0 && dx < n && dy >= 0 && dy < m && !visited[dx][dy] && grid[dx][dy] == '1') {
				//若找到岛屿,将其入队并标记状态为已访问
				q.push({ dx,dy });
				visited[dx][dy] = true;
			}
		}
	}
}

int numIslands(vector<vector<char>>& grid) {
	int n = grid.size();//行
	int m = grid[0].size();//列

	//初始化visited数组全为false
	visited.resize(n, vector<bool>(m));

	int ans = 0;
	for (int i = 0; i < n; i++) {
		for (int j = 0; j < m; j++) {
			//若当前位置是岛屿且未被访问过,ans++
			if (grid[i][j] == '1' && !visited[i][j]) {
				bfs(grid, i, j);
				ans++;
			}
		}
	}

	return ans;
}

总结出的bfs模版大致如下:

void bfs(图,当前顶点的数据/当前顶点的坐标){
	queue<数据类型> q;//核心队列 
	
	//当前顶点入队并标记该顶点状态为已访问 
	q.push(当前顶点);
	Visited[当前顶点]=true;
	
	//while循环 
	while(!q.empty()){
		//取出队首元素并将其出队列 
		数据类型 vertex = q.front();
		q.pop();
		
		//遍历
		for(int i=0;i<四周相连顶点的个数;i++){
			
			用方向数组/结构体保存周围顶点 
			
			if(限制条件){
				满足限制条件,将该顶点入队,标记该顶点状态为已访问 
			}
		}
	}
}


写在最后

        DFS和BFS在数据结构及算法题当中占据重要地位,应用场景很多,所以需要深入理解并掌握,希望我的博客能够帮助到大家,谢谢!

更多推荐