1. 岛屿数量问题解析与实战指南
作为算法面试中的经典题型,"岛屿数量"问题在LeetCode热题100中排名靠前,也是各大科技公司面试中的高频考点。这道题考察的是对二维矩阵的遍历技巧和深度优先搜索(DFS)/广度优先搜索(BFS)的应用能力。我第一次遇到这个问题是在一次模拟面试中,当时花了近40分钟才写出一个勉强能运行的解法,后来经过系统训练,现在能在5分钟内写出三种不同解法的完整代码。
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
1.2 核心解题思路
解决岛屿数量问题的关键在于理解"岛屿"的定义和如何有效地标记已访问的陆地。基本思路是遍历整个网格,当遇到'1'时,就启动DFS或BFS来标记所有相连的'1',同时将岛屿计数加1。
关键点:必须确保每个'1'只被处理一次,否则会导致重复计数和无限循环。常见的做法是直接修改原矩阵,将访问过的'1'改为'0',或者使用额外的visited矩阵记录访问状态。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 深度优先搜索(DFS)解法详解
2.1 递归实现DFS
递归是最直观的DFS实现方式。当我们在网格中遇到'1'时,递归地访问它的上下左右相邻单元格。
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, rows, cols)
return count
def dfs(grid, i, j, rows, cols):
if i < 0 or j < 0 or i >= rows or j >= cols or grid[i][j] != '1':
return
grid[i][j] = '0' # 标记为已访问
dfs(grid, i+1, j, rows, cols)
dfs(grid, i-1, j, rows, cols)
dfs(grid, i, j+1, rows, cols)
dfs(grid, i, j-1, rows, cols)
2.2 迭代实现DFS
对于大型网格,递归实现可能导致栈溢出。这时可以使用显式栈的迭代方式:
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
stack = [(i, j)]
grid[i][j] = '0'
while stack:
x, y = stack.pop()
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] = '0'
stack.append((nx, ny))
return count
3. 广度优先搜索(BFS)解法
3.1 标准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([(i, j)])
grid[i][j] = '0'
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] = '0'
queue.append((nx, ny))
return count
3.2 BFS与DFS的选择考量
在实际面试中,面试官可能会询问两种方法的区别:
- DFS:实现简单,代码简洁,但递归深度可能受限于栈大小
- BFS:使用队列,适合寻找最短路径,内存消耗取决于队列长度
- 性能比较:对于岛屿问题,两者时间复杂度相同(O(M×N)),实际性能差异不大
4. 并查集(Union-Find)解法
4.1 并查集基础
并查集是一种树型的数据结构,用于处理不相交集合的合并与查询问题。它支持两种操作:
- Find:查找元素所属集合
- Union:合并两个集合
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 i in range(rows):
for j in range(cols):
if grid[i][j] == '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
4.2 应用并查集解决问题
python复制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':
grid[i][j] = '0'
index = i * cols + j
if i > 0 and grid[i-1][j] == '1':
uf.union(index, (i-1)*cols + j)
if j > 0 and grid[i][j-1] == '1':
uf.union(index, i*cols + (j-1))
if i < rows-1 and grid[i+1][j] == '1':
uf.union(index, (i+1)*cols + j)
if j < cols-1 and grid[i][j+1] == '1':
uf.union(index, i*cols + (j+1))
return uf.count
5. 性能优化与边界处理
5.1 时间复杂度分析
所有解法的时间复杂度都是O(M×N),其中M和N分别是网格的行数和列数。因为我们需要访问每个单元格至少一次。
空间复杂度:
- DFS递归:O(M×N)最坏情况(网格全为陆地,递归深度为M×N)
- DFS迭代/BFS:O(min(M,N)),因为队列/栈中最多存储对角线上的元素
- 并查集:O(M×N)用于存储父节点和秩
5.2 特殊边界情况
在实际编码中需要考虑以下边界情况:
- 空输入([])
- 全为'0'的网格
- 全为'1'的网格
- 只有一行或一列的网格
- 极大网格(测试栈/队列深度)
5.3 实际编码技巧
- 方向数组:使用[(-1,0),(1,0),(0,-1),(0,1)]表示四个方向,避免重复代码
- 提前返回:在循环中检查边界条件,减少不必要的递归/入队
- 原地修改:直接修改输入矩阵而不是使用额外空间,但需确认是否允许
- 变量命名:使用rows, cols而不是m, n提高可读性
6. 常见错误与调试技巧
6.1 典型错误模式
- 无限递归:忘记标记已访问的节点,导致重复处理
- 边界检查顺序错误:应先检查索引是否越界,再访问数组元素
- 类型混淆:LeetCode输入中的'1'是字符而非数字1
- 计数错误:在错误的位置增加岛屿计数
6.2 调试方法
- 小规模测试:先用2x2、3x3网格测试基本功能
- 打印中间状态:在DFS/BFS过程中打印当前处理的坐标
- 可视化工具:使用LeetCode的可视化功能观察算法执行过程
- 边界测试:专门测试全0、全1、单行、单列等情况
调试心得:当遇到错误时,先手动模拟算法在小网格上的执行过程,往往能快速定位问题所在。我在最初实现时经常忘记标记已访问节点,导致无限循环,后来养成了在访问节点后立即标记的习惯。
7. 问题变种与扩展
7.1 相关LeetCode题目
- 岛屿的最大面积(695)
- 被围绕的区域(130)
- 统计封闭岛屿的数量(1254)
- 岛屿的周长(463)
- 不同岛屿的数量(694)
7.2 实际应用场景
- 图像处理中的连通区域分析
- 地图服务中的地块划分
- 游戏开发中的地形生成
- 社交网络中的群体检测
- 电路板中的连通性检查
7.3 高级优化技巧
- 并行计算:对于极大网格,可以将网格分块并行处理
- 增量计算:当网格动态变化时,只需重新计算受影响区域
- 位压缩:对于布尔型网格,可以用位运算压缩存储
- 多方法融合:结合DFS/BFS和并查集的优点处理特定场景
8. 面试实战建议
8.1 面试考察重点
- 能否正确理解问题并给出清晰定义
- 能否选择合适的数据结构和算法
- 代码实现的完整性和鲁棒性
- 时间/空间复杂度分析的准确性
- 对边界条件的考虑是否全面
8.2 回答策略
- 先澄清问题要求和输入输出格式
- 简要说明解题思路和算法选择理由
- 边写代码边解释关键步骤
- 完成后主动分析复杂度
- 提出可能的优化方向
8.3 代码模板准备
建议熟记DFS和BFS的代码模板,并能快速写出:
python复制# DFS模板
def dfs(matrix, i, j):
# 终止条件
if not (0 <= i < len(matrix)) or not (0 <= j < len(matrix[0])):
return
if matrix[i][j] != '1':
return
# 处理当前节点
matrix[i][j] = '0'
# 递归访问相邻节点
dfs(matrix, i+1, j)
dfs(matrix, i-1, j)
dfs(matrix, i, j+1)
dfs(matrix, i, j-1)
# BFS模板
from collections import deque
def bfs(matrix, i, j):
queue = deque([(i, j)])
matrix[i][j] = '0'
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(matrix) and 0 <= ny < len(matrix[0]) and matrix[nx][ny] == '1':
matrix[nx][ny] = '0'
queue.append((nx, ny))
经过多次面试实践,我发现掌握这几种解法的核心思想比死记硬背代码更重要。当遇到类似但稍有变化的问题时,能够灵活调整基础算法才是面试官最看重的能力。比如在解决"被围绕的区域"问题时,就需要从边界出发逆向思考,而不是简单地套用岛屿数量的解法。
