1. 力扣岛屿数量问题解析
这道题在力扣(LeetCode)上编号为200题,是经典的图论和深度优先搜索(DFS)应用场景。题目要求计算二维网格中岛屿的数量,其中'1'代表陆地,'0'代表水,岛屿由水平或垂直方向相邻的陆地组成。
我第一次做这道题时,花了整整两小时才通过所有测试用例。后来在面试中多次遇到它的变种,才发现掌握它的核心解法有多么重要。这道题不仅能考察基础算法能力,还能延伸到实际应用场景,比如图像处理中的连通区域分析。
1.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
注意:网格的四个边界都被水包围,这是题目给定的前提条件。实际面试中可能需要确认这个假设是否成立。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心解法与思路拆解
2.1 深度优先搜索(DFS)解法
DFS是最直观的解法,时间复杂度O(M×N),空间复杂度最坏情况下也是O(M×N)(当网格全是陆地时递归深度)。
python复制def numIslands(grid):
if not grid:
return 0
count = 0
for i in range(len(grid)):
for j in range(len(grid[0])):
if grid[i][j] == '1':
dfs(grid, i, j)
count += 1
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] = '0' # 标记为已访问
dfs(grid, i+1, j)
dfs(grid, i-1, j)
dfs(grid, i, j+1)
dfs(grid, i, j-1)
关键点解析:
- 双重循环遍历每个单元格
- 遇到'1'时启动DFS
- DFS过程中将访问过的'1'改为'0'(避免重复计数)
- 每次DFS完成意味着处理完一个完整岛屿
实际编码时最容易犯的错误是忘记处理网格为空的情况,这会导致后续的len(grid[0])报错。
2.2 广度优先搜索(BFS)解法
BFS使用队列实现,时空复杂度与DFS相同,但更适合大规模数据(递归深度不会过大)。
python复制from collections import deque
def numIslands(grid):
if not grid:
return 0
count = 0
queue = deque()
for i in range(len(grid)):
for j in range(len(grid[0])):
if grid[i][j] == '1':
grid[i][j] = '0'
queue.append((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<len(grid) and 0<=ny<len(grid[0]) and grid[nx][ny] == '1':
grid[nx][ny] = '0'
queue.append((nx, ny))
count += 1
return count
BFS与DFS的选择考量:
- 数据规模小时两者差异不大
- 网格非常大时BFS更稳定(避免递归栈溢出)
- 面试时可以先实现DFS,再讨论BFS优化
3. 高级优化与变种问题
3.1 并查集(Union-Find)解法
并查集特别适合动态连通性问题,虽然代码稍复杂但时间复杂度更优(近似O(M×N))。
python复制class UnionFind:
def __init__(self, grid):
m, n = len(grid), len(grid[0])
self.count = 0
self.parent = [0] * (m * n)
self.rank = [0] * (m * n)
for i in range(m):
for j in range(n):
if grid[i][j] == '1':
self.parent[i * n + j] = i * n + j
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
uf = UnionFind(grid)
directions = [(1,0), (-1,0), (0,1), (0,-1)]
m, n = len(grid), len(grid[0])
for i in range(m):
for j in range(n):
if grid[i][j] == '1':
for d in directions:
ni, nj = i + d[0], j + d[1]
if 0 <= ni < m and 0 <= nj < n and grid[ni][nj] == '1':
uf.union(i * n + j, ni * n + nj)
return uf.count
并查集适用场景:
- 网格会动态变化(实时添加/删除岛屿)
- 需要频繁查询岛屿数量
- 超大规模网格处理
3.2 常见变种问题
-
岛屿的最大面积(力扣695题)
- 解法:在DFS/BFS时记录访问的单元格数量
-
封闭岛屿数量(力扣1254题)
- 边界上的岛屿不算封闭岛屿
- 技巧:先处理边界岛屿,再计算内部
-
不同岛屿的数量(力扣694题)
- 需要记录岛屿的形状特征
- 解法:使用DFS序列化岛屿形状
4. 面试实战技巧与避坑指南
4.1 面试常见考察点
-
代码健壮性检查
- 空输入处理
- 非矩形网格处理
- 非法字符处理
-
复杂度分析
- 要能准确分析时间/空间复杂度
- 解释为什么是O(M×N)
-
变种问题思路
- 面试官常会基于原题扩展
- 提前准备常见变种的解法
4.2 高频失误点
-
忘记标记已访问节点
- 导致无限循环或重复计数
- 解决方案:修改原矩阵或使用额外visited矩阵
-
边界条件处理不当
- 网格索引越界
- 特殊测试用例:全0、全1、单行、单列
-
方向数组定义错误
- 漏掉某些方向
- 错误的方向偏移量
4.3 性能优化技巧
-
原地修改矩阵
- 将'1'改为'0'比维护visited矩阵更节省空间
- 注意:会破坏原始输入数据
-
方向数组的四种写法
python复制# 写法1:显式列出 directions = [(1,0), (-1,0), (0,1), (0,-1)] # 写法2:使用product from itertools import product directions = list(product((-1,0,1), repeat=2)) directions.remove((0,0)) # 去除(0,0) # 选择依据:代码可读性 vs 简洁性 -
提前终止条件
- 当剩余未访问的'1'数量小于当前最大岛屿面积时
5. 实际应用场景延伸
这道算法题看似简单,但在实际工程中有广泛应用:
-
图像处理
- 连通区域分析
- 图像分割预处理
-
游戏开发
- 地图区域划分
- 可通行区域计算
-
社交网络分析
- 群体检测
- 关联用户发现
我在实际项目中曾用类似算法处理过卫星图像中的云团检测,发现优化后的并查集实现比OpenCV的连通组件分析在某些场景下快2-3倍。关键是要根据数据特点选择合适算法——当岛屿形状非常复杂时,DFS/BFS可能更合适;而当需要实时更新时,并查集优势明显。
