1. 算法训练营中的岛屿问题解析
今天想和大家聊聊算法训练中经典的岛屿问题。这类题目在各大厂的笔试面试中出现的频率相当高,特别是像字节、腾讯这样的互联网公司。我最近在刷"代码随想录"的算法训练营时,正好做到第50天的两道岛屿题目:99.岛屿数量和100.岛屿最大面积。这两道题看似简单,但想要写出高效且正确的解法,还是有不少细节需要注意的。
岛屿问题本质上属于图的遍历问题,通常可以用DFS(深度优先搜索)或BFS(广度优先搜索)来解决。在实际面试中,面试官不仅会考察你能不能写出解法,更会关注你对算法选择的理解、代码的健壮性以及时间空间复杂度的分析。下面我就结合这两道题目,详细讲解一下解题思路和实际编码中的各种坑。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 岛屿数量问题(LeetCode 200)
2.1 问题描述与理解
给定一个由 '1'(陆地)和 '0'(水)组成的二维网格,计算网格中岛屿的数量。岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。
示例:
code复制输入:
[
['1','1','0','0','0'],
['1','1','0','0','0'],
['0','0','1','0','0'],
['0','0','0','1','1']
]
输出:3
2.2 DFS解法详解
DFS是解决这类连通性问题最直观的方法。基本思路是:遍历整个网格,当遇到一个'1'时,就从这个位置开始进行DFS,将所有相连的'1'都标记为已访问(通常可以直接修改为'0'),这样整个岛屿就被"淹没"了,同时岛屿计数加1。
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实现中,一定要先检查边界条件,再检查当前点是否是陆地,这个顺序不能反,否则可能导致数组越界。
2.3 BFS解法实现
BFS同样可以解决这个问题,使用队列来存储待访问的节点。相比DFS的递归实现,BFS的迭代实现通常更节省栈空间,特别适合处理大规模网格。
python复制from collections import deque
def numIslands(grid):
if not grid:
return 0
count = 0
rows, cols = len(grid), len(grid[0])
for r in range(rows):
for c in range(cols):
if grid[r][c] == '1':
count += 1
grid[r][c] = '0'
queue = deque([(r, c)])
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))
return count
2.4 复杂度分析与优化
两种方法的时间复杂度都是O(M×N),其中M和N分别是网格的行数和列数,因为每个网格最多被访问一次。空间复杂度方面:
- DFS:最坏情况下需要O(M×N)的栈空间(当整个网格都是陆地时)
- BFS:最坏情况下需要O(min(M,N))的队列空间
在实际应用中,如果网格非常大,DFS可能会导致栈溢出,这时BFS是更好的选择。另外,可以使用"原地修改"的技巧来避免使用额外的visited数组,直接修改原网格中的值为'0'表示已访问。
3. 岛屿最大面积问题(LeetCode 695)
3.1 问题描述
给定一个由0和1组成的非空二维数组,找到只包含1的最大岛屿面积。岛屿是由相邻的1组成的区域,相邻要求两个1必须在水平或者竖直方向上相邻。
示例:
code复制输入:
[
[0,0,1,0,0,0,0,1,0,0,0,0,0],
[0,0,0,0,0,0,0,1,1,1,0,0,0],
[0,1,1,0,1,0,0,0,0,0,0,0,0],
[0,1,0,0,1,1,0,0,1,0,1,0,0],
[0,1,0,0,1,1,0,0,1,1,1,0,0],
[0,0,0,0,0,0,0,0,0,0,1,0,0],
[0,0,0,0,0,0,0,1,1,1,0,0,0],
[0,0,0,0,0,0,0,1,1,0,0,0,0]
]
输出:6
3.2 DFS解法实现
与岛屿数量问题类似,但这次我们需要在遍历过程中记录每个岛屿的面积,并维护一个最大值。
python复制def maxAreaOfIsland(grid):
if not grid:
return 0
max_area = 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 0
grid[r][c] = 0 # 标记为已访问
return 1 + 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:
max_area = max(max_area, dfs(r, c))
return max_area
3.3 BFS解法实现
同样,我们也可以用BFS来计算每个岛屿的面积:
python复制from collections import deque
def maxAreaOfIsland(grid):
if not grid:
return 0
max_area = 0
rows, cols = len(grid), len(grid[0])
for r in range(rows):
for c in range(cols):
if grid[r][c] == 1:
grid[r][c] = 0
area = 1
queue = deque([(r, c)])
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
area += 1
queue.append((nr, nc))
max_area = max(max_area, area)
return max_area
3.4 性能对比与选择
在实际编码面试中,DFS的实现通常更简洁,递归写法也更符合直觉。但在处理大规模数据时,BFS的迭代实现通常更可靠,因为它不会出现栈溢出的问题。对于岛屿最大面积问题,还需要注意:
- 面积计算方式:DFS可以通过返回值累加,BFS需要在遍历过程中累加
- 最大值更新时机:每次完成一个岛屿的遍历后立即更新最大值
- 边界条件处理:与岛屿数量问题相同
4. 岛屿问题的变种与扩展
4.1 常见变种题目
岛屿问题有很多变种,掌握基本解法后可以尝试:
- 岛屿周长(LeetCode 463):计算岛屿的周长
- 封闭岛屿数量(LeetCode 1254):统计完全被水包围的岛屿数量
- 不同岛屿的数量(LeetCode 694):根据岛屿形状判断不同岛屿的数量
- 最大人工岛(LeetCode 827):可以通过将一格0变为1来创造的最大岛屿面积
4.2 并查集解法
除了DFS/BFS,岛屿问题还可以用并查集(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.rank = [0] * (rows * cols)
self.count = sum(grid[i][j] 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:
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':
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
并查集在动态连通性问题中表现优异,特别是当网格会动态变化时(如LeetCode 305)。
4.3 实际应用场景
岛屿问题不仅仅是算法题,在实际工程中也有很多应用:
- 图像处理中的连通区域分析
- 地图服务中的地块划分
- 游戏开发中的区域探测
- 社交网络中的群体发现
5. 面试中的常见问题与技巧
5.1 面试官常问的问题
在面试中,除了写出代码,面试官可能会问:
- 为什么选择DFS/BFS?各自的优缺点是什么?
- 如何优化空间复杂度?
- 如果网格非常大,无法全部放入内存怎么办?
- 如何并行化处理这个问题?
- 如何修改算法来处理8方向连通的情况?
5.2 代码实现中的常见错误
根据我的经验,同学们在实现时容易犯以下错误:
- 忘记处理空输入的情况
- 边界条件检查顺序错误(应该先检查是否越界,再检查是否是陆地)
- 在BFS实现中忘记标记已访问节点,导致重复访问和无限循环
- 在计算岛屿面积时,初始化错误(应该初始化为1而不是0)
- 使用额外的visited数组而忘记同步更新
5.3 调试技巧
当你的岛屿问题解法出现错误时,可以:
- 用小规模测试用例手动模拟算法执行过程
- 打印中间状态,特别是访问标记的变化
- 检查四个方向的遍历顺序是否正确
- 对于并查集解法,打印parent数组查看合并过程
我在最初练习时,经常因为方向数组定义不全或边界条件处理不当而出错。后来养成了先写边界检查的习惯,错误率大大降低。
