这段文字描述的是图的遍历。图的遍历与树的遍历有些相似,都是从某个点开始,按照一定的规则遍历其他的点,但是图的遍历更加复杂,因为图中存在循环和回路,这意味着可能会多次访问到同一顶点。
### 主要内容:
1. **图的遍历**
- 图的遍历是从图中的某一顶点出发,按照某种方法,对图中的所有顶点进行访问,每个顶点都只访问一次。
- 图的遍历是图算法的基础,可以用于解决连通性问题、拓扑排序、寻找关键路径等。
2. **复杂性**
- 图的遍历比树的遍历要复杂,因为图中的任一顶点都可能与其它顶点相邻,可能会存在回路,导致多次访问同一顶点。
3. **避免重复访问**
- 为了避免同一顶点被访问多次,使用一个辅助数组 `visited` 来记录每个已经访问过的顶点。
- 当顶点被访问时,将相应的 `visited` 数组的值设为 `true`。
4. **遍历方法**
- 图的遍历主要有两种方法:深度优先搜索(DFS)和广度优先搜索(BFS)。
- 这两种遍历方法都适用于无向图和有向图。
### 总结:
在图的遍历中,深度优先搜索和广度优先搜索是两种主要的遍历方法。深度优先搜索在探索尽可能深的分支时,会一直前进,直到没有更深的分支为止。而广度优先搜索则是一层一层地进行遍历。在进行图的遍历时,为了避免重复访问,通常需要一个 `visited` 数组来标记已经访问过的顶点。
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 广度优先搜索
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×n),其中`m`和`n`分别为二维数组的行数和列数。
C++ 实现:
深度优先遍历
- #include
- using namespace std;
-
- class Solution {
- public:
- int numIslands(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
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
- #include
- using namespace std;
-
- class Solution {
- public:
- int numIslands(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
char >>& grid, int i, int j) { - queue
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. 对于广度优先遍历,可以用一个队列来保存待访问的点。
在迷宫搜索问题中,深度优先搜索(DFS)和广度优先搜索(BFS)有不同的优势,这取决于迷宫的结构和出口的位置。
深度优先搜索(DFS)的优势:
广度优先搜索(BFS)的优势:
理论上,两种算法都能找到出口,但在实际应用中,迷宫的具体布局和出口位置会影响它们的效率。DFS在深且直的路径中表现良好,而BFS在寻找最短路径和探索近处区域时更为高效。通常,在不了解迷宫具体信息的情况下,BFS更常被用于找到最短路径,而DFS在某些情况下因其内存使用较少而被选用。
深度优先搜索(DFS)和广度优先搜索(BFS)是两种常用的图和树搜索算法,各有其优缺点及适用场景。
优点:
缺点:
适用场景:
优点:
缺点:
适用场景: