1. 力扣岛屿数量问题解析
今天想和大家分享一道经典的力扣算法题——"岛屿数量"(LeetCode 200)。这道题在各大科技公司的面试中出现频率极高,也是检验递归和深度优先搜索(DFS)基本功的绝佳例题。我第一次遇到这个问题是在亚马逊的面试中,当时因为边界条件没处理好而翻车,后来经过反复练习才真正掌握其中的精髓。
岛屿数量问题描述很简单:给定一个由'1'(陆地)和'0'(水)组成的二维网格,计算其中岛屿的数量。岛屿被定义为被水包围的、通过相邻陆地水平或垂直连接形成的区域。这个问题看似简单,但蕴含着DFS/BFS算法的核心思想,也是很多更复杂网格问题的基础。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 问题分析与解法思路
2.1 问题建模与理解
首先我们需要明确几个关键点:
- 网格中的每个格子要么是'1'(陆地),要么是'0'(水)
- 相邻指的是上下左右四个方向,对角线不算
- 一个岛屿是由相邻的'1'组成的连通区域
- 网格边界外都是水
举个例子:
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)
- 广度优先搜索(BFS)
- 并查集(Union-Find)
对于面试场景,DFS是最常被考察的实现方式,因为它代码简洁,能很好地展示递归思维。BFS则需要使用队列,适合需要最短路径的场景。并查集更适合动态连通性问题。
3. DFS解法详细实现
3.1 基础DFS实现
DFS的核心思想是:当遇到一个'1'时,就以此为起点向四周探索,把所有连通的'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)
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)
3.2 关键点解析
- 边界检查:递归时首先要检查是否越界
- 访问标记:将访问过的'1'改为'0',避免重复计数
- 四个方向的递归调用:上下左右都要探索
- 主函数中的双层循环:遍历整个网格寻找未访问的'1'
注意:在Python中,修改grid是可行的,因为列表是可变的。在某些语言中可能需要额外维护一个visited数组。
4. 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] = '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
5. 复杂度分析与优化
5.1 时间复杂度
两种解法的时间复杂度都是O(M×N),其中M和N分别是网格的行数和列数。因为最坏情况下我们需要访问每个格子一次。
5.2 空间复杂度
DFS的空间复杂度取决于递归的深度,最坏情况下是O(M×N)(当网格全是陆地时)。BFS的空间复杂度取决于队列的大小,同样最坏是O(min(M,N))。
5.3 实际优化技巧
- 对于特别大的网格,BFS通常更安全,避免栈溢出
- 可以修改原网格来节省visited数组的空间
- 并行处理:将网格分块,分别计算后再合并结果
6. 常见错误与调试技巧
6.1 典型错误案例
- 忘记边界检查导致数组越界
- 没有正确标记已访问节点导致无限循环
- 对角线也被当作相邻(题目要求仅上下左右)
- 输入为空网格时没有处理
6.2 调试建议
- 打印网格状态:在每次递归/入队前后打印网格
- 使用小测试用例:2x2或3x3的网格更容易发现问题
- 单步调试:在IDE中设置断点跟踪程序流程
7. 问题变种与扩展
掌握了基础解法后,可以尝试这些变种问题:
- 统计岛屿的最大面积(LeetCode 695)
- 岛屿的周长(LeetCode 463)
- 封闭岛屿数量(LeetCode 1254)
- 不同岛屿的数量(通过序列化形状来判断)
8. 面试实战建议
在面试中遇到这个问题时:
- 先明确问题要求,确认相邻的定义和边界条件
- 从暴力解法开始,逐步优化
- 解释清楚算法的时间和空间复杂度
- 考虑可能的优化和变种问题
- 写出整洁的代码,注意变量命名和缩进
我在多次面试中总结出一个技巧:先写出DFS解法,然后主动提到"对于特别大的网格,我们也可以使用BFS来避免栈溢出",这能展示你考虑问题的全面性。
