1. 问题背景与理解
岛屿数量问题是Leetcode上经典的图论题目,编号200,难度为中等。这道题在技术面试中出现的频率相当高,尤其是考察候选人对深度优先搜索(DFS)和广度优先搜索(BFS)这两种基础算法的掌握程度。
题目描述很简单:给定一个由'1'(陆地)和'0'(水)组成的二维网格,计算网格中岛屿的数量。岛屿被定义为被水包围的、通过水平或垂直方向相邻的陆地连接形成的区域。你可以假设网格的四个边都被水包围。
举个例子:
code复制输入:
[
['1','1','0','0','0'],
['1','1','0','0','0'],
['0','0','1','0','0'],
['0','0','0','1','1']
]
输出:3
这个题目看似简单,但实际解决时需要处理几个关键点:
- 如何高效地遍历整个网格
- 如何标记已经访问过的陆地,避免重复计数
- 如何处理边界条件(网格为空或只有一行/一列)
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. DFS解法详解
深度优先搜索是解决这类连通性问题最直观的方法。基本思路是:当我们遇到一个'1'时,就从这个点开始进行DFS,把所有相连的'1'都标记为已访问,这样它们就不会被重复计数了。
2.1 基础DFS实现
python复制def numIslands(grid):
if not grid:
return 0
count = 0
rows, cols = len(grid), len(grid[0])
def dfs(r, c):
if r < 0 or c < 0 or r >= rows or c >= cols or grid[r][c] != '1':
return
grid[r][c] = '0' # 标记为已访问
dfs(r+1, c)
dfs(r-1, c)
dfs(r, c+1)
dfs(r, c-1)
for r in range(rows):
for c in range(cols):
if grid[r][c] == '1':
count += 1
dfs(r, c)
return count
这个实现有几个关键点:
- 使用递归实现DFS,代码简洁但可能有栈溢出风险
- 直接在原数组上修改,将访问过的'1'改为'0',节省了额外空间
- 四个方向的搜索顺序不影响结果,但可能影响性能
2.2 DFS的优化与注意事项
在实际编码和面试中,有几个细节需要注意:
- 边界检查:必须放在DFS函数的最前面,否则可能导致数组越界
- 访问标记:必须在递归调用前完成,避免重复访问
- 递归深度:对于非常大的网格,递归可能导致栈溢出,这时需要考虑迭代实现
提示:在面试中,即使你写出了递归版本,也可以主动提到对于大规模数据可能的栈溢出问题,并说明可以用栈来模拟递归过程。这展示了你的全面思考。
3. BFS解法详解
广度优先搜索是另一种解决此问题的有效方法。与DFS不同,BFS使用队列来逐层扩展搜索,更适合寻找最短路径等问题,但在这里同样适用。
3.1 基础BFS实现
python复制from collections import deque
def numIslands(grid):
if not grid:
return 0
count = 0
rows, cols = len(grid), len(grid[0])
def bfs(r, c):
queue = deque()
queue.append((r, c))
grid[r][c] = '0'
while queue:
row, col = queue.popleft()
for dr, dc in [(1,0), (-1,0), (0,1), (0,-1)]:
nr, nc = row + dr, col + dc
if 0 <= nr < rows and 0 <= nc < cols and grid[nr][nc] == '1':
grid[nr][nc] = '0'
queue.append((nr, nc))
for r in range(rows):
for c in range(cols):
if grid[r][c] == '1':
count += 1
bfs(r, c)
return count
BFS实现的特点:
- 使用队列来存储待访问的节点
- 访问顺序是"先发现的先访问"
- 空间复杂度在最坏情况下可能高于DFS
3.2 BFS与DFS的选择
在实际应用中,选择BFS还是DFS取决于具体场景:
| 比较维度 | DFS | BFS |
|---|---|---|
| 实现方式 | 递归/栈 | 队列 |
| 空间复杂度 | O(mn)最坏 | O(min(m,n))平均 |
| 适用场景 | 简单实现 | 避免递归深度问题 |
| 扩展性 | 适合连通性问题 | 适合最短路径问题 |
在岛屿数量问题中,两者时间复杂度相同(都是O(mn)),但BFS的空间复杂度在最坏情况下可能更好。
4. 并查集解法
除了DFS和BFS,并查集(Union-Find)也是解决此类连通性问题的有力工具。虽然在这个特定问题中可能不是最优解,但了解这种解法有助于应对更复杂的变种题目。
4.1 并查集基础实现
python复制class UnionFind:
def __init__(self, grid):
rows, cols = len(grid), len(grid[0])
self.count = 0
self.parent = [i for i in range(rows * cols)]
self.rank = [0] * (rows * cols)
for r in range(rows):
for c in range(cols):
if grid[r][c] == '1':
self.count += 1
def find(self, i):
if self.parent[i] != i:
self.parent[i] = self.find(self.parent[i])
return self.parent[i]
def union(self, x, y):
rootx = self.find(x)
rooty = self.find(y)
if rootx != rooty:
if self.rank[rootx] > self.rank[rooty]:
self.parent[rooty] = rootx
elif self.rank[rootx] < self.rank[rooty]:
self.parent[rootx] = rooty
else:
self.parent[rooty] = rootx
self.rank[rootx] += 1
self.count -= 1
def numIslands(grid):
if not grid:
return 0
rows, cols = len(grid), len(grid[0])
uf = UnionFind(grid)
for r in range(rows):
for c in range(cols):
if grid[r][c] == '1':
grid[r][c] = '0'
for dr, dc in [(1,0), (-1,0), (0,1), (0,-1)]:
nr, nc = r + dr, c + dc
if 0 <= nr < rows and 0 <= nc < cols and grid[nr][nc] == '1':
uf.union(r * cols + c, nr * cols + nc)
return uf.count
并查集解法的特点:
- 需要额外的类来实现并查集数据结构
- 路径压缩和按秩合并优化了性能
- 时间复杂度接近O(mnα(mn)),其中α是反阿克曼函数
4.2 并查集的适用场景
并查集特别适合以下场景:
- 需要动态处理连通性问题(边可能动态添加)
- 问题规模非常大,需要最优时间复杂度
- 需要统计连通分量数量
在静态的岛屿数量问题中,并查集可能不如DFS/BFS简洁,但在一些变种问题(如动态岛屿数量)中非常有用。
5. 常见错误与边界情况
在实现岛屿数量问题时,有几个常见的陷阱需要注意:
5.1 访问顺序问题
一个常见错误是在DFS/BFS中没有正确处理访问顺序,导致:
- 重复访问同一个节点
- 漏掉某些连接部分
- 栈溢出或队列无限增长
解决方法:
- 确保在将节点加入队列/栈前就标记为已访问
- 使用一致的访问顺序(如上下左右)
5.2 网格表示问题
不同语言的网格表示方式可能不同:
- Python中可能是列表的列表
- Java/C++中可能是二维数组
- 需要正确处理行列索引
5.3 特殊边界情况
需要特别处理的边界情况包括:
- 空网格([])
- 只有一行或一列的网格
- 全'0'或全'1'的网格
- 非常大的网格(需要考虑算法选择)
测试用例示例:
python复制def test_numIslands():
# 空网格
assert numIslands([]) == 0
# 全水
assert numIslands([['0','0'],['0','0']]) == 0
# 全陆地
assert numIslands([['1','1'],['1','1']]) == 1
# 单行
assert numIslands([['1','0','1','1','0','1']]) == 2
# 单列
assert numIslands([['1'],['0'],['1']]) == 2
# 常规情况
grid = [
['1','1','0','0','0'],
['1','1','0','0','0'],
['0','0','1','0','0'],
['0','0','0','1','1']
]
assert numIslands(grid) == 3
6. 性能优化与进阶思考
对于大规模网格或性能敏感的场景,可以考虑以下优化:
6.1 并行处理
岛屿数量问题本质上是寻找连通分量,可以并行处理:
- 将网格分割为多个区域
- 在各区域内独立计算
- 合并区域边界的结果
6.2 增量计算
如果需要频繁查询岛屿数量(网格会动态变化),可以:
- 维护一个并查集结构
- 在每次修改后只更新受影响的部分
- 保持岛屿数量的实时统计
6.3 空间优化
如果原始网格不能修改,可以使用:
- 额外的访问标记数组
- 位压缩技术减少空间使用
- 按需加载网格部分(对于极大网格)
在实际面试中,即使面试官没有明确要求,讨论这些优化方向也能展示你的深度思考能力。
7. 题目变种与扩展
岛屿数量问题有很多有趣的变种,掌握基础解法后可以尝试:
7.1 统计岛屿周长
给定一个网格,计算所有岛屿的周长总和。关键在于识别水陆边界。
7.2 最大岛屿面积
不仅要统计岛屿数量,还要找出最大的岛屿面积。可以在DFS/BFS时累加面积。
7.3 封闭岛屿数量
只统计完全被水包围的岛屿(不接触网格边缘)。需要额外判断岛屿是否接触边界。
7.4 动态岛屿问题
网格会随时间变化(陆地变水或水变陆地),需要高效维护岛屿数量。这需要结合并查集和时间序列分析。
7.5 三维岛屿问题
将问题扩展到三维空间,陆地可能在上下左右前后六个方向连接。算法逻辑类似但实现更复杂。
解决这些变种问题的关键在于灵活调整基础算法,通常只需要修改DFS/BFS的某些细节或添加额外的判断条件。
