☰
岛屿数量问题解析:DFS、BFS与并查集实现
2026/11/2 15:57:52 网站建设 项目流程

1. 岛屿数量问题解析

"岛屿数量"是计算机算法中一个经典的图论问题,也是各大科技公司面试中的高频考题。我第一次遇到这个问题是在准备算法面试时,当时就被它简洁描述背后隐藏的巧妙解法所吸引。这个问题看似简单,却能考察面试者对深度优先搜索(DFS)和广度优先搜索(BFS)等基础算法的掌握程度。

问题的标准描述是:给定一个由'1'(陆地)和'0'(水)组成的二维网格,计算网格中岛屿的数量。岛屿被定义为被水包围的、且通过水平或垂直方向上相邻的陆地连接形成的区域。你可以假设网格的四个边均被水包围。

2. 问题分析与解法思路

2.1 问题理解与建模

首先我们需要明确几个关键点:

  1. 岛屿是由相邻的'1'组成的区域,相邻指的是水平或垂直方向,对角线不算
  2. 单独的'1'也可以构成一个岛屿
  3. 我们需要统计这样的独立区域有多少个

这个问题可以建模为图论中的"连通分量"问题,每个'1'可以看作图中的一个节点,相邻的'1'之间存在边。我们的目标就是计算这个图中有多少个连通分量。

2.2 基本解法思路

最直观的解法是遍历整个网格,当遇到一个'1'时:

  1. 岛屿计数加1
  2. 通过DFS或BFS将所有与之相连的'1'标记为已访问(可以改为'0')
  3. 继续遍历直到网格结束

这种方法确保每个岛屿只被计数一次,因为发现岛屿后会立即"淹没"整个岛屿。

3. 深度优先搜索(DFS)实现

3.1 DFS算法详解

DFS是解决这个问题的自然选择,因为它可以深入探索一个岛屿的所有部分。下面是详细的实现步骤:

def numIslands(grid): if not grid: return 0 count = 0 rows, cols = len(grid), len(grid[0]) for i in range(rows): for j in range(cols): if grid[i][j] == '1': count += 1 dfs(grid, i, j, rows, cols) return count def dfs(grid, i, j, rows, cols): if i < 0 or j < 0 or i >= rows or j >= cols or grid[i][j] != '1': return grid[i][j] = '0' # 标记为已访问 # 递归访问四个方向 dfs(grid, i+1, j, rows, cols) dfs(grid, i-1, j, rows, cols) dfs(grid, i, j+1, rows, cols) dfs(grid, i, j-1, rows, cols)

3.2 DFS的时间复杂度分析

时间复杂度:O(M×N),其中M是行数,N是列数。因为我们需要遍历整个网格,每个单元格最多被访问两次(一次在主循环,一次在DFS)。

空间复杂度:最坏情况下O(M×N),当整个网格都是陆地时,递归深度可能达到M×N。

4. 广度优先搜索(BFS)实现

4.1 BFS算法详解

BFS是另一种可行的解法,它使用队列来逐层探索相邻的陆地:

from collections import deque def numIslands(grid): if not grid: return 0 count = 0 rows, cols = len(grid), len(grid[0]) for i in range(rows): for j in range(cols): if grid[i][j] == '1': count += 1 grid[i][j] = '0' # 标记为已访问 queue = deque() queue.append((i, j)) while queue: x, y = queue.popleft() for dx, dy in [(1,0), (-1,0), (0,1), (0,-1)]: nx, ny = x + dx, y + dy if 0 <= nx < rows and 0 <= ny < cols and grid[nx][ny] == '1': grid[nx][ny] = '0' queue.append((nx, ny)) return count

4.2 BFS与DFS的比较

  1. BFS使用队列,DFS使用递归或显式栈
  2. BFS的空间复杂度在最坏情况下可能比DFS更好(特别是当岛屿形状为宽而短时)
  3. DFS的代码通常更简洁,但可能有栈溢出的风险(对于非常大的网格)
  4. 实际应用中,两者性能差异不大,选择取决于具体场景和个人偏好

5. 并查集(Union-Find)解法

5.1 并查集原理

并查集是一种树型的数据结构,用于处理不相交集合的合并及查询问题。它支持两种操作:

  • Find:确定元素属于哪个子集
  • Union:将两个子集合并成一个集合

5.2 并查集实现岛屿计数

class UnionFind: def __init__(self, grid): rows, cols = len(grid), len(grid[0]) self.count = 0 self.parent = [i for i in range(rows * cols)] self.rank = [0] * (rows * cols) for i in range(rows): for j in range(cols): if grid[i][j] == '1': self.count += 1 def find(self, i): if self.parent[i] != i: self.parent[i] = self.find(self.parent[i]) return self.parent[i] def union(self, x, y): rootx = self.find(x) rooty = self.find(y) if rootx != rooty: if self.rank[rootx] > self.rank[rooty]: self.parent[rooty] = rootx elif self.rank[rootx] < self.rank[rooty]: self.parent[rootx] = rooty else: self.parent[rooty] = rootx self.rank[rootx] += 1 self.count -= 1 def numIslands(grid): if not grid: return 0 rows, cols = len(grid), len(grid[0]) uf = UnionFind(grid) for i in range(rows): for j in range(cols): if grid[i][j] == '1': grid[i][j] = '0' index = i * cols + j if i > 0 and grid[i-1][j] == '1': uf.union(index, (i-1)*cols + j) if i < rows-1 and grid[i+1][j] == '1': uf.union(index, (i+1)*cols + j) if j > 0 and grid[i][j-1] == '1': uf.union(index, i*cols + j-1) if j < cols-1 and grid[i][j+1] == '1': uf.union(index, i*cols + j+1) return uf.count

5.3 并查集的优缺点

优点:

  1. 适合动态处理连通性问题
  2. 可以逐步添加新的连接关系

缺点:

  1. 实现相对复杂
  2. 对于静态问题(如岛屿计数),可能不如DFS/BFS高效

6. 实际应用与变种问题

6.1 实际应用场景

岛屿数量问题不仅仅是算法题,它在实际中有很多应用:

  1. 图像处理中的连通区域分析
  2. 地图服务中的陆地面积计算
  3. 医学图像中的病灶区域识别
  4. 电路板中的连通性检查

6.2 常见变种问题

  1. 统计岛屿的最大面积
  2. 计算岛屿的周长
  3. 不同形状的岛屿(允许对角线连接)
  4. 动态岛屿问题(网格会随时间变化)
  5. 三维岛屿问题(扩展到三维网格)

7. 性能优化与注意事项

7.1 优化技巧

  1. 对于非常大的网格,可以考虑迭代式DFS避免递归栈溢出
  2. 并行处理:将网格分块,分别处理后再合并结果
  3. 使用位运算优化存储(如果网格是二进制的)

7.2 常见错误与调试

  1. 忘记处理空输入的情况
  2. 数组越界访问(特别是在递归/循环中)
  3. 错误地标记已访问的单元格
  4. 忽略对角线是否应该被视为相邻的条件
  5. 在并查集实现中,路径压缩或按秩合并的实现错误

提示:在面试中,建议先讨论最简单的DFS/BFS解法,然后再讨论可能的优化和变种。清晰地解释你的思路比直接写出完美代码更重要。

8. 代码测试与验证

8.1 测试用例设计

好的测试用例应该包括:

  1. 空网格
  2. 全为'0'的网格
  3. 全为'1'的网格
  4. 常规情况(混合'0'和'1')
  5. 边缘情况(最小网格1x1)
  6. 特殊形状的岛屿(如十字形、L形等)

8.2 示例测试

def test_numIslands(): # 测试空网格 assert numIslands([]) == 0 # 测试全水网格 assert numIslands([['0','0'],['0','0']]) == 0 # 测试全陆地网格 assert numIslands([['1','1'],['1','1']]) == 1 # 测试常规情况 grid1 = [ ['1','1','0','0','0'], ['1','1','0','0','0'], ['0','0','1','0','0'], ['0','0','0','1','1'] ] assert numIslands(grid1) == 3 # 测试单个单元格 assert numIslands([['1']]) == 1 assert numIslands([['0']]) == 0 print("所有测试用例通过!")

9. 语言特定实现注意事项

9.1 Python实现要点

  1. 注意列表的边界检查
  2. Python的递归深度限制(默认1000),对于大网格可能需要改为迭代实现
  3. 使用deque实现BFS比列表更高效

9.2 Java/C++实现差异

  1. 需要显式管理内存和数据结构
  2. 递归实现可能更高效(取决于编译器优化)
  3. 多维数组的访问语法不同

10. 扩展思考与进阶学习

岛屿数量问题可以引发出许多有趣的扩展思考:

  1. 如何实时维护岛屿数量(当网格动态变化时)?
  2. 如何处理海量数据(网格无法完全放入内存)?
  3. 如何分布式计算岛屿数量?
  4. 如何可视化算法执行过程?

对于想要深入学习的同学,我推荐以下方向:

  1. 图论中的连通性问题
  2. 并查集数据结构的其他应用
  3. 并行算法设计
  4. 图像处理中的连通组件标记算法

在实际编程中遇到类似问题时,最重要的是先明确问题定义和边界条件,然后选择合适的数据结构和算法。岛屿问题教会我们,看似复杂的问题往往有简洁优雅的解决方案,关键在于如何将问题抽象和建模。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询