LeetCode 第 200 题「岛屿数量」
字数 2550 2025-10-25 17:21:47
好的,我们这次来详细讲解 LeetCode 第 200 题「岛屿数量」。
这是一道非常经典的深度优先搜索(DFS)和广度优先搜索(BFS)的应用题,也是理解「网格类 DFS」的绝佳起点。
1. 题目描述
题目链接:LeetCode 200 - Number of Islands
难度:中等
题目描述:
给你一个由 '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
解释:图中所有的 '1' 都是连在一起的,所以算作一个岛屿。
示例 2:
输入:grid = [
["1","1","0","0","0"],
["1","1","0","0","0"],
["0","0","1","0","0"],
["0","0","0","1","1"]
]
输出:3
解释:有三块互不相连的陆地,所以是三个岛屿。
2. 问题分析与核心思路
第一步:理解问题本质
我们可以把整个网格看作一张地图:
'1'代表一小块陆地。'0'代表海洋。- 如果两块陆地在上、下、左、右四个方向之一相邻,它们就属于同一个岛屿。
我们的目标是:找出图中所有连通分量的个数。这里的「连通分量」就是指由相邻的 '1' 组成的一个最大集合。
第二步:直观的暴力解法思考
一个最直接的想法是:遍历网格中的每一个点。
- 如果遇到一块陆地 (
'1'),我们就发现了一个新的岛屿。但是,和这块陆地相邻的所有陆地都属于同一个岛屿,我们不能重复计数。 - 所以,当我们发现一块新的陆地时,我们需要做两件事:
- 岛屿数量加 1。
- 通过搜索(DFS/BFS)将属于这个岛屿的所有陆地标记为“已访问”,这样在后续的遍历中就不会再重复计算它们。
第三步:关键的「标记」策略
如何标记「已访问」的陆地呢?有两种常见方法:
- 使用一个额外的二维
visited数组,记录每个位置是否被访问过。 - 直接修改原网格:将访问过的陆地
'1'修改成另一个字符,例如'0'或'2'。这种方法更节省空间,我们通常采用这种方法。
核心算法(洪水填充法):
- 初始化岛屿计数器
count = 0。 - 遍历网格中的每一个单元格
(i, j)。 - 如果当前单元格是
'1'(未访问的陆地):- 将
count加 1。 - 从
(i, j)开始,进行 DFS 或 BFS,将所有与之相连的陆地(即整个岛屿)全部标记为已访问(例如,改为'0')。
- 将
- 遍历完成后,
count就是岛屿的数量。
3. 解决方案详解:深度优先搜索 (DFS)
DFS 的思路是“不撞南墙不回头”。从一块陆地出发,尽可能深地搜索它的四个方向,直到所有相连的陆地都被访问。
步骤拆解:
-
主函数
numIslands:- 处理特殊情况(网格为空)。
- 遍历每一个格子。
- 遇到
'1',就启动一次 DFS,同时计数器加一。
-
DFS 辅助函数
dfs:- 递归终止条件:
- 当前坐标
(i, j)超出网格边界(i < 0或j < 0或i >= 行数或j >= 列数)。 - 当前格子不是陆地(是
'0',也就是海洋或者已访问过的陆地)。
- 当前坐标
- 处理当前节点:将当前陆地
'1'标记为'0'(表示已访问,防止重复计算)。 - 递归访问四个方向:对上、下、左、右四个方向的格子调用 DFS 函数。
- 递归终止条件:
代码实现(Python)
class Solution:
def numIslands(self, grid: List[List[str]]) -> int:
# 如果网格是空的,直接返回0
if not grid or not grid[0]:
return 0
# 获取网格的行数和列数
rows, cols = len(grid), len(grid[0])
count = 0 # 初始化岛屿计数器
# 定义DFS递归函数
def dfs(i, j):
# 递归终止条件:越界或者当前格子不是未访问的陆地
if i < 0 or j < 0 or i >= rows or j >= cols or grid[i][j] != '1':
return
# 标记当前陆地为已访问(沉没这座岛屿的一部分)
grid[i][j] = '0'
# 递归遍历四个方向:上、下、左、右
dfs(i - 1, j) # 上
dfs(i + 1, j) # 下
dfs(i, j - 1) # 左
dfs(i, j + 1) # 右
# 遍历网格中的每一个格子
for i in range(rows):
for j in range(cols):
# 如果发现一块未访问的陆地
if grid[i][j] == '1':
count += 1 # 发现一个新岛屿
dfs(i, j) # 用DFS"沉没"整个岛屿,避免重复计算
return count
DFS 过程图解(以示例1为例):
- 遍历到
(0,0),发现'1',count变为 1。 - 从
(0,0)开始 DFS:- 标记
(0,0)为'0'。 - 检查上方:越界,返回。
- 检查下方
(1,0):是'1',递归。- 标记
(1,0)为'0'。 - 检查
(1,0)的四个方向...(这个过程会像洪水一样蔓延,把所有连通的'1'都标记为'0')。
- 标记
- 标记
- 当这次 DFS 结束时,整个最初的岛屿的所有陆地都被「沉没」了(标记为
'0')。 - 继续遍历网格,剩下的格子都是
'0',所以不会再启动 DFS。 - 最终
count = 1。
4. 解决方案详解:广度优先搜索 (BFS)
BFS 的思路是“一圈一圈地扩散”。从一块陆地出发,先访问它所有直接相邻的陆地,再访问相邻陆地的相邻陆地。
步骤拆解:
- 主函数逻辑不变。
- BFS 辅助过程:
- 使用一个队列(例如
collections.deque)来存储待访问的陆地坐标。 - 将起始陆地
(i, j)加入队列,并立即标记为已访问。 - 当队列不为空时:
- 从队列中取出一个坐标
(x, y)。 - 检查它的四个方向。
- 如果某个方向
(nx, ny)是未访问的陆地'1',则将其标记为已访问,并加入队列。
- 从队列中取出一个坐标
- 使用一个队列(例如
代码实现(Python)
from collections import deque
class Solution:
def numIslands(self, grid: List[List[str]]) -> int:
if not grid or not grid[0]:
return 0
rows, cols = len(grid), len(grid[0])
count = 0
# 定义四个方向向量:上、右、下、左
directions = [(-1, 0), (0, 1), (1, 0), (0, -1)]
for i in range(rows):
for j in range(cols):
if grid[i][j] == '1':
count += 1
# 开始BFS
queue = deque([(i, j)])
grid[i][j] = '0' # 标记为已访问
while queue:
x, y = queue.popleft()
# 遍历四个邻居
for dx, dy in directions:
nx, ny = x + dx, y + dy
# 检查新坐标是否合法且是未访问的陆地
if 0 <= nx < rows and 0 <= ny < cols and grid[nx][ny] == '1':
grid[nx][ny] = '0' # 标记为已访问
queue.append((nx, ny))
return count
5. 总结与对比
| 特性 | 深度优先搜索 (DFS) | 广度优先搜索 (BFS) |
|---|---|---|
| 核心思想 | 递归深入,一条路走到底再回溯 | 层层扩散,先访问完所有相邻节点 |
| 数据结构 | 递归调用栈(隐式栈) | 队列(显式,如 deque) |
| 空间复杂度 | 最坏情况 O(M*N),网格深度可能很大 | 平均情况可能比DFS好,取决于岛屿形状 |
| 适用场景 | 代码简洁,易于实现 | 寻找最短路径时更优(本题不是) |
对于本题,DFS 通常更常用,因为代码更简洁。但两者的时间复杂度和空间复杂度都是 O(M × N),其中 M 和 N 是网格的行数和列数,因为每个点最多被访问一次。
6. 关键点回顾
- 核心思想:找到一块新陆地 -> 岛屿数+1 -> 标记整个连通分量。
- 标记已访问:通过修改原数组,将
'1'变为'0',避免重复计算。 - 搜索方式:DFS(递归/栈)或 BFS(队列)均可。
- 方向处理:通常使用方向数组
[(-1,0), (1,0), (0,-1), (0,1)]来简化代码。
这道题是图论搜索的基础,掌握了它,对于后续的「岛屿的最大面积」、「被围绕的区域」等题目都会有很大的帮助。希望这个循序渐进的讲解能让你彻底理解!