力扣解题-200. 岛屿数量
给你一个由 '1'(陆地)和 '0'(水)组成的的二维网格,请你计算网格中岛屿的数量。
岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。 此外,你可以假设该网格的四条边均被水包围。
示例 1:
输入:grid = [
['1','1','1','1','0'],
['1','1','0','1','0'],
['1','1','0','0','0'],
['0','0','0','0','0']
]
输出:1
示例 2:
输入:grid = [
['1','1','0','0','0'],
['1','1','0','0','0'],
['0','0','1','0','0'],
['0','0','0','1','1']
]
输出:3
提示:
m == grid.length
n == grid[i].length
1 <= m, n <= 300
grid[i][j] 的值为 '0' 或 '1'
Related Topics
深度优先搜索、广度优先搜索、并查集、数组、矩阵
第一次解答
解题思路
核心方法:深度优先搜索(DFS)淹没法,遍历网格中的每个位置,发现陆地('1')时计数+1,并通过DFS将该陆地及其所有相邻的陆地“淹没”(置为'0'),确保每块岛屿仅被计数一次,时间复杂度O(m×n)、空间复杂度O(m×n)(最坏情况全为陆地,递归栈深度达m×n),是本题最直观、易实现的经典解法。
核心逻辑拆解
计算岛屿数量的核心是“发现一块陆地,就标记其所有连通陆地为已访问”,避免重复计数:
- 空网格处理:若网格为null或行数为0,直接返回0(无岛屿);
- 初始化变量:
m/n:记录网格的行数和列数,方便后续边界判断;count:岛屿计数器,初始为0;
- 网格遍历:双重循环遍历每个网格位置
(i,j):- 若当前位置是陆地(
grid[i][j] == '1'):- 计数器
count++(找到新岛屿); - 调用DFS方法,将该陆地及其所有上下左右连通的陆地全部置为'0'(“淹没”,标记为已访问);
- 计数器
- 若当前位置是陆地(
- DFS核心逻辑(淹没连通陆地):
- 递归终止条件:坐标越界(
i<0/j<0/i>=m/j>=n)或当前位置不是陆地(grid[i][j] != '1'); - 淹没当前陆地:将
grid[i][j]置为'0',避免重复访问; - 递归遍历上下左右四个方向:继续淹没相邻的陆地。
- 递归终止条件:坐标越界(
执行流程可视化(以示例2为例)
| 遍历步骤 | 位置(i,j) | 网格值 | 操作 | count | 关键结果 |
|---|---|---|---|---|---|
| 1 | (0,0) | '1' | count=1,DFS淹没(0,0)连通区 | 1 | 第一块岛屿被标记为0 |
| 2 | (0,2) | '0' | 无操作 | 1 | - |
| 3 | (2,2) | '1' | count=2,DFS淹没(2,2)连通区 | 2 | 第二块岛屿被标记为0 |
| 4 | (3,3) | '1' | count=3,DFS淹没(3,3)连通区 | 3 | 第三块岛屿被标记为0 |
关键细节说明
- 原地修改网格:直接将访问过的陆地置为'0',无需额外的访问标记数组,节省空间;
- 边界判断顺序:DFS中先判断坐标越界,再判断网格值,避免数组下标越界异常;
- 连通方向:仅考虑上下左右四个方向(题目明确“水平/竖直相邻”,不包含对角线);
- 递归终止条件:必须同时判断越界和非陆地,缺一不可(如越界位置无需处理,非陆地直接返回)。
性能说明
- 时间复杂度:O(m×n)(每个网格位置仅被访问一次,DFS过程中被淹没的陆地不会重复处理);
- 空间复杂度:O(m×n)(最坏情况网格全为陆地,递归栈深度等于网格总元素数;平衡场景下为O(min(m,n)));
- 优势:
- 逻辑直观,贴合“找连通区域”的问题本质;
- 原地修改网格,空间效率高;
- 代码简洁,仅需基础DFS框架即可实现。
public int numIslands(char[][] grid) {
if(grid==null || grid.length==0){
return 0;
}
int m=grid.length;
int n=grid[0].length;
int count=0;
for(int i=0;i<m;i++){
for(int j=0;j<n;j++){
if(grid[i][j]=='1'){
count++;//找到岛屿
dfs(grid,i,j);//将岛屿淹没
}
}
}
return count;
}
public void dfs(char[][] grid,int i,int j){
if(i<0 || j<0 || i>=grid.length || j>=grid[0].length || grid[i][j]!='1'){
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);//右
}
示例解答
解题思路
解法1:广度优先搜索(BFS)法(避免递归栈溢出)
核心方法:用队列替代递归实现连通区域的遍历,发现陆地后通过BFS将其所有相邻陆地淹没,逻辑与DFS一致,但避免了递归栈溢出风险(如超大网格场景)。
代码实现
public int numIslands(char[][] grid) {
if (grid == null || grid.length == 0) {
return 0;
}
int m = grid.length;
int n = grid[0].length;
int count = 0;
// 定义上下左右四个方向的偏移量
int[][] dirs = {{-1,0}, {1,0}, {0,-1}, {0,1}};
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (grid[i][j] == '1') {
count++;
// 队列存储坐标,用数组或自定义类封装
Queue<int[]> queue = new LinkedList<>();
queue.offer(new int[]{i, j});
grid[i][j] = '0'; // 入队时立即标记为已访问,避免重复入队
// BFS遍历连通区域
while (!queue.isEmpty()) {
int[] curr = queue.poll();
int x = curr[0];
int y = curr[1];
// 遍历四个方向
for (int[] dir : dirs) {
int nx = x + dir[0];
int ny = y + dir[1];
// 边界判断+陆地判断
if (nx >= 0 && nx < m && ny >= 0 && ny < n && grid[nx][ny] == '1') {
queue.offer(new int[]{nx, ny});
grid[nx][ny] = '0'; // 入队时标记,避免重复处理
}
}
}
}
}
}
return count;
}
核心逻辑说明
- 方向偏移量:用
dirs数组统一管理上下左右四个方向,简化代码; - 队列存储坐标:用
int[]封装网格坐标,入队时立即将陆地置为'0',避免同一位置多次入队; - BFS循环:队列非空时,持续弹出当前坐标,遍历四个方向,将符合条件的陆地入队并标记。
性能说明
- 时间复杂度:O(m×n)(与DFS一致);
- 空间复杂度:O(min(m,n))(最坏情况队列存储一行/一列的所有陆地,如网格为长条形陆地);
- 优势:
- 非递归实现,避免超大网格导致的栈溢出;
- 队列的空间复杂度通常低于DFS的递归栈;
- 劣势:代码量略多于DFS,需管理队列和坐标封装。
解法2:并查集(Union-Find)法(适合动态连通性场景)
核心方法:将每个陆地位置视为一个节点,相邻陆地合并为同一个集合,最终统计集合的数量即为岛屿数,适合需要频繁查询/修改连通性的进阶场景。
代码实现
class UnionFind {
private int count; // 连通分量数量
private int[] parent;
private int[] rank; // 按秩合并,优化效率
public UnionFind(char[][] grid) {
int m = grid.length;
int n = grid[0].length;
parent = new int[m * n];
rank = new int[m * n];
count = 0;
// 初始化并查集
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (grid[i][j] == '1') {
parent[i * n + j] = i * n + j; // 父节点指向自己
count++; // 初始每个陆地都是独立集合
}
rank[i * n + j] = 0;
}
}
}
// 查找根节点(路径压缩)
public int find(int x) {
if (parent[x] != x) {
parent[x] = find(parent[x]);
}
return parent[x];
}
// 合并两个集合(按秩合并)
public void union(int x, int y) {
int rootX = find(x);
int rootY = find(y);
if (rootX != rootY) {
if (rank[rootX] > rank[rootY]) {
parent[rootY] = rootX;
} else if (rank[rootX] < rank[rootY]) {
parent[rootX] = rootY;
} else {
parent[rootY] = rootX;
rank[rootX]++;
}
count--; // 合并后连通分量数-1
}
}
public int getCount() {
return count;
}
}
public int numIslands(char[][] grid) {
if (grid == null || grid.length == 0) {
return 0;
}
int m = grid.length;
int n = grid[0].length;
UnionFind uf = new UnionFind(grid);
int[][] dirs = {{1,0}, {0,1}}; // 仅需检查下、右两个方向,避免重复合并
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (grid[i][j] == '1') {
// 遍历下、右方向,合并相邻陆地
for (int[] dir : dirs) {
int nx = i + dir[0];
int ny = j + dir[1];
if (nx < m && ny < n && grid[nx][ny] == '1') {
uf.union(i * n + j, nx * n + ny);
}
}
}
}
}
return uf.getCount();
}
核心逻辑说明
- 坐标映射:将二维坐标
(i,j)映射为一维索引i*n + j,方便并查集存储; - 并查集初始化:每个陆地节点的父节点指向自己,初始连通分量数为陆地总数;
- 合并相邻陆地:遍历每个陆地,仅检查下、右两个方向(避免重复合并上、左方向),将相邻陆地合并为同一集合;
- 结果统计:最终连通分量数即为岛屿数。
性能说明
- 时间复杂度:O(m×n×α(m×n))(α为阿克曼函数的反函数,接近常数,可视为O(m×n));
- 空间复杂度:O(m×n)(存储parent和rank数组);
- 优势:
- 适合动态连通性场景(如频繁添加/删除陆地后查询岛屿数);
- 路径压缩+按秩合并,效率接近线性;
- 劣势:
- 代码实现复杂,新手入门门槛高;
- 一次性查询场景下,效率略低于DFS/BFS。
总结
- DFS淹没法(第一次解答):O(m×n)时间+O(m×n)空间,逻辑直观、代码简洁,是新手入门的首选解法;
- BFS法:O(m×n)时间+O(min(m,n))空间,非递归实现,避免栈溢出,适合超大网格场景;
- 并查集法:O(m×n×α(m×n))时间+O(m×n)空间,适合动态连通性场景,工程拓展性强;
- 关键技巧:
- 核心思想:岛屿数量=连通陆地集合的数量,核心是“标记已访问的陆地”避免重复计数;
- 空间优化:DFS/BFS可原地修改网格,无需额外标记数组;
- 方向优化:并查集仅需检查下、右方向,减少重复合并操作。