LeetCode 200 岛屿数量:python3 题解
题目链接:200. 岛屿数量
目录
1. 题目理解
简单来说:
我们有一张地图,上面画着陆地('1')和水('0')。
如果两块陆地是上下左右相邻的,它们就属于同一个岛屿。
斜着相邻不算。
我们的任务是数一数,这张地图里一共有多少个独立的岛屿。
核心难点:
如何避免重复计数?
比如一个很大的岛屿由几十个 '1' 组成,我们遍历到第一个 '1' 时知道这是一个新岛屿,但接下来遍历到它旁边的 '1' 时,不能把它当成新岛屿。我们需要一种机制,把属于同一个岛屿的所有 '1' 都标记为“已访问”。
2. 解题思路总览
这道题本质上是图论中的连通分量计数问题。我们可以把网格看作一个图,每个格子是一个节点,相邻的 '1' 之间有边相连。
我们有三种主流解法:
- 深度优先搜索 (DFS):最常用,代码简洁。发现一个
'1',就递归地把周围相连的'1'全部“淹没”(标记为已访问)。 - 广度优先搜索 (BFS):使用队列。发现一个
'1',把它放入队列,然后一层层把周围相连的'1'都找出来并标记。 - 并查集 (Union-Find):一种数据结构方法。初始时每个
'1'都是独立的集合,如果两个'1'相邻,就把它们合并到同一个集合。最后数有多少个集合。
下面将重点讲解 DFS 和 BFS,因为它们最直观且面试中最常考,最后简要介绍并查集。
3. 解法一:深度优先搜索 (DFS)
思路讲解
想象我们站在一个陆地上('1')。
- 我们数一下:“这是一个新岛屿,计数 +1"。
- 然后,我们开始“淹没”这个岛屿。首先把自己站的位置改成水(
'0'),表示这里已经处理过了。 - 接着,我们看上下左右四个方向,如果旁边还是陆地(
'1'),我们就走到那边去,重复第 2 步。 - 这个过程会一直持续,直到这个岛屿所有的陆地都变成了水。
- 回到主程序,继续遍历网格,寻找下一个还没被淹没的
'1'。
为什么要改成 '0'?
这样可以起到“标记已访问”的作用,而且不需要额外的 visited 数组,节省了空间。
代码实现 (DFS)
from typing import List
class Solution:
def numIslands(self, grid: List[List[str]]) -> int:
# 获取网格的行数和列数
if not grid:
return 0
m, n = len(grid), len(grid[0])
count = 0
# 定义 DFS 辅助函数
def dfs(i, j):
# 1. 边界检查:如果坐标超出网格范围,直接返回
if i < 0 or i >= m or j < 0 or j >= n:
return
# 2. 内容检查:如果当前格子是水 ('0'),说明已经访问过或本来就是水,返回
if grid[i][j] == '0':
return
# 3. 标记当前格子为已访问 (将其变为 '0',即“淹没”陆地)
grid[i][j] = '0'
# 4. 递归访问上下左右四个方向
dfs(i - 1, j) # 上
dfs(i + 1, j) # 下
dfs(i, j - 1) # 左
dfs(i, j + 1) # 右
# 主循环:遍历网格中的每一个格子
for i in range(m):
for j in range(n):
# 如果发现一个未被访问的陆地 ('1')
if grid[i][j] == '1':
count += 1 # 岛屿数量加 1
dfs(i, j) # 启动 DFS,淹没整个岛屿
return count
复杂度分析
- 时间复杂度: \(O(M \times N)\)。我们需要遍历网格中的每个格子一次。
- 空间复杂度: \(O(M \times N)\)。最坏情况下(整个网格都是陆地),递归栈的深度可能达到 \(M \times N\)。
4. 解法二:广度优先搜索 (BFS)【⭐】
(个人感觉这个思路最直观)
思路讲解
BFS 和 DFS 的目标一样,都是找到所有相连的 '1' 并标记。区别在于遍历的顺序。
- DFS 是一条路走到黑(递归)。
- BFS 是像水波扩散一样,一层一层向外扩展(使用队列)。
步骤:
- 遍历网格,遇到
'1',计数 +1。 - 把这个
'1'的坐标放入队列queue,并立即将其标记为'0'。 - 当队列不为空时:
- 弹出一个坐标。
- 检查它的上下左右。
- 如果邻居是
'1',将其标记为'0'并加入队列。
- 队列空了,说明这个岛屿处理完了。
代码实现 (BFS)
from typing import List
from collections import deque
class SolutionBFS:
def numIslands(self, grid: List[List[str]]) -> int:
if not grid:
return 0
m, n = len(grid), len(grid[0])
count = 0
for i in range(m):
for j in range(n):
if grid[i][j] == '1':
count += 1
# 发现新岛屿,开始 BFS
self.bfs(grid, i, j, m, n)
return count
def bfs(self, grid: List[List[str]], i: int, j: int, m: int, n: int) -> None:
grid[i][j] = '0' # 标记为已访问
queue = deque([(i, j)]) # 初始化队列
directions = [(-1, 0), (1, 0), (0, -1), (0, 1)]
while queue:
r, c = queue.popleft() # 取出队首元素
# 检查四个方向
for dr, dc in directions:
nr, nc = r + dr, c + dc
# 如果邻居在网格内 且 是陆地
if 0 <= nr < m and 0 <= nc < n and grid[nr][nc] == '1':
grid[nr][nc] = '0' # 标记为已访问 (入队前标记,防止重复入队)
queue.append((nr, nc))
复杂度分析
- 时间复杂度: \(O(M \times N)\)。每个节点最多入队一次。
- 空间复杂度: \(O(\min(M, N))\)。队列中存储的节点数量在最坏情况下与网格的短边长度相关(实际上最坏也是 \(O(M \times N)\),但通常比 DFS 递归栈更稳定,不会栈溢出)。
5. 解法三:并查集 (Union-Find) - 进阶思路
思路讲解
这是一种不同的思维方式。
- 把二维网格拍平成一维。坐标
(i, j)可以映射为i * n + j。 - 初始化并查集:每个
'1'初始时都是一个独立的集合。 - 遍历网格:如果当前格子是
'1',检查它的右边和下边(避免重复检查)。如果邻居也是'1',就将它们所在的集合合并 (union)。 - 最后统计有多少个独立的集合(即根节点的数量)。
这种方法在动态连接问题(比如陆地会随时间增加)中很有用,但对于这道静态题,代码量稍大,效率略低于 DFS/BFS。
代码实现 (Union-Find)
代码实现要点:
- 对于并查集,需要维护一个 parent 数组,用来标记每个元素属于什么集合。相当于 parent[x] 是 x 的直接上级,每个人只知道自己的直接上级是谁。
- 并查集的工作原理:
- 基本操作 find:对于元素 x,寻找 parent[x] 是什么元素,直到 x == parent[x]。相当于,如果我们想两个人是否在同一个团体,就顺着上级一直找,看最终的老大是不是同一个人。
- 基本操作 union:合并元素 x y 的操作是,令 parent[find(x)] = find(y)。相当于,我们去找到两个人的老大,让其中一个老大认另一个老大当上级。
- 性能优化 路径压缩:if parent[i] != i: parent[i] = find(parent[i]),相当于让中途的人都被老大直接领导,让组织变扁平。
from typing import List
class SolutionUF:
def numIslands(self, grid: List[List[str]]) -> int:
if not grid:
return 0
m, n = len(grid), len(grid[0])
# parent 数组,parent[i] 表示节点 i 的父节点
# 大小为 m * n,初始时每个节点的父节点是自己
parent = list(range(m * n))
# 记录当前岛屿数量,初始假设每个 '1' 都是一个岛屿
count = 0
# 查找根节点 (带路径压缩优化)
def find(x):
if parent[x] != x:
parent[x] = find(parent[x])
return parent[x]
# 合并两个集合
def union(x, y):
rootX = find(x)
rootY = find(y)
if rootX != rootY:
parent[rootX] = rootY
return True # 成功合并,岛屿数减 1
return False # 已经在同一集合
# 遍历网格,初始化并查集并进行合并
for i in range(m):
for j in range(n):
if grid[i][j] == '1':
count += 1 # 先假设这是一个新岛屿
current_idx = i * n + j
# 只需要检查右边和下边,避免重复处理
# 检查下方
if i + 1 < m and grid[i + 1][j] == '1':
down_idx = (i + 1) * n + j
if union(current_idx, down_idx):
count -= 1 # 合并成功,说明这两个 '1' 属于同一个岛屿,总数减 1
# 检查右方
if j + 1 < n and grid[i][j + 1] == '1':
right_idx = i * n + (j + 1)
if union(current_idx, right_idx):
count -= 1
return count
6. 总结与对比
| 特性 | DFS (递归) | BFS (队列) | 并查集 (Union-Find) |
|---|---|---|---|
| 代码难度 | ⭐ (最简单) | ⭐⭐ | ⭐⭐⭐ |
| 空间消耗 | 递归栈 (可能较深) | 队列 (较稳定) | 额外 parent 数组 |
| 运行速度 | 快 | 快 | 稍慢 (常数大) |
| 适用场景 | 通用,面试首选 | 网格很大怕栈溢出时 | 动态连通性问题 |
建议:
在面试中,DFS 解法通常是首选,因为它写起来最快,逻辑最清晰。如果你担心网格太大导致递归深度溢出(Python 默认递归深度限制约为 1000),可以主动提出使用 BFS 作为优化方案。

浙公网安备 33010602011771号