LeetCode 200 岛屿数量:python3 题解


题目链接:200. 岛屿数量


1. 题目理解

简单来说
我们有一张地图,上面画着陆地('1')和水('0')。
如果两块陆地是上下左右相邻的,它们就属于同一个岛屿。
斜着相邻不算。
我们的任务是数一数,这张地图里一共有多少个独立的岛屿。

核心难点
如何避免重复计数?
比如一个很大的岛屿由几十个 '1' 组成,我们遍历到第一个 '1' 时知道这是一个新岛屿,但接下来遍历到它旁边的 '1' 时,不能把它当成新岛屿。我们需要一种机制,把属于同一个岛屿的所有 '1' 都标记为“已访问”。


2. 解题思路总览

这道题本质上是图论中的连通分量计数问题。我们可以把网格看作一个图,每个格子是一个节点,相邻的 '1' 之间有边相连。

我们有三种主流解法:

  1. 深度优先搜索 (DFS):最常用,代码简洁。发现一个 '1',就递归地把周围相连的 '1' 全部“淹没”(标记为已访问)。
  2. 广度优先搜索 (BFS):使用队列。发现一个 '1',把它放入队列,然后一层层把周围相连的 '1' 都找出来并标记。
  3. 并查集 (Union-Find):一种数据结构方法。初始时每个 '1' 都是独立的集合,如果两个 '1' 相邻,就把它们合并到同一个集合。最后数有多少个集合。

下面将重点讲解 DFSBFS,因为它们最直观且面试中最常考,最后简要介绍并查集。


3. 解法一:深度优先搜索 (DFS)

思路讲解

想象我们站在一个陆地上('1')。

  1. 我们数一下:“这是一个新岛屿,计数 +1"。
  2. 然后,我们开始“淹没”这个岛屿。首先把自己站的位置改成水('0'),表示这里已经处理过了。
  3. 接着,我们看上下左右四个方向,如果旁边还是陆地('1'),我们就走到那边去,重复第 2 步。
  4. 这个过程会一直持续,直到这个岛屿所有的陆地都变成了水。
  5. 回到主程序,继续遍历网格,寻找下一个还没被淹没的 '1'

为什么要改成 '0'
这样可以起到“标记已访问”的作用,而且不需要额外的 visited 数组,节省了空间。

代码实现 (DFS)

from typing import List

class Solution:
    def numIslands(self, grid: List[List[str]]) -> int:
        # 获取网格的行数和列数
        if not grid:
            return 0
        m, n = len(grid), len(grid[0])
        count = 0
        
        # 定义 DFS 辅助函数
        def dfs(i, j):
            # 1. 边界检查:如果坐标超出网格范围,直接返回
            if i < 0 or i >= m or j < 0 or j >= n:
                return
            # 2. 内容检查:如果当前格子是水 ('0'),说明已经访问过或本来就是水,返回
            if grid[i][j] == '0':
                return
            
            # 3. 标记当前格子为已访问 (将其变为 '0',即“淹没”陆地)
            grid[i][j] = '0'
            
            # 4. 递归访问上下左右四个方向
            dfs(i - 1, j) # 上
            dfs(i + 1, j) # 下
            dfs(i, j - 1) # 左
            dfs(i, j + 1) # 右
        
        # 主循环:遍历网格中的每一个格子
        for i in range(m):
            for j in range(n):
                # 如果发现一个未被访问的陆地 ('1')
                if grid[i][j] == '1':
                    count += 1  # 岛屿数量加 1
                    dfs(i, j)   # 启动 DFS,淹没整个岛屿
        
        return count

复杂度分析

  • 时间复杂度: \(O(M \times N)\)。我们需要遍历网格中的每个格子一次。
  • 空间复杂度: \(O(M \times N)\)。最坏情况下(整个网格都是陆地),递归栈的深度可能达到 \(M \times N\)

4. 解法二:广度优先搜索 (BFS)【⭐】

(个人感觉这个思路最直观)

思路讲解

BFS 和 DFS 的目标一样,都是找到所有相连的 '1' 并标记。区别在于遍历的顺序。

  • DFS 是一条路走到黑(递归)。
  • BFS 是像水波扩散一样,一层一层向外扩展(使用队列)。

步骤

  1. 遍历网格,遇到 '1',计数 +1。
  2. 把这个 '1' 的坐标放入队列 queue,并立即将其标记为 '0'
  3. 当队列不为空时:
    • 弹出一个坐标。
    • 检查它的上下左右。
    • 如果邻居是 '1',将其标记为 '0' 并加入队列。
  4. 队列空了,说明这个岛屿处理完了。

代码实现 (BFS)

from typing import List
from collections import deque

class SolutionBFS:
    def numIslands(self, grid: List[List[str]]) -> int:
        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
                    # 发现新岛屿,开始 BFS
					self.bfs(grid, i, j, m, n)

        return count
	
	def bfs(self, grid: List[List[str]], i: int, j: int, m: int, n: int) -> None:
		grid[i][j] = '0' # 标记为已访问
		queue = deque([(i, j)]) # 初始化队列
		directions = [(-1, 0), (1, 0), (0, -1), (0, 1)]

		while queue:
			r, c = queue.popleft() # 取出队首元素

			# 检查四个方向
			for dr, dc in directions:
				nr, nc = r + dr, c + dc
				# 如果邻居在网格内 且 是陆地
				if 0 <= nr < m and 0 <= nc < n and grid[nr][nc] == '1':
					grid[nr][nc] = '0' # 标记为已访问 (入队前标记,防止重复入队)
					queue.append((nr, nc))

复杂度分析

  • 时间复杂度: \(O(M \times N)\)。每个节点最多入队一次。
  • 空间复杂度: \(O(\min(M, N))\)。队列中存储的节点数量在最坏情况下与网格的短边长度相关(实际上最坏也是 \(O(M \times N)\),但通常比 DFS 递归栈更稳定,不会栈溢出)。

5. 解法三:并查集 (Union-Find) - 进阶思路

思路讲解

这是一种不同的思维方式。

  1. 把二维网格拍平成一维。坐标 (i, j) 可以映射为 i * n + j
  2. 初始化并查集:每个 '1' 初始时都是一个独立的集合。
  3. 遍历网格:如果当前格子是 '1',检查它的右边下边(避免重复检查)。如果邻居也是 '1',就将它们所在的集合合并 (union)
  4. 最后统计有多少个独立的集合(即根节点的数量)。

这种方法在动态连接问题(比如陆地会随时间增加)中很有用,但对于这道静态题,代码量稍大,效率略低于 DFS/BFS。

代码实现 (Union-Find)

代码实现要点:

  • 对于并查集,需要维护一个 parent 数组,用来标记每个元素属于什么集合。相当于 parent[x] 是 x 的直接上级,每个人只知道自己的直接上级是谁。
  • 并查集的工作原理:
    • 基本操作 find:对于元素 x,寻找 parent[x] 是什么元素,直到 x == parent[x]。相当于,如果我们想两个人是否在同一个团体,就顺着上级一直找,看最终的老大是不是同一个人。
    • 基本操作 union:合并元素 x y 的操作是,令 parent[find(x)] = find(y)。相当于,我们去找到两个人的老大,让其中一个老大认另一个老大当上级。
    • 性能优化 路径压缩:if parent[i] != i: parent[i] = find(parent[i]),相当于让中途的人都被老大直接领导,让组织变扁平。
from typing import List

class SolutionUF:
    def numIslands(self, grid: List[List[str]]) -> int:
        if not grid:
            return 0
        
        m, n = len(grid), len(grid[0])
        # parent 数组,parent[i] 表示节点 i 的父节点
        # 大小为 m * n,初始时每个节点的父节点是自己
        parent = list(range(m * n))
        # 记录当前岛屿数量,初始假设每个 '1' 都是一个岛屿
        count = 0
        
        # 查找根节点 (带路径压缩优化)
        def find(x):
            if parent[x] != x:
                parent[x] = find(parent[x])
            return parent[x]
        
        # 合并两个集合
        def union(x, y):
            rootX = find(x)
            rootY = find(y)
            if rootX != rootY:
                parent[rootX] = rootY
                return True # 成功合并,岛屿数减 1
            return False # 已经在同一集合
        
        # 遍历网格,初始化并查集并进行合并
        for i in range(m):
            for j in range(n):
                if grid[i][j] == '1':
                    count += 1 # 先假设这是一个新岛屿
                    current_idx = i * n + j
                    
                    # 只需要检查右边和下边,避免重复处理
                    # 检查下方
                    if i + 1 < m and grid[i + 1][j] == '1':
                        down_idx = (i + 1) * n + j
                        if union(current_idx, down_idx):
                            count -= 1 # 合并成功,说明这两个 '1' 属于同一个岛屿,总数减 1
                            
                    # 检查右方
                    if j + 1 < n and grid[i][j + 1] == '1':
                        right_idx = i * n + (j + 1)
                        if union(current_idx, right_idx):
                            count -= 1
                            
        return count

6. 总结与对比

特性 DFS (递归) BFS (队列) 并查集 (Union-Find)
代码难度 ⭐ (最简单) ⭐⭐ ⭐⭐⭐
空间消耗 递归栈 (可能较深) 队列 (较稳定) 额外 parent 数组
运行速度 稍慢 (常数大)
适用场景 通用,面试首选 网格很大怕栈溢出时 动态连通性问题

建议
在面试中,DFS 解法通常是首选,因为它写起来最快,逻辑最清晰。如果你担心网格太大导致递归深度溢出(Python 默认递归深度限制约为 1000),可以主动提出使用 BFS 作为优化方案。



posted @ 2026-03-10 15:00  MoonOut  阅读(141)  评论(0)    收藏  举报