LeetCode岛屿数量问题:从DFS到BFS的优化之路
本文记录了我在解决LeetCode 200题"岛屿数量"时的完整思考过程,包括DFS实现、BFS优化时踩的坑,以及最终在DeepSeek的帮助下突破性能瓶颈的全过程。保留所有错误尝试,因为这些思考过程本身就有学习价值。
问题描述
给定一个由'1'(陆地)和'0'(水)组成的二维网格,计算岛屿的数量。岛屿通过水平或垂直方向相邻陆地连接形成,且网格四周都被水包围。
示例:
输入:
[
["1","1","0","0","0"],
["1","1","0","0","0"],
["0","0","1","0","0"],
["0","0","0","1","1"]
]
输出:3
初版DFS解决方案
我首先想到的是经典的DFS解法,核心思路是:"感染"相邻陆地,确保每个岛屿只计数一次。
class Solution {
public int numIslands(char[][] grid) {
int total = 0;
for (int i = 0; i < grid.length; i++) {
for (int j = 0; j < grid[0].length; j++) {
if (grid[i][j] == '1') {
total++;
dfs(grid, i, j);
}
}
}
return total;
}
private void dfs(char[][] grid, int x, int y) {
if (x < 0 || y < 0 || x >= grid.length || y >= grid[0].length) return;
if (grid[x][y] == '1') {
grid[x][y] = '#'; // 标记为已访问
dfs(grid, x-1, y);
dfs(grid, x+1, y);
dfs(grid, x, y-1);
dfs(grid, x, y+1);
}
}
}
实现要点
- 感染标记法:发现陆地后立即标记为
'#',避免重复计数 - 递归边界:先判断坐标有效性,再处理当前节点
- 四方向递归:向上下左右四个方向深度搜索
复杂度分析
• 时间复杂度:O(mn) • 空间复杂度:O(mn)(最坏情况全为陆地时的递归栈深度)
BFS优化尝试
考虑到DFS可能存在的栈溢出风险,我决定尝试BFS实现。这是我的第一版BFS代码:
private void bfs(char[][] grid, int x, int y) {
Deque<int[]> queue = new LinkedList<>();
if (grid[x][y] == '1') {
queue.offer(new int[]{x, y});
}
while (!queue.isEmpty()) {
int[] pos = queue.pop();
int i = pos[0];
int j = pos[1];
if (pos[0] < 0 || pos[1] < 0 ||
pos[0] >= grid.length || pos[1] >= grid[0].length) {
continue;
} else if (grid[i][j] == '1') {
grid[i][j] = '#';
queue.offer(new int[]{i-1, j});
queue.offer(new int[]{i+1, j});
queue.offer(new int[]{i, j-1});
queue.offer(new int[]{i, j+1});
}
}
}
遇到的问题
虽然代码能正确运行,但在LeetCode提交时运行时间明显长于DFS版本。经过分析,发现了两个关键问题:
- 无效节点入队:未在入队前检查坐标有效性,导致大量越界坐标进入队列
- 重复标记延迟:在出队时才标记节点,导致同一节点可能被多次入队
在DeepSeek帮助下突破瓶颈
通过与DeepSeek的讨论,我意识到BFS实现的关键优化点:
关键优化
- 入队前检查有效性
- 入队时立即标记
- 使用方向数组简化代码
优化后的BFS实现:
private void bfs(char[][] grid, int x, int y) {
Deque<int[]> queue = new LinkedList<>();
queue.offer(new int[]{x, y});
grid[x][y] = '#'; // 立即标记
int[][] dirs = {{-1,0}, {1,0}, {0,-1}, {0,1}}; // 方向数组
while (!queue.isEmpty()) {
int[] pos = queue.poll();
for (int[] dir : dirs) {
int newX = pos[0] + dir[0];
int newY = pos[1] + dir[1];
// 入队前检查有效性
if (newX >= 0 && newX < grid.length &&
newY >= 0 && newY < grid[0].length &&
grid[newX][newY] == '1') {
grid[newX][newY] = '#'; // 立即标记
queue.offer(new int[]{newX, newY});
}
}
}
}
性能对比
| 版本 | 时间复杂度 | 空间复杂度 | LeetCode运行时间 |
|---|---|---|---|
| 原始DFS | O(mn) | O(mn) | 3ms |
| 原始BFS | ~O(4mn) | O(min(m,n)) | 6ms |
| 优化后BFS | O(mn) | O(min(m,n)) | 4ms |
关键优化点详解
- 方向数组:使用
dirs数组统一管理四个搜索方向,代码更简洁且易于扩展 - 入队前检查:确保只有有效坐标才会进入队列
- 即时标记:在入队时立即标记节点,避免重复处理
- 正确使用队列:用
poll()代替pop(),遵循FIFO原则
总结与思考
- DFS vs BFS:虽然DFS代码更简洁,但BFS在空间复杂度上更有优势,特别是对于大规模网格
- 标记时机:入队时立即标记是BFS优化的关键,这需要打破"处理时才标记"的直觉
- 性能分析:理论复杂度相同的情况下,实现细节对实际性能影响巨大
这个优化过程让我深刻体会到:算法不仅要正确,更要关注实现细节。感谢DeepSeek在关键优化点上的指导,帮助我突破了性能瓶颈。建议大家在实现算法时,多思考数据结构的操作细节,这往往是性能优化的关键所在。