算法困惑问答 · LC 200 岛屿数量
岛屿数量为什么要把数过的岛「沉掉」?
直接答案
「沉岛」(把整块相连的 1 全改成 0,或打上 visited 标记)是为了让一座岛只被计数一次。外层双重循环逐格扫描,每碰到一个 1 就答案 +1,然后立刻 DFS 把这块陆地上下左右相连的所有 1 全部抹掉——之后扫描再遇到的 1,必然属于还没数过的新岛。不沉岛的话,同一座岛的每个格子都会触发一次计数,答案直接爆炸。
计数的单位是「连通块」,不是格子
这题本质是数连通块:一座岛 = 一片上下左右相连的 1。外层扫描负责「发现新岛」,DFS 负责「圈定整座岛的边界并注销它」。发现与注销配对,每个连通块恰好贡献一次计数——这是所有连通块类题目的通用骨架,洪水填充(LC733)、省份数量(LC547)都是它的变体。
DFS 本身很朴素:越界或遇到 0/海水就返回,否则把当前格改成 0,再向四个方向递归。注意题目定义是四连通,斜对角相邻不算同一座岛,八个方向都递归就数错了。
不想修改输入、怕爆栈,各有替代方案
沉岛直接改写了输入网格。工程上(或题目要求保留输入时)用一个等大的 visited 布尔数组,判断条件从「是 1」改成「是 1 且未访问」,代价是 O(R×C) 额外空间,正确性完全等价。
另一个隐患是递归深度:网格极大且整片都是陆地时,DFS 递归可能深达几十万层而爆栈。把递归换成显式队列就是 BFS 版本——发现新岛后把起点入队,循环弹出、把四邻的未访问陆地标记并入队,效果一样且不吃调用栈。
代码关键行(Python)
def numIslands(grid):
def sink(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' # 沉掉当前格
sink(i+1, j); sink(i-1, j); sink(i, j+1); sink(i, j-1)
count = 0
for i in range(len(grid)):
for j in range(len(grid[0])):
if grid[i][j] == '1':
count += 1 # 发现新岛
sink(i, j) # 立刻注销整座岛
return count`count += 1` 和 `sink(i, j)` 必须成对:先计数、马上注销。边界判断放在递归入口处(先判越界再取值),漏判会数组越界——Python 里负下标还会静默绕回,错得更隐蔽。
常见追问
要求最大岛屿面积怎么改?
让 sink 返回它抹掉的格子数(1 + 四个方向递归之和),外层对每座岛取最大值,就是 LC695 岛屿的最大面积。骨架不动,只是把「注销」升级成「注销并清点」。
斜对角相连算一座岛吗?
本题不算,只认上下左右四连通。有些变体题定义八连通,读题时先确认方向数组是 4 个还是 8 个,这是网格题第一件要核对的事。
把这道题彻底吃透
LC 200「岛屿数量」有逐步交互动画和完整图文题解,配着本页的结论看,一遍就通。