热题100 - 200. 岛屿数量

92 阅读4分钟

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);
        }
    }
}

实现要点

  1. 感染标记法:发现陆地后立即标记为'#',避免重复计数
  2. 递归边界:先判断坐标有效性,再处理当前节点
  3. 四方向递归:向上下左右四个方向深度搜索

复杂度分析

• 时间复杂度: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版本。经过分析,发现了两个关键问题:

  1. 无效节点入队:未在入队前检查坐标有效性,导致大量越界坐标进入队列
  2. 重复标记延迟:在出队时才标记节点,导致同一节点可能被多次入队

在DeepSeek帮助下突破瓶颈

通过与DeepSeek的讨论,我意识到BFS实现的关键优化点:

关键优化

  1. 入队前检查有效性
  2. 入队时立即标记
  3. 使用方向数组简化代码

优化后的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运行时间
原始DFSO(mn)O(mn)3ms
原始BFS~O(4mn)O(min(m,n))6ms
优化后BFSO(mn)O(min(m,n))4ms

关键优化点详解

  1. 方向数组:使用dirs数组统一管理四个搜索方向,代码更简洁且易于扩展
  2. 入队前检查:确保只有有效坐标才会进入队列
  3. 即时标记:在入队时立即标记节点,避免重复处理
  4. 正确使用队列:用poll()代替pop(),遵循FIFO原则

总结与思考

  1. DFS vs BFS:虽然DFS代码更简洁,但BFS在空间复杂度上更有优势,特别是对于大规模网格
  2. 标记时机:入队时立即标记是BFS优化的关键,这需要打破"处理时才标记"的直觉
  3. 性能分析:理论复杂度相同的情况下,实现细节对实际性能影响巨大

这个优化过程让我深刻体会到:算法不仅要正确,更要关注实现细节。感谢DeepSeek在关键优化点上的指导,帮助我突破了性能瓶颈。建议大家在实现算法时,多思考数据结构的操作细节,这往往是性能优化的关键所在。