1. 岛屿数量问题概述
力扣第200题"岛屿数量"是hot100中的经典题目,也是面试中频繁出现的算法考题。题目给定一个由'1'(陆地)和'0'(水)组成的二维网格,要求计算网格中岛屿的数量。岛屿被定义为被水包围的陆地区域,且岛屿之间通过水平或垂直方向相邻的陆地连接。
这个问题看似简单,但涉及图论中的连通分量概念,是深度优先搜索(DFS)和广度优先搜索(BFS)算法的典型应用场景。在实际工程中,类似算法可用于图像处理中的连通区域分析、社交网络中的社群发现等场景。
2. 问题分析与解法思路
2.1 问题建模
首先我们需要将这个问题转化为计算机可以处理的形式。给定的二维网格可以看作一个矩阵,其中每个元素要么是'1'(陆地),要么是'0'(水)。两个'1'如果在水平或垂直方向相邻,则认为它们属于同一个岛屿。
从图论的角度看,我们可以把每个'1'看作图中的一个节点,相邻的'1'之间存在边。那么问题就转化为求这个图中的连通分量数量。
2.2 基本解法思路
解决这个问题的核心思路是:
- 遍历整个网格
- 当遇到一个'1'时,就发现了一个新的岛屿
- 通过DFS或BFS将这个岛屿的所有相连的'1'标记为已访问
- 继续遍历,跳过已访问的'1'
- 统计遇到的未访问'1'的次数,即为岛屿数量
这种方法的正确性基于:每次遇到未访问的'1',都代表发现了一个新的连通区域(岛屿),然后我们会通过搜索算法完整地探索并标记这个区域的所有部分。
3. 深度优先搜索(DFS)解法详解
3.1 DFS算法实现
DFS是解决这个问题的自然选择,因为它可以递归地探索一个岛屿的所有部分。下面是Python实现的核心代码:
python复制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分别是网格的行数和列数。最坏情况下,我们需要访问每个格子一次。
空间复杂度:O(M×N),在最坏情况下(网格全是陆地),递归的深度可能达到M×N。
注意:在实际编码面试中,对于特别大的网格,这种递归实现可能会导致栈溢出。这时可以考虑使用显式栈的迭代实现,或者改用BFS。
4. 广度优先搜索(BFS)解法详解
4.1 BFS算法实现
BFS是另一种可行的解法,它使用队列来按层探索相邻的陆地。下面是Python实现:
python复制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解法分析
时间复杂度:同样为O(M×N),每个格子最多被访问一次。
空间复杂度:O(min(M,N)),这是由队列的最大可能大小决定的。在最坏情况下,当网格全是陆地时,队列中最多存储网格的周长级别的元素。
提示:BFS通常比DFS更适合处理大规模数据,因为它不会出现栈溢出问题,且空间复杂度通常更优。
5. 并查集(Union-Find)解法详解
5.1 并查集算法原理
并查集是一种处理不相交集合的数据结构,支持两种操作:
- Find:查找元素属于哪个集合
- Union:合并两个集合
对于岛屿问题,我们可以:
- 初始化时,每个'1'都是一个独立的集合
- 遍历网格,将相邻的'1'进行Union操作
- 最后统计剩余的集合数量
5.2 并查集实现代码
python复制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'
for dx, dy in [(1,0), (-1,0), (0,1), (0,-1)]:
nx, ny = i + dx, j + dy
if 0 <= nx < rows and 0 <= ny < cols and grid[nx][ny] == '1':
uf.union(i * cols + j, nx * cols + ny)
return uf.count
5.3 并查集解法分析
时间复杂度:O(M×N×α(M×N)),其中α是反阿克曼函数,增长极其缓慢,可以认为是常数时间。
空间复杂度:O(M×N),用于存储父节点和秩数组。
技巧:并查集解法在需要动态处理连通性问题(如边会动态增加或删除)时特别有用,虽然在这个静态问题中可能不如DFS/BFS简洁。
6. 算法优化与变种
6.1 原址算法优化
上述解法都修改了原始输入网格。如果要求不修改原网格,可以使用额外的visited数组来记录访问状态:
python复制def numIslands(grid):
if not grid:
return 0
count = 0
rows, cols = len(grid), len(grid[0])
visited = [[False for _ in range(cols)] for _ in range(rows)]
for i in range(rows):
for j in range(cols):
if grid[i][j] == '1' and not visited[i][j]:
count += 1
dfs(grid, i, j, rows, cols, visited)
return count
def dfs(grid, i, j, rows, cols, visited):
if i < 0 or j < 0 or i >= rows or j >= cols or grid[i][j] != '1' or visited[i][j]:
return
visited[i][j] = True
dfs(grid, i+1, j, rows, cols, visited)
dfs(grid, i-1, j, rows, cols, visited)
dfs(grid, i, j+1, rows, cols, visited)
dfs(grid, i, j-1, rows, cols, visited)
6.2 并行计算思路
对于特别大的网格,可以考虑并行计算:
- 将网格分割成多个区块
- 在每个区块内独立计算局部岛屿
- 合并区块时处理边界上的连通性
6.3 岛屿问题的常见变种
- 最大岛屿面积:在统计岛屿数量的同时,记录每个岛屿的大小
- 岛屿周长:计算所有岛屿的周长总和
- 封闭岛屿数量:统计完全被水包围的岛屿数量
- 不同形状岛屿数量:统计形状不同的岛屿数量
7. 面试技巧与常见问题
7.1 面试常见问题
-
如何处理非常大的网格?
- 可以考虑使用BFS避免递归栈溢出
- 或者使用迭代式DFS
- 对于极端情况,讨论并行计算的可能性
-
如果网格是动态变化的怎么办?
- 并查集数据结构更适合动态情况
- 可以增量维护岛屿数量
-
如何优化空间复杂度?
- 使用原址标记法(修改输入网格)
- 对于BFS,使用更高效的数据结构如collections.deque
7.2 代码实现注意事项
- 边界检查:确保在访问网格时不会越界
- 访问标记:确保不会重复访问已处理的节点
- 输入验证:处理空输入或非法输入的情况
- 方向处理:使用循环处理四个方向,避免重复代码
7.3 测试用例设计
好的测试用例应包括:
- 空网格
- 全为'0'的网格
- 全为'1'的网格
- 常规有多个岛屿的网格
- 大规模网格(测试性能)
- 特殊形状的岛屿(如长条形、环形)
8. 实际应用场景
岛屿数量问题看似抽象,但实际上有许多现实应用:
- 图像处理:识别二值图像中的连通区域
- 地图分析:识别地理信息系统中的独立区域
- 社交网络:发现网络中的连通社群
- 电路设计:识别电路板上的连通部分
- 医学影像:分析CT/MRI扫描中的组织区域
理解这类基础算法问题,能帮助我们在面对更复杂的实际问题时,快速识别出适用的算法模式。
