1. 问题背景与理解
"岛屿数量"是LeetCode上经典的图论问题(编号200),也是面试中的高频考题。我第一次遇到这个问题是在亚马逊的面试中,面试官在白板上画了一个由'1'和'0'组成的网格,要求我计算其中岛屿的数量。当时因为对深度优先搜索(DFS)的理解不够深入,导致解题过程磕磕绊绊。经过大量练习后,我发现这个问题是理解图遍历算法的绝佳切入点。
岛屿问题的标准定义是:给定一个由'1'(陆地)和'0'(水)组成的二维网格,计算其中岛屿的数量。岛屿由水平或垂直方向上相邻的陆地连接形成,假设网格四周都被水包围。例如:
code复制输入:
[
["1","1","0","0","0"],
["1","1","0","0","0"],
["0","0","1","0","0"],
["0","0","0","1","1"]
]
输出:3
这个问题看似简单,但涉及几个关键考察点:
- 二维数组的遍历方式
- 深度优先搜索(DFS)或广度优先搜索(BFS)的实现
- 避免重复计数的技巧(即如何标记已访问的节点)
- 边界条件的处理
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 深度优先搜索解法详解
2.1 基础DFS实现
DFS是解决岛屿问题的自然选择——当我们遇到一个'1'时,以此为起点向四周探索所有相连的陆地,就像墨水在纸上扩散一样。以下是Python实现的核心代码:
python复制def numIslands(grid):
if not grid:
return 0
count = 0
rows, cols = len(grid), len(grid[0])
for i in range(rows):
for j in range(cols):
if grid[i][j] == '1':
count += 1
dfs(grid, i, j)
return count
def dfs(grid, i, j):
if i < 0 or j < 0 or i >= len(grid) or j >= len(grid[0]) or grid[i][j] != '1':
return
grid[i][j] = '#' # 标记为已访问
dfs(grid, i+1, j)
dfs(grid, i-1, j)
dfs(grid, i, j+1)
dfs(grid, i, j-1)
关键点说明:
- 主函数中双重循环遍历每个单元格
- 遇到'1'时计数器加1,并启动DFS
- DFS递归访问上下左右四个方向
- 将访问过的'1'标记为'#'避免重复计数
注意:直接修改输入数组在某些场景下可能不被允许,面试时应与面试官确认。替代方案是使用单独的visited矩阵。
2.2 时间复杂度分析
假设网格大小为M×N:
- 每个节点最多被访问一次(被标记后不再处理)
- 每个节点访问时的操作是O(1)的
- 因此总时间复杂度为O(M×N)
空间复杂度主要来自递归调用栈:
- 最坏情况下(整个网格都是陆地),递归深度为M×N
- 因此空间复杂度为O(M×N)
3. 广度优先搜索解法对比
3.1 BFS实现方案
虽然DFS更简洁,但BFS可以避免递归导致的栈溢出风险,特别适合大规模网格。以下是BFS的实现:
python复制from collections import deque
def numIslands(grid):
if not grid:
return 0
count = 0
rows, cols = len(grid), len(grid[0])
for i in range(rows):
for j in range(cols):
if grid[i][j] == '1':
count += 1
queue = deque()
queue.append((i, j))
grid[i][j] = '#'
while queue:
x, y = queue.popleft()
for dx, dy in [(1,0), (-1,0), (0,1), (0,-1)]:
nx, ny = x + dx, y + dy
if 0 <= nx < rows and 0 <= ny < cols and grid[nx][ny] == '1':
grid[nx][ny] = '#'
queue.append((nx, ny))
return count
BFS与DFS的关键区别:
- 使用队列代替递归调用栈
- 每次从队列中取出一个位置,检查其四周
- 将未访问的相邻陆地加入队列
3.2 DFS与BFS的选择建议
| 特性 | DFS | BFS |
|---|---|---|
| 实现方式 | 递归或显式栈 | 队列 |
| 空间复杂度 | O(M×N)(递归深度) | O(min(M,N))(队列最大长度) |
| 适用场景 | 代码简洁,小规模网格 | 大规模网格,避免栈溢出 |
| 扩展性 | 适合统计岛屿面积等问题 | 适合找最短路径等问题 |
在实际面试中,建议先实现DFS版本,然后根据面试官要求讨论BFS实现。我曾遇到面试官要求解释为什么BFS的空间复杂度理论上更优,关键在于BFS的队列最大长度取决于网格的宽度而非面积。
4. 常见变种与扩展问题
4.1 统计岛屿的最大面积
这是岛屿问题的直接变种,LeetCode编号695。只需在DFS/BFS时增加一个计数器:
python复制def maxAreaOfIsland(grid):
max_area = 0
rows, cols = len(grid), len(grid[0])
def dfs(i, j):
if i < 0 or j < 0 or i >= rows or j >= cols or grid[i][j] != 1:
return 0
grid[i][j] = 0
return 1 + 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:
max_area = max(max_area, dfs(i, j))
return max_area
4.2 封闭岛屿数量
LeetCode1254要求统计完全被水包围的岛屿(边缘的岛屿不算)。解决思路:
- 先淹没所有边缘岛屿
- 再统计剩余岛屿数量
python复制def closedIsland(grid):
rows, cols = len(grid), len(grid[0])
def dfs(i, j):
if i < 0 or j < 0 or i >= rows or j >= cols:
return False
if grid[i][j] != 0:
return True
grid[i][j] = 1
return dfs(i+1,j) & dfs(i-1,j) & dfs(i,j+1) & dfs(i,j-1)
count = 0
for i in range(rows):
for j in range(cols):
if grid[i][j] == 0 and dfs(i, j):
count += 1
return count
4.3 不同岛屿形状
LeetCode694要求统计不同形状岛屿的数量。关键在于如何编码岛屿形状:
- 使用相对路径记录DFS遍历方向
- 将路径序列作为哈希键
python复制def numDistinctIslands(grid):
rows, cols = len(grid), len(grid[0])
shapes = set()
def dfs(i, j, direction):
if i < 0 or j < 0 or i >= rows or j >= cols or grid[i][j] != 1:
return ''
grid[i][j] = 0
path = direction
for dir, (di, dj) in zip('drul', [(1,0), (0,1), (-1,0), (0,-1)]):
path += dfs(i+di, j+dj, dir)
path += 'b' # 回溯标记
return path
for i in range(rows):
for j in range(cols):
if grid[i][j] == 1:
shapes.add(dfs(i, j, 's')) # 's'表示起点
return len(shapes)
5. 实战技巧与优化策略
5.1 并查集解法
对于超大规模网格或需要动态更新的场景,并查集(Union-Find)是更优选择。其核心思想是将相邻陆地合并到同一集合:
python复制class UnionFind:
def __init__(self, grid):
rows, cols = len(grid), len(grid[0])
self.parent = [i for i in range(rows * cols)]
self.count = sum(grid[i][j] == '1' for i in range(rows) for j in range(cols))
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:
self.parent[rooty] = rootx
self.count -= 1
def numIslands(grid):
if not grid:
return 0
rows, cols = len(grid), len(grid[0])
uf = UnionFind(grid)
for i in range(rows):
for j in range(cols):
if grid[i][j] == '1':
for di, dj in [(1,0), (0,1)]: # 只需检查右和下
ni, nj = i + di, j + dj
if ni < rows and nj < cols and grid[ni][nj] == '1':
uf.union(i * cols + j, ni * cols + nj)
return uf.count
并查集的优势在于:
- 时间复杂度接近O(M×N×α(M×N)),其中α是反阿克曼函数
- 特别适合动态添加/删除岛屿的场景
5.2 面试中的常见错误
根据我的面试经验,候选人常犯以下错误:
- 忘记处理空输入(grid = [])
- 访问数组越界(没有检查i和j的范围)
- 重复计数(没有正确标记已访问节点)
- 混淆行和列的顺序(尤其在不同语言中)
- 在BFS实现中忘记将节点标记为已访问就加入队列
一个典型的错误案例:
python复制# 错误示范:BFS中未立即标记已访问
while queue:
x, y = queue.popleft()
grid[x][y] = '#' # 这里标记太晚了!
for dx, dy in [(1,0), (-1,0), (0,1), (0,-1)]:
nx, ny = x + dx, y + dy
if 0 <= nx < rows and 0 <= ny < cols and grid[nx][ny] == '1':
queue.append((nx, ny)) # 可能导致同一节点多次入队
正确做法是在节点入队时立即标记:
python复制queue.append((nx, ny))
grid[nx][ny] = '#' # 立即标记
5.3 性能优化技巧
-
方向数组:使用方向数组使代码更整洁
python复制directions = [(-1,0), (1,0), (0,-1), (0,1)] for di, dj in directions: ni, nj = i + di, j + dj -
提前终止:对于统计岛屿数量问题,如果只需要判断是否有岛屿,可以在count=1时提前返回
-
并行处理:超大规模网格可考虑并行DFS/BFS,将网格分块处理
-
内存优化:对于只有0/1的网格,可以用位图压缩存储
我在处理一个2000×2000的网格时,通过以下优化将运行时间从3.2秒降到1.8秒:
- 将网格从list of lists转换为单一list
- 使用显式栈替代递归DFS
- 用数字0/1代替字符串'0'/'1'
