LC 200 岛屿数量:经典 DFS 入门题,我第一次写居然连方向都搞错了

不知道你们有没有这种感觉 ------ 有些算法题看着平平无奇,真动手写才发现全是小坑。岛屿数量就是这么一道。

当初我刚学完 DFS,信心满满点开这道题,觉得不就是遍历嘛,结果写了三遍才过。要么越界报错,要么数量算不对,说多了都是泪。今天就把这道面试高频题掰开揉碎了讲,DFS 和 BFS 两种写法都安排上,顺便说说我踩过的坑,帮你们少走点弯路。

题目说的啥

题意其实很简单。给你一个二维网格,'1' 代表陆地,'0' 代表水。只有上下左右紧挨着的陆地,才算同一块岛屿。网格四周默认全是水。求总共有多少座岛屿。

解法一:DFS 深度优先遍历

先说最容易想到的 DFS。

我当时的第一反应是:我一个个格子扫过去,碰到一块陆地,就说明发现了一座新岛,数量加一。但一座岛肯定不止一块陆地啊,相邻的都算同一块。总不能下次碰到相邻的陆地,又算一座新岛吧?

所以得想办法标记:这块陆地我已经算过了,别重复数。怎么标记最省事?直接把走过的陆地改成 '0' 不就行了?反正已经统计过了,下次碰到就当水处理,连额外的访问数组都不用开,省空间。

具体怎么写

写一个辅助的 dfs 函数,传当前的行号和列号。函数进来第一件事,先判断合不合法:坐标越界了?或者当前格子不是陆地?那直接返回,啥也不干。如果是合法的陆地,先把它改成 '0' ------ 标记为已访问。然后递归调用 dfs,分别去走它的上、下、左、右四个邻居。

说个丢人的事,我第一次写的时候,脑子一抽把斜着的四个方向也加上了,写了八个方向。结果样例 1 直接算出来不对,盯了半天才反应过来 ------ 题目明明白白说 "水平和竖直方向相邻",斜着不算。这个低级错误我记到现在,现在写网格题先看清楚相邻规则。

还有就是边界判断,一定要写在最前面,不然一递归就数组越界,这个坑我结结实实踩了两次。

代码实现

javascript 复制代码
var numIslands = function(grid) {
    let count = 0;
    const m = grid.length;
    const n = grid[0].length;

    // 深度优先遍历:把当前岛屿的所有陆地都淹没成0
    const dfs = (i, j) => {
        // 越界或者不是陆地,直接跑路
        if (i < 0 || i >= m || j < 0 || j >= n || grid[i][j] === '0') {
            return;
        }
        // 标记为已访问:直接改成水,省得开额外数组
        grid[i][j] = '0';
        // 上下左右四个方向挨个走
        dfs(i - 1, j); // 上
        dfs(i + 1, j); // 下
        dfs(i, j - 1); // 左
        dfs(i, j + 1); // 右
    }

    // 遍历整个网格的每个格子
    for (let i = 0; i < m; i++) {
        for (let j = 0; j < n; j++) {
            // 碰到未访问的陆地,说明发现新岛屿
            if (grid[i][j] === '1') {
                count++;
                dfs(i, j);
            }
        }
    }
    return count;
};

复杂度

时间复杂度是 O(m*n),每个格子最多被访问一次,不会重复走。空间最坏情况就是整个网格全是陆地,递归栈深度会到 m*n。

解法二:BFS 广度优先遍历

说完 DFS,再聊聊 BFS。思路其实和 DFS 一模一样,都是碰到陆地就计数、然后标记所有相连的陆地。区别只在于遍历的顺序。

DFS 是一条路走到黑,走不动了再回头;BFS 是一圈一圈往外扩散,像水波纹一样铺开。实现上用队列就行:碰到一块陆地,就把它放进队列。然后每次从队头拿出一个格子,标记成 0,再把它上下左右的陆地都放进队列。直到队列为空,这座岛就处理完了。

BFS 的好处是不会有递归栈溢出的问题。如果网格特别大,DFS 递归深度太深可能栈溢出,BFS 就没这个顾虑。

这里有个很容易踩的细节坑。一定要在入队的时候就把格子标记成 0,别等出队的时候再标记。我一开始就写反了,结果同一个格子会被好几个邻居重复加入队列,数据量大了直接超时。别问我怎么知道的,卡了我快十分钟。

代码实现

javascript 复制代码
var numIslands = function(grid) {
    let count = 0;
    const m = grid.length;
    const n = grid[0].length;
    // 方向数组:上下左右四个方向
    const dirs = [[-1,0], [1,0], [0,-1], [0,1]];

    for (let i = 0; i < m; i++) {
        for (let j = 0; j < n; j++) {
            if (grid[i][j] === '1') {
                count++;
                // 队列存格子坐标
                const queue = [[i, j]];
                grid[i][j] = '0'; // 入队就标记,避免重复入队
                
                while (queue.length) {
                    const [x, y] = queue.shift();
                    // 遍历四个方向的邻居
                    for (let [dx, dy] of dirs) {
                        const nx = x + dx;
                        const ny = y + dy;
                        // 边界合法 + 是陆地,就入队
                        if (nx >=0 && nx < m && ny >=0 && ny < n && grid[nx][ny] === '1') {
                            grid[nx][ny] = '0';
                            queue.push([nx, ny]);
                        }
                    }
                }
            }
        }
    }
    return count;
};

最后碎碎念

其实这道题就是网格类算法的入门基石,核心就是「洪水填充」的思想 ------ 碰到一个点,就把它相连的同类型区域全部处理掉。后面很多更难的网格题,比如岛屿最大面积、岛屿周长,本质都是在这个思路上做变形。

再帮你们避一遍我踩过的坑:

  1. 先看清楚相邻规则,是四方向还是八方向,别上来就写八个方向
  2. 边界判断永远放最前面,不然越界报错教你做人
  3. BFS 入队就标记,别等出队再改

我当初刷这题的时候,还纠结过能不能修改原数组。如果题目明确说不能改,那就得额外开一个 visited 数组记录访问状态,逻辑是一样的。

你第一次写这道题的时候踩过什么有意思的坑?或者你有更巧妙的写法?评论区聊聊呗,我每条都会看。如果觉得这篇对你有帮助,点个赞让更多朋友看到呀~

相关推荐
labixiong3 小时前
实现一个能跑的迷你版Promise(一)
前端·javascript·面试
weedsfly8 小时前
还在用 Axios?你可能需要重新理解 XHR 与 Fetch
前端·javascript·面试
CoderWeen8 小时前
从零实现一个 Vue3 流程图编辑器:节点拖拽、贝塞尔连线与框选
前端·javascript
To_OC18 小时前
LC 128 最长连续序列:别上来就排序,O (n) 解法才是这题的灵魂
javascript·算法·leetcode
kyriewen1 天前
我用 50 行代码重写了 React Router 核心,终于搞懂了前端路由原理
前端·javascript·react.js
Asize1 天前
HTML5 Canvas 基础:从按帧动画到 ECharts 数据可视化
前端·javascript·canvas
默_笙1 天前
🎄 后端给我一堆扁平数据,我 10 行代码把它变成了树
前端·javascript
前端Hardy1 天前
又一个 AI 神器火了!
前端·javascript·后端
PBitW1 天前
GPT训练我的第二天,我表示不过如此!!!😕😕😕
前端·javascript·面试