250 lines
9.5 KiB
Markdown
250 lines
9.5 KiB
Markdown
|
||
# 200. 岛屿数量
|
||
|
||
[题目链接](https://leetcode.cn/problems/number-of-islands/)
|
||
|
||
给你一个由 '1'(陆地)和 '0'(水)组成的的二维网格,请你计算网格中岛屿的数量。
|
||
|
||
岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。
|
||
|
||
此外,你可以假设该网格的四条边均被水包围。
|
||
|
||

|
||
|
||
提示:
|
||
|
||
* m == grid.length
|
||
* n == grid[i].length
|
||
* 1 <= m, n <= 300
|
||
* grid[i][j] 的值为 '0' 或 '1'
|
||
|
||
## 思路
|
||
|
||
注意题目中每座岛屿只能由**水平方向和/或竖直方向上**相邻的陆地连接形成。
|
||
|
||
也就是说斜角度链接是不算了, 例如示例二,是三个岛屿,如图:
|
||
|
||

|
||
|
||
这道题题目是 DFS,BFS,并查集,基础题目。
|
||
|
||
本题思路,是用遇到一个没有遍历过的节点陆地,计数器就加一,然后把该节点陆地所能遍历到的陆地都标记上。
|
||
|
||
在遇到标记过的陆地节点和海洋节点的时候直接跳过。 这样计数器就是最终岛屿的数量。
|
||
|
||
那么如果把节点陆地所能遍历到的陆地都标记上呢,就可以使用 DFS,BFS或者并查集。
|
||
|
||
### 深度优先搜索
|
||
|
||
以下代码使用dfs实现,如果对dfs不太了解的话,建议先看这篇题解:[797.所有可能的路径](https://leetcode.cn/problems/all-paths-from-source-to-target/solution/by-carlsun-2-66pf/),
|
||
|
||
C++代码如下:
|
||
|
||
```CPP
|
||
// 版本一
|
||
class Solution {
|
||
private:
|
||
int dir[4][2] = {0, 1, 1, 0, -1, 0, 0, -1}; // 四个方向
|
||
void dfs(vector<vector<char>>& grid, vector<vector<bool>>& visited, int x, int y) {
|
||
for (int i = 0; i < 4; i++) {
|
||
int nextx = x + dir[i][0];
|
||
int nexty = y + dir[i][1];
|
||
if (nextx < 0 || nextx >= grid.size() || nexty < 0 || nexty >= grid[0].size()) continue; // 越界了,直接跳过
|
||
if (!visited[nextx][nexty] && grid[nextx][nexty] == '1') { // 没有访问过的 同时 是陆地的
|
||
|
||
visited[nextx][nexty] = true;
|
||
dfs(grid, visited, nextx, nexty);
|
||
}
|
||
}
|
||
}
|
||
public:
|
||
int numIslands(vector<vector<char>>& grid) {
|
||
int n = grid.size(), m = grid[0].size();
|
||
vector<vector<bool>> visited = vector<vector<bool>>(n, vector<bool>(m, false));
|
||
|
||
int result = 0;
|
||
for (int i = 0; i < n; i++) {
|
||
for (int j = 0; j < m; j++) {
|
||
if (!visited[i][j] && grid[i][j] == '1') {
|
||
visited[i][j] = true;
|
||
result++; // 遇到没访问过的陆地,+1
|
||
dfs(grid, visited, i, j); // 将与其链接的陆地都标记上 true
|
||
}
|
||
}
|
||
}
|
||
return result;
|
||
}
|
||
};
|
||
```
|
||
|
||
很多录友可能有疑惑,为什么 以上代码中的dfs函数,没有终止条件呢? 感觉递归没有终止很危险。
|
||
|
||
其实终止条件 就写在了,调用dfs的地方,如果遇到不合法的方向,直接不会去调用dfs。
|
||
|
||
当然,也可以这么写:
|
||
|
||
```CPP
|
||
// 版本二
|
||
class Solution {
|
||
private:
|
||
int dir[4][2] = {0, 1, 1, 0, -1, 0, 0, -1}; // 四个方向
|
||
void dfs(vector<vector<char>>& grid, vector<vector<bool>>& visited, int x, int y) {
|
||
if (visited[x][y] || grid[x][y] == '0') return; // 终止条件:访问过的节点 或者 遇到海水
|
||
visited[x][y] = true; // 标记访问过
|
||
for (int i = 0; i < 4; i++) {
|
||
int nextx = x + dir[i][0];
|
||
int nexty = y + dir[i][1];
|
||
if (nextx < 0 || nextx >= grid.size() || nexty < 0 || nexty >= grid[0].size()) continue; // 越界了,直接跳过
|
||
dfs(grid, visited, nextx, nexty);
|
||
}
|
||
}
|
||
public:
|
||
int numIslands(vector<vector<char>>& grid) {
|
||
int n = grid.size(), m = grid[0].size();
|
||
vector<vector<bool>> visited = vector<vector<bool>>(n, vector<bool>(m, false));
|
||
|
||
int result = 0;
|
||
for (int i = 0; i < n; i++) {
|
||
for (int j = 0; j < m; j++) {
|
||
if (!visited[i][j] && grid[i][j] == '1') {
|
||
result++; // 遇到没访问过的陆地,+1
|
||
dfs(grid, visited, i, j); // 将与其链接的陆地都标记上 true
|
||
}
|
||
}
|
||
}
|
||
return result;
|
||
}
|
||
};
|
||
```
|
||
|
||
这里大家应该能看出区别了,无疑就是版本一中 调用dfs 的条件,放在了 版本二 的 终止条件位置上。
|
||
|
||
**版本一的写法**是 :下一个节点是否能合法已经判断完了,只要调用dfs就是可以合法的节点。
|
||
|
||
**版本二的写法**是:不管节点是否合法,上来就dfs,然后在终止条件的地方进行判断,不合法再return。
|
||
|
||
**理论上来讲,版本一的效率更高一些**,因为避免了 没有意义的递归调用,在调用dfs之前,就做合法性判断。 但从写法来说,可能版本二 更利于理解一些。(不过其实都差不太多)
|
||
|
||
很多同学看了同一道题目,都是dfs,写法却不一样,有时候有终止条件,有时候连终止条件都没有,其实这就是根本原因,两种写法而已。
|
||
|
||
|
||
### 广度优先搜索
|
||
|
||
不少同学用广搜做这道题目的时候,超时了。 这里有一个广搜中很重要的细节:
|
||
|
||
根本原因是**只要 加入队列就代表 走过,就需要标记,而不是从队列拿出来的时候再去标记走过**。
|
||
|
||
很多同学可能感觉这有区别吗?
|
||
|
||
如果从队列拿出节点,再去标记这个节点走过,就会发生下图所示的结果,会导致很多节点重复加入队列。
|
||
|
||

|
||
|
||
超时写法 (从队列中取出节点再标记)
|
||
|
||
```CPP
|
||
int dir[4][2] = {0, 1, 1, 0, -1, 0, 0, -1}; // 四个方向
|
||
void bfs(vector<vector<char>>& grid, vector<vector<bool>>& visited, int x, int y) {
|
||
queue<pair<int, int>> que;
|
||
que.push({x, y});
|
||
while(!que.empty()) {
|
||
pair<int ,int> cur = que.front(); que.pop();
|
||
int curx = cur.first;
|
||
int cury = cur.second;
|
||
visited[curx][cury] = true; // 从队列中取出在标记走过
|
||
for (int i = 0; i < 4; i++) {
|
||
int nextx = curx + dir[i][0];
|
||
int nexty = cury + dir[i][1];
|
||
if (nextx < 0 || nextx >= grid.size() || nexty < 0 || nexty >= grid[0].size()) continue; // 越界了,直接跳过
|
||
if (!visited[nextx][nexty] && grid[nextx][nexty] == '1') {
|
||
que.push({nextx, nexty});
|
||
}
|
||
}
|
||
}
|
||
|
||
}
|
||
```
|
||
|
||
加入队列 就代表走过,立刻标记,正确写法:
|
||
|
||
```CPP
|
||
int dir[4][2] = {0, 1, 1, 0, -1, 0, 0, -1}; // 四个方向
|
||
void bfs(vector<vector<char>>& grid, vector<vector<bool>>& visited, int x, int y) {
|
||
queue<pair<int, int>> que;
|
||
que.push({x, y});
|
||
visited[x][y] = true; // 只要加入队列,立刻标记
|
||
while(!que.empty()) {
|
||
pair<int ,int> cur = que.front(); que.pop();
|
||
int curx = cur.first;
|
||
int cury = cur.second;
|
||
for (int i = 0; i < 4; i++) {
|
||
int nextx = curx + dir[i][0];
|
||
int nexty = cury + dir[i][1];
|
||
if (nextx < 0 || nextx >= grid.size() || nexty < 0 || nexty >= grid[0].size()) continue; // 越界了,直接跳过
|
||
if (!visited[nextx][nexty] && grid[nextx][nexty] == '1') {
|
||
que.push({nextx, nexty});
|
||
visited[nextx][nexty] = true; // 只要加入队列立刻标记
|
||
}
|
||
}
|
||
}
|
||
|
||
}
|
||
```
|
||
|
||
以上两个版本其实,其实只有细微区别,就是 `visited[x][y] = true;` 放在的地方,着去取决于我们对 代码中队列的定义,队列中的节点就表示已经走过的节点。 **所以只要加入队列,理解标记该节点走过**。
|
||
|
||
本题完整广搜代码:
|
||
|
||
```CPP
|
||
class Solution {
|
||
private:
|
||
int dir[4][2] = {0, 1, 1, 0, -1, 0, 0, -1}; // 四个方向
|
||
void bfs(vector<vector<char>>& grid, vector<vector<bool>>& visited, int x, int y) {
|
||
queue<pair<int, int>> que;
|
||
que.push({x, y});
|
||
visited[x][y] = true; // 只要加入队列,立刻标记
|
||
while(!que.empty()) {
|
||
pair<int ,int> cur = que.front(); que.pop();
|
||
int curx = cur.first;
|
||
int cury = cur.second;
|
||
for (int i = 0; i < 4; i++) {
|
||
int nextx = curx + dir[i][0];
|
||
int nexty = cury + dir[i][1];
|
||
if (nextx < 0 || nextx >= grid.size() || nexty < 0 || nexty >= grid[0].size()) continue; // 越界了,直接跳过
|
||
if (!visited[nextx][nexty] && grid[nextx][nexty] == '1') {
|
||
que.push({nextx, nexty});
|
||
visited[nextx][nexty] = true; // 只要加入队列立刻标记
|
||
}
|
||
}
|
||
}
|
||
}
|
||
public:
|
||
int numIslands(vector<vector<char>>& grid) {
|
||
int n = grid.size(), m = grid[0].size();
|
||
vector<vector<bool>> visited = vector<vector<bool>>(n, vector<bool>(m, false));
|
||
|
||
int result = 0;
|
||
for (int i = 0; i < n; i++) {
|
||
for (int j = 0; j < m; j++) {
|
||
if (!visited[i][j] && grid[i][j] == '1') {
|
||
result++; // 遇到没访问过的陆地,+1
|
||
bfs(grid, visited, i, j); // 将与其链接的陆地都标记上 true
|
||
}
|
||
}
|
||
}
|
||
return result;
|
||
}
|
||
};
|
||
|
||
```
|
||
|
||
## 总结
|
||
|
||
其实本题是 dfs,bfs 模板题,但正是因为是模板题,所以大家或者一些题解把重要的细节都很忽略了,我这里把大家没注意的但以后会踩的坑 都给列出来了。
|
||
|
||
|
||
|
||
|
||
|
||
## 其他语言版本
|