不知道你们有没有这种感觉 ------ 有些算法题看着平平无奇,真动手写才发现全是小坑。岛屿数量就是这么一道。
当初我刚学完 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;
};
最后碎碎念
其实这道题就是网格类算法的入门基石,核心就是「洪水填充」的思想 ------ 碰到一个点,就把它相连的同类型区域全部处理掉。后面很多更难的网格题,比如岛屿最大面积、岛屿周长,本质都是在这个思路上做变形。
再帮你们避一遍我踩过的坑:
- 先看清楚相邻规则,是四方向还是八方向,别上来就写八个方向
- 边界判断永远放最前面,不然越界报错教你做人
- BFS 入队就标记,别等出队再改
我当初刷这题的时候,还纠结过能不能修改原数组。如果题目明确说不能改,那就得额外开一个 visited 数组记录访问状态,逻辑是一样的。
你第一次写这道题的时候踩过什么有意思的坑?或者你有更巧妙的写法?评论区聊聊呗,我每条都会看。如果觉得这篇对你有帮助,点个赞让更多朋友看到呀~