前言:
主要内容:
这段文字描述的是图的遍历。图的遍历与树的遍历有些相似,都是从某个点开始,按照一定的规则遍历其他的点,但是图的遍历更加复杂,因为图中存在循环和回路,这意味着可能会多次访问到同一顶点。
### 主要内容:
1. **图的遍历**
- 图的遍历是从图中的某一顶点出发,按照某种方法,对图中的所有顶点进行访问,每个顶点都只访问一次。
- 图的遍历是图算法的基础,可以用于解决连通性问题、拓扑排序、寻找关键路径等。
2. **复杂性**
- 图的遍历比树的遍历要复杂,因为图中的任一顶点都可能与其它顶点相邻,可能会存在回路,导致多次访问同一顶点。
3. **避免重复访问**
- 为了避免同一顶点被访问多次,使用一个辅助数组 `visited` 来记录每个已经访问过的顶点。
- 当顶点被访问时,将相应的 `visited` 数组的值设为 `true`。
4. **遍历方法**
- 图的遍历主要有两种方法:深度优先搜索(DFS)和广度优先搜索(BFS)。
- 这两种遍历方法都适用于无向图和有向图。
### 总结:
在图的遍历中,深度优先搜索和广度优先搜索是两种主要的遍历方法。深度优先搜索在探索尽可能深的分支时,会一直前进,直到没有更深的分支为止。而广度优先搜索则是一层一层地进行遍历。在进行图的遍历时,为了避免重复访问,通常需要一个 `visited` 数组来标记已经访问过的顶点。
6.5.1 深度优先搜索
主要内容:
### 6.5.1 深度优先搜索 (DFS)
#### 1. 深度优先搜索遍历过程
- 深度优先搜索遍历类似于树的先序遍历。
- 对于连通图的遍历过程:
1. 从图中某顶点v出发,访问v。
2. 找出刚访问过的顶点的未被访问的邻接点,访问该顶点,并以该顶点为新顶点重复此步骤,直至没有未被访问的邻接点为止。
3. 返回至上一个有未访问邻接点的顶点,访问其未被访问的邻接点。
4. 重复2和3步骤,直至所有顶点都被访问过。
- 举例: 无向图G₄的深度优先搜索遍历图如图6.17(b)所示。
- 深度优先搜索所构成的所有顶点和实箭头的边,称为深度优先生成树。
#### 2. 算法实现
- 深度优先搜索遍历是递归的。
- 使用访问标志数组`visited[n]`来标记顶点是否被访问。
**算法6.3 深度优先搜索遍历连通图**
void DFS(Graph G, int v) {
cout << v;
visited[v] = true;
for (w = FirstAdjVex(G, v); w >= 0; w = NextAdjVex(G, v, w))
if (!visited[w]) DFS(G, w);
}
- 如果是非连通图,需要循环调用该算法,直到所有顶点都被访问过。
**算法6.4 深度优先搜索遍历非连通图**
void DFSTraverse(Graph G) {
for (v = 0; v < G.vexnum; ++v) visited[v] = false;
for (v = 0; v < G.vexnum; ++v) if (!visited[v]) DFS(G, v);
}
- 算法6.5 和算法6.6 分别采用邻接矩阵和邻接表实现了深度优先搜索遍历。
#### 3. 算法分析
- 每个顶点至多调用一次DFS()函数。
- 时间复杂度取决于存储结构。邻接矩阵为O(n²);邻接表为O(e),其中n为顶点数,e为边数。
### 总结
深度优先搜索(DFS)是一种常用的图遍历算法,适用于连通图和非连通图。它从图中的一个顶点出发,递归地访问其所有未访问过的邻接点。DFS的实现依赖于图的存储结构,因此,不同的存储结构会导致不同的实现方法和时间复杂度。在实际应用中,需要根据具体情况选择合适的存储结构和实现方法。
我的理解:
我们可以将深度优先搜索(DFS)比喻为一个人在迷宫中探路的过程。
### 深度优先搜索(DFS)如同探索迷宫:
1. **迷宫与图**
- 想象一个迷宫,其中每个房间是图中的一个顶点,每个通道是边。
- 迷宫的入口是搜索的起始顶点。
2. **探险家与算法执行**
- 想象探险家是在执行DFS的算法。
- 探险家进入一个房间(访问一个顶点)时会做记号,避免再次进入(标记为已访问)。
3. **深入探索与递归**
- 探险家每次会选择一个未探索的通道深入探索,直到来到一个没有未探索通道的房间(递归访问邻接点)。
- 在无法继续深入时,他会回溯到上一个房间,寻找其他未探索的通道(回溯)。
4. **全图遍历**
- 如果迷宫有多个不连通的区域,探险家会完成一个区域的探索后,返回到入口,选择另一个未探索的区域进行探索,直到整个迷宫都被探索完毕。
- 这就如同在非连通图中,从每个未访问的顶点出发进行DFS,直至图中的所有顶点都被访问。
### 深度优先生成树
就像探险家探索迷宫时可能会画出一张地图,这张地图记录了他访问过的所有房间和他通过的通道,这张地图就像是深度优先搜索过程中生成的树,称为深度优先生成树。
### 总结
通过以上的比喻,深度优先搜索就像一个探险家在未知的迷宫中进行探索,他会深入每一个通道,直到不能再深入为止,然后回溯到上一个分岔口,继续探索其他通道,直到整个迷宫都被探索完毕。在这个过程中,探险家画出的迷宫地图就是深度优先生成树。
6.5.2 广度优先搜索
主要内容:
### 6.5.2 广度优先搜索
#### 1. 广度优先搜索遍历的过程
广度优先搜索(BFS)类似于树的层次遍历。过程如下:
- (1) 从图中某个顶点v出发并访问v。
- (2) 依次访问v的各个未曾访问过的邻接点。
- (3) 对每个邻接点,再依次访问它们的邻接点,重复此过程,直至所有已被访问的顶点的邻接点都被访问到。
- 例子中展示了一种具体的广度优先搜索过程,所有的顶点和边构成了一棵广度优先生成树。
#### 2. 广度优先搜索遍历的算法实现
广度优先搜索的特点是尽可能先进行横向搜索。算法实现时需要引入队列来保存已被访问过的顶点,并需要一个访问标志数组来记录哪些顶点被访问过。
【算法步骤】
- ① 从图中某个顶点v出发,访问v,并将visited[v]置为true,然后使v入队。
- ② 只要队列不空,重复以下操作:
- 队头元素u出队;
- 依次检查u的所有邻接点w,如果visited[w]为false, 则访问w,并将visited[w]置为true,然后使w入队。
【算法描述】
void BFS(Graph G, int v) {
cout << v;
visited[v] = true; // 访问第v个顶点,并将访问标志数组的相应分量值置为true
InitQueue(Q); // 辅助队列Q初始化,置空
EnQueue(Q, v); // v入队
while (!QueueEmpty(Q)) { // 队列非空
DeQueue(Q, u); // 队头元素出队并置为u
for (w = FirstAdjVex(G, u); w >= 0; w = NextAdjVex(G, u, w)) // 依次检查u的所有邻接点w
if (!visited[w]) {
cout << w;
visited[w] = true;
EnQueue(Q, w);
}
}
}
#### 3. 广度优先搜索的时间复杂度
广度优先搜索的时间复杂度与遍历所有顶点和边的次数有关,通常情况下,广度优先搜索的时间复杂度为\(O(V + E)\),其中V是顶点的数量,E是边的数量。
#### 笔记总结
广度优先搜索(BFS)是一种用于遍历或搜索树或图的算法。这个算法使用队列来记录要访问的顶点,并使用一个访问标志数组来避免重复访问。广度优先搜索的基本思想是“宽度优先”,即先访问起始顶点的所有邻接点,然后再访问这些邻接点的未访问邻接点。此算法通常用于找到无权图中的最短路径。
我的理解:
当然可以。我们可以将广度优先搜索(BFS)比喻为在一座未知的城市中探索。
假设你现在站在一个城市的某个路口(初始顶点),你的目标是探索这座城市中的每个路口(每个顶点)。城市中的每条街道(边)都连接着两个路口(两个顶点)。
1. **开始探索:**
- 当你来到一个新的路口,你会在地图上标记这个路口(访问顶点,并标记为已访问)。
- 然后,你会查看与这个路口直接相连的所有其他路口(邻接点),并将它们加入到你的“待探索”名单中(入队)。
2. **探索邻居:**
- 你会先探索与起始路口直接相连的所有路口,即在移动到下一层邻居之前,你会探索所有当前层的邻居(横向搜索)。
- 在探索每个路口时,你会将所有未被探索的相邻路口加入到“待探索”名单中。
- 你会重复这个过程,直到所有的路口都被探索过(队列为空)。
3. **记录与计划:**
- 为了记住你需要探索哪些路口,你会使用一张清单(队列)。
- 每次你探索一个新的路口,你会将与它直接相连的所有未探索的路口加入到这张清单中。
- 同时,你会在地图上标记每个已探索的路口,以防重复探索(访问标志数组)。
这个过程就像是一场由内而外的探索。你始终优先探索离你最近的、还未探索过的路口,直到整座城市都被你探索过。在这个过程中,你使用了一个清单(队列)来保持探索的顺序,以及一张地图(访问标志数组)来避免重复探索。
例题:
### 题目:岛屿数量
假设有一个二维数组,`0` 表示水域,`1` 表示陆地。相邻的陆地形成一个岛屿,求出岛屿的数量。
例如:
```plaintext
11110
11010
11000
00000
```
输出:`1`
```plaintext
11000
11000
00100
00011
```
输出:`3`
### 思考过程和分析过程:
#### 思考过程:
1. **深度优先遍历:**
从一个陆地出发,尽可能深入,将与其相连的所有陆地都访问一遍,每找到一个陆地就将其标记为已访问,最终我们就能得到一个岛屿。
2. **广度优先遍历:**
从一个陆地出发,访问其所有相邻的陆地,将未访问的陆地加入队列,再依次访问队列中的每个陆地及其相邻陆地,直到队列为空。
#### 分析过程:
1. 对于深度优先遍历和广度优先遍历,我们都需要遍历整个二维数组,当遇到一个`1`时,就从这个点出发,进行深度优先遍历或广度优先遍历,标记相连的所有陆地,岛屿数加`1`。
2. 时间复杂度:
遍历整个二维数组,每个点都会被访问一次,所以时间复杂度为:\(O(m \times n)\),其中`m`和`n`分别为二维数组的行数和列数。
### C++ 实现:
#### 深度优先遍历
#include <vector>
using namespace std;
class Solution {
public:
int numIslands(vector<vector<char>>& grid) {
if(grid.empty() || grid[0].empty()) return 0;
int count = 0;
for(int i = 0; i < grid.size(); ++i) {
for(int j = 0; j < grid[0].size(); ++j) {
if(grid[i][j] == '1') {
dfs(grid, i, j);
++count;
}
}
}
return count;
}
void dfs(vector<vector<char>>& grid, int i, int j) {
if(i < 0 || i >= grid.size() || j < 0 || j >= grid[0].size() || grid[i][j] == '0')
return;
grid[i][j] = '0';
dfs(grid, i + 1, j);
dfs(grid, i - 1, j);
dfs(grid, i, j + 1);
dfs(grid, i, j - 1);
}
};
#### 广度优先遍历
#include <vector>
#include <queue>
using namespace std;
class Solution {
public:
int numIslands(vector<vector<char>>& grid) {
if(grid.empty() || grid[0].empty()) return 0;
int count = 0;
for(int i = 0; i < grid.size(); ++i) {
for(int j = 0; j < grid[0].size(); ++j) {
if(grid[i][j] == '1') {
bfs(grid, i, j);
++count;
}
}
}
return count;
}
void bfs(vector<vector<char>>& grid, int i, int j) {
queue<pair<int, int>> q;
q.push({i, j});
grid[i][j] = '0';
while(!q.empty()) {
auto [x, y] = q.front(); q.pop();
if(x > 0 && grid[x - 1][y] == '1') {
q.push({x - 1, y});
grid[x - 1][y] = '0';
}
if(x < grid.size() - 1 && grid[x + 1][y] == '1') {
q.push({x + 1, y});
grid[x + 1][y] = '0';
}
if(y > 0 && grid[x][y - 1] == '1') {
q.push({x, y - 1});
grid[x][y - 1] = '0';
}
if(y < grid[0].size() - 1 && grid[x][y + 1] == '1') {
q.push({x, y + 1});
grid[x][y + 1] = '0';
}
}
}
};
### Java 实现
#### 深度优先遍历
class Solution {
public int numIslands(char[][] grid) {
if(grid == null || grid.length == 0 || grid[0].length == 0) return 0;
int count = 0;
for(int i = 0; i < grid.length; ++i) {
for(int j = 0; j < grid[0].length; ++j) {
if(grid[i][j] == '1') {
dfs(grid, i, j);
++count;
}
}
}
return count;
}
void dfs(char[][] grid, int i, int j) {
if(i < 0 || i >= grid.length || j < 0 || j >= grid[0].length || grid[i][j] == '0')
return;
grid[i][j] = '0';
dfs(grid, i + 1, j);
dfs(grid, i - 1, j);
dfs(grid, i, j + 1);
dfs(grid, i, j - 1);
}
}
#### 广度优先遍历
import java.util.Queue;
import java.util.LinkedList;
class Solution {
public int numIslands(char[][] grid) {
if(grid == null || grid.length == 0 || grid[0].length == 0) return 0;
int count = 0;
for(int i = 0; i < grid.length; ++i) {
for(int j = 0; j < grid[0].length; ++j) {
if(grid[i][j] == '1') {
bfs(grid, i, j);
++count;
}
}
}
return count;
}
void bfs(char[][] grid, int i, int j) {
Queue<int[]> q = new
LinkedList<>();
q.add(new int[]{i, j});
grid[i][j] = '0';
while(!q.isEmpty()) {
int[] cur = q.poll();
int x = cur[0], y = cur[1];
if(x > 0 && grid[x - 1][y] == '1') {
q.add(new int[]{x - 1, y});
grid[x - 1][y] = '0';
}
if(x < grid.length - 1 && grid[x + 1][y] == '1') {
q.add(new int[]{x + 1, y});
grid[x + 1][y] = '0';
}
if(y > 0 && grid[x][y - 1] == '1') {
q.add(new int[]{x, y - 1});
grid[x][y - 1] = '0';
}
if(y < grid[0].length - 1 && grid[x][y + 1] == '1') {
q.add(new int[]{x, y + 1});
grid[x][y + 1] = '0';
}
}
}
}
### C实现
由于C语言编写会稍显复杂一些,这里简要描述一下步骤和关键实现:
1. 用一个二维字符数组表示地图,用两重循环遍历地图上的每一点。
2. 如果找到一个‘1’,就用深度优先遍历或者广度优先遍历将与它相连的所有‘1’都标记为‘0’,同时岛屿数量+1。
3. 对于深度优先遍历,可以用递归实现。
4. 对于广度优先遍历,可以用一个队列来保存待访问的点。