6.5 图的遍历

6.5 图的遍历_第1张图片 

6.5 图的遍历_第2张图片

 前言:

主要内容:

这段文字描述的是图的遍历。图的遍历与树的遍历有些相似,都是从某个点开始,按照一定的规则遍历其他的点,但是图的遍历更加复杂,因为图中存在循环和回路,这意味着可能会多次访问到同一顶点。

### 主要内容:
1. **图的遍历**
   - 图的遍历是从图中的某一顶点出发,按照某种方法,对图中的所有顶点进行访问,每个顶点都只访问一次。
   - 图的遍历是图算法的基础,可以用于解决连通性问题、拓扑排序、寻找关键路径等。

2. **复杂性**
   - 图的遍历比树的遍历要复杂,因为图中的任一顶点都可能与其它顶点相邻,可能会存在回路,导致多次访问同一顶点。

3. **避免重复访问**
   - 为了避免同一顶点被访问多次,使用一个辅助数组 `visited` 来记录每个已经访问过的顶点。
   - 当顶点被访问时,将相应的 `visited` 数组的值设为 `true`。

4. **遍历方法**
   - 图的遍历主要有两种方法:深度优先搜索(DFS)和广度优先搜索(BFS)。
   - 这两种遍历方法都适用于无向图和有向图。

### 总结:
在图的遍历中,深度优先搜索和广度优先搜索是两种主要的遍历方法。深度优先搜索在探索尽可能深的分支时,会一直前进,直到没有更深的分支为止。而广度优先搜索则是一层一层地进行遍历。在进行图的遍历时,为了避免重复访问,通常需要一个 `visited` 数组来标记已经访问过的顶点。

6.5 图的遍历_第3张图片 

 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 图的遍历_第4张图片 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. **记录与计划:**
   - 为了记住你需要探索哪些路口,你会使用一张清单(队列)。
   - 每次你探索一个新的路口,你会将与它直接相连的所有未探索的路口加入到这张清单中。
   - 同时,你会在地图上标记每个已探索的路口,以防重复探索(访问标志数组)。

这个过程就像是一场由内而外的探索。你始终优先探索离你最近的、还未探索过的路口,直到整座城市都被你探索过。在这个过程中,你使用了一个清单(队列)来保持探索的顺序,以及一张地图(访问标志数组)来避免重复探索。

6.5 图的遍历_第5张图片

 

 

6.5 图的遍历_第6张图片 

例题: 

### 题目:岛屿数量

假设有一个二维数组,`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 
using namespace std;

class Solution {
public:
    int numIslands(vector>& 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>& 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>& 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>& grid, int i, int j) {
        queue> 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 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. 对于广度优先遍历,可以用一个队列来保存待访问的点。

6.5 图的遍历_第7张图片 

你可能感兴趣的:(深度优先,算法,数据结构,c++,leetcode)