200. 岛屿数量 (Medium)

专题归类: 07-图论 LeetCode 链接: https://leetcode.cn/problems/number-of-islands/


在线做题: 写 Python 代码并运行 · Java 跳转 LeetCode

题目描述

给你一个由 '1'(陆地)和 '0'(水)组成的二维网格,请你计算网格中岛屿的数量。

岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。

此外,你可以假设该网格的四条边均被水包围。

示例 1:

输入:grid = [
  ["1","1","1","1","0"],
  ["1","1","0","1","0"],
  ["1","1","0","0","0"],
  ["0","0","0","0","0"]
]
输出:1

示例 2:

输入:grid = [
  ["1","1","0","0","0"],
  ["1","1","0","0","0"],
  ["0","0","1","0","0"],
  ["0","0","0","1","1"]
]
输出:3

题目详细分析

  • 数据范围: m grid.length, n grid[i].length,1 ≤ m, n ≤ 300。网格最大可达 300×300 = 90,000 个格子。
  • 输入输出特征: 只有 '1''0' 两种字符,不区分大小写。岛屿由上下左右四方向相邻的 '1' 组成(对角线不算)。
  • 边界条件: 空网格不会出现(至少 1×1)。网格四边之外都是水,不需要额外处理边界外的逻辑,但代码必须做边界检查。
  • 核心约束: 不允许原地修改网格?不,题目没说不能修改,因此可以用「沉岛法」原地标记,将访问过的 '1' 改为 '0' 来节省 visited 数组。
  • 隐藏条件: 如果网格全部是 '0',返回 0。如果全部是 '1',返回 1。

小白版直白理解

想象你坐在直升机上俯瞰一片海洋,海面上有很多小岛。海水用 0 表示,陆地用 1 表示。你要数清楚有多少块独立的陆地。

你怎么数?一块一块来:看到一块没去过的陆地,就跳上去,然后把这块陆地以及所有和它连着的陆地都走一遍(标记为”已去过”),计数加 1。接着继续飞,找下一块没去过的陆地,重复操作。最后数出来的数字就是岛屿数量。

这里的「走一遍」就是 DFS(一条路走到黑,走完四周)或 BFS(一层一层往外扩散)。


解题思路

思路一:DFS 沉岛法(推荐)

核心思想: 遍历每个格子,遇到 '1' 就启动 DFS,把当前格子以及所有与其相连的 '1' 都标记为 '0'(沉岛),然后岛屿计数加 1。这样每个格子最多被访问一次。

为什么 DFS 可行?因为岛屿的形状不管多复杂,只要四方向相连都属于同一岛屿。DFS 可以沿着一条分支深入到底,再回溯探索其他分支,恰好覆盖整块相连区域。

def numIslands(grid):
    if not grid:
        return 0
    m, n = len(grid), len(grid[0])
 
    def dfs(i, j):
        # 超出边界 或 当前是水/已访问
        if i < 0 or i >= m or j < 0 or j >= n or grid[i][j] != '1':
            return
        grid[i][j] = '0'              # 沉岛:标记为已访问
        dfs(i + 1, j)                 # 下
        dfs(i - 1, j)                 # 上
        dfs(i, j + 1)                 # 右
        dfs(i, j - 1)                 # 左
 
    count = 0
    for i in range(m):
        for j in range(n):
            if grid[i][j] == '1':     # 发现新岛屿
                count += 1
                dfs(i, j)             # 沉掉整座岛
    return count

复杂度: 时间 O(m×n),空间 O(m×n)(最坏递归深度)

思路二:BFS 沉岛法

用队列代替递归,避免递归栈溢出。同样遍历网格,遇到 '1' 就 BFS 扩散沉岛。

from collections import deque
 
def numIslands(grid):
    if not grid:
        return 0
    m, n = len(grid), len(grid[0])
    count = 0
 
    for i in range(m):
        for j in range(n):
            if grid[i][j] == '1':
                count += 1
                q = deque([(i, j)])
                grid[i][j] = '0'      # 入队即标记
                while q:
                    x, y = q.popleft()
                    for dx, dy in [(1,0), (-1,0), (0,1), (0,-1)]:
                        nx, ny = x + dx, y + dy
                        if 0 <= nx < m and 0 <= ny < n and grid[nx][ny] == '1':
                            grid[nx][ny] = '0'
                            q.append((nx, ny))
    return count

复杂度: 时间 O(m×n),空间 O(min(m,n))(队列最坏宽度)

思路三:并查集(Union-Find)

将每个 '1' 视为一个节点,相邻的 '1' 合并到同一集合。最后统计有多少个集合根节点是 '1'

class UnionFind:
    def __init__(self, n):
        self.parent = list(range(n))
        self.rank = [0] * n
 
    def find(self, x):
        if self.parent[x] != x:
            self.parent[x] = self.find(self.parent[x])
        return self.parent[x]
 
    def union(self, x, y):
        rx, ry = self.find(x), self.find(y)
        if rx == ry:
            return
        if self.rank[rx] < self.rank[ry]:
            self.parent[rx] = ry
        elif self.rank[rx] > self.rank[ry]:
            self.parent[ry] = rx
        else:
            self.parent[ry] = rx
            self.rank[rx] += 1
 
def numIslands(grid):
    if not grid:
        return 0
    m, n = len(grid), len(grid[0])
    uf = UnionFind(m * n)
    directions = [(1,0), (-1,0), (0,1), (0,-1)]
 
    for i in range(m):
        for j in range(n):
            if grid[i][j] == '1':
                idx = i * n + j
                for di, dj in directions:
                    ni, nj = i + di, j + dj
                    if 0 <= ni < m and 0 <= nj < n and grid[ni][nj] == '1':
                        uf.union(idx, ni * n + nj)
 
    # 统计所有 '1' 的根节点(去重)
    roots = set()
    for i in range(m):
        for j in range(n):
            if grid[i][j] == '1':
                roots.add(uf.find(i * n + j))
    return len(roots)

复杂度: 时间 O(m×n × α(mn)),空间 O(m×n)


易错点

  • 边界越界: 递归 DFS 时一定要先检查行列索引是否在有效范围内,否则会报 IndexError。
  • 重复访问: 必须及时标记已访问,避免死循环(上下左右来回走)。DFS 在调用递归前就应该标记,BFS 在入队时就要标记,不要等到出队时再标记。
  • 递归深度爆栈: 极端情况下(整个网格全是 '1'),DFS 递归深度可达 90,000 层,Python 会 RecursionError。此时应改用 BFS 或手动设置 sys.setrecursionlimit()
  • 行列顺序: grid[i][j] 中 i 是行号(纵坐标),j 是列号(横坐标),四方向遍历时注意 dx 对应行变化。
  • 原地修改副作用: 沉岛法会直接修改原输入数组,如果不允许修改原数组,需要额外 visited 矩阵。

框架提炼

网格 DFS 模板(四方向):

def grid_dfs(grid):
    def dfs(i, j):
        # 1. 边界 + 有效性检查
        if i < 0 or i >= m or j < 0 or j >= n or visited[i][j] or grid[i][j] != target:
            return
        visited[i][j] = True   # 2. 标记已访问
        # 3. 四方向递归
        dfs(i+1, j); dfs(i-1, j)
        dfs(i, j+1); dfs(i, j-1)
 
    m, n = len(grid), len(grid[0])
    visited = [[False] * n for _ in range(m)]
    for i in range(m):
        for j in range(n):
            if not visited[i][j] and grid[i][j] == target:
                dfs(i, j)  # 或在这里计数

网格 BFS 模板:

def grid_bfs(grid):
    from collections import deque
    m, n = len(grid), len(grid[0])
    visited = [[False] * n for _ in range(m)]
    q = deque()
    while q:
        i, j = q.popleft()
        for di, dj in [(1,0),(-1,0),(0,1),(0,-1)]:
            ni, nj = i+di, j+dj
            if 0 <= ni < m and 0 <= nj < n and not visited[ni][nj] and grid[ni][nj] == target:
                visited[ni][nj] = True
                q.append((ni, nj))

关联题目

  • 994-腐烂的橘子 — 同一网格四方向扩散模型,但本题是 DFS 找连通分量,994 是多源 BFS 求最短扩散时间。
  • 207-课程表 — 图论的另一种题目:有向图拓扑排序,与岛屿的网格图遍历互为补充。
  • 130-被围绕的区域 — 也是网格 DFS,但边界条件处理更复杂(从边界 O 出发 DFS,标记未被包围的区域)。