1. 算法训练营项目概述
最近在算法训练营中遇到了两个经典的岛屿类问题:岛屿数量(LeetCode 200)和岛屿的最大面积(LeetCode 695)。这类问题在技术面试中出现频率极高,也是考察图遍历基本功的绝佳案例。作为参加过多次算法竞赛的老手,我想分享一下这类问题的系统解法框架和实际编码中的关键细节。
岛屿问题本质上属于二维矩阵中的连通分量问题,通常使用深度优先搜索(DFS)或广度优先搜索(BFS)来解决。虽然题目表面看起来简单,但在实际实现时边界条件处理、访问标记策略等细节往往会让新手栽跟头。下面我将从问题分析、解法对比到具体实现,完整拆解这两个经典问题。
2. 问题分析与算法选型
2.1 岛屿数量问题解析
给定一个由 '1'(陆地)和 '0'(水)组成的二维网格,计算岛屿的数量。岛屿由水平或垂直方向上相邻的陆地连接形成,假设网格四周都被水包围。
关键观察点:
- 连通性定义:仅考虑上下左右四个方向(有些变种问题会包含对角线)
- 孤立陆地也算作一个岛屿
- 需要避免重复计数
2.2 岛屿最大面积问题解析
在相同类型的网格中,找出最大的岛屿面积。岛屿面积定义为组成岛屿的陆地格子总数。
与岛屿数量问题的区别:
- 需要记录每个连通块的大小
- 要在遍历过程中维护最大值
- 基本遍历策略相同但需要额外计数
2.3 算法选择与复杂度分析
对于这类网格遍历问题,DFS和BFS都是可行方案。我通常基于以下考虑做出选择:
DFS优势:
- 代码实现更简洁
- 递归调用栈在网格不大时效率不错
- 适合需要"探到底"的场景
BFS优势:
- 非递归实现避免栈溢出
- 更符合"层层推进"的直观理解
- 适合求最短路径等场景
时间复杂度:O(M×N),其中M和N分别是网格的行数和列数。因为每个格子最多被访问一次。
空间复杂度:O(M×N),最坏情况下整个网格都是陆地,递归调用栈或队列会达到M×N。
3. DFS解法实现细节
3.1 岛屿数量的DFS实现
python复制def numIslands(grid):
if not grid:
return 0
count = 0
rows, cols = len(grid), len(grid[0])
def dfs(r, c):
if r < 0 or c < 0 or r >= rows or c >= cols or grid[r][c] != '1':
return
grid[r][c] = '#' # 标记为已访问
dfs(r+1, c)
dfs(r-1, c)
dfs(r, c+1)
dfs(r, c-1)
for r in range(rows):
for c in range(cols):
if grid[r][c] == '1':
count += 1
dfs(r, c)
return count
关键点说明:
- 使用原地修改网格值的方法标记访问状态,节省空间
- 递归前先检查边界条件,避免无效递归
- 主循环中每次遇到未访问的'1'就启动一次DFS
3.2 岛屿最大面积的DFS实现
python复制def maxAreaOfIsland(grid):
if not grid:
return 0
max_area = 0
rows, cols = len(grid), len(grid[0])
def dfs(r, c):
if r < 0 or c < 0 or r >= rows or c >= cols or grid[r][c] != 1:
return 0
grid[r][c] = 0 # 标记为已访问
return 1 + dfs(r+1, c) + dfs(r-1, c) + dfs(r, c+1) + dfs(r, c-1)
for r in range(rows):
for c in range(cols):
if grid[r][c] == 1:
max_area = max(max_area, dfs(r, c))
return max_area
改进点:
- DFS函数返回当前岛屿的面积
- 使用返回值累加计算总面积
- 实时更新最大面积
4. BFS解法实现细节
4.1 岛屿数量的BFS实现
python复制from collections import deque
def numIslands(grid):
if not grid:
return 0
count = 0
rows, cols = len(grid), len(grid[0])
for r in range(rows):
for c in range(cols):
if grid[r][c] == '1':
count += 1
grid[r][c] = '0' # 标记为已访问
queue = deque([(r, c)])
while queue:
row, col = queue.popleft()
for dr, dc in [(-1,0),(1,0),(0,-1),(0,1)]:
nr, nc = row + dr, col + dc
if 0 <= nr < rows and 0 <= nc < cols and grid[nr][nc] == '1':
grid[nr][nc] = '0'
queue.append((nr, nc))
return count
BFS特点:
- 使用队列管理待访问节点
- 每次从队列取出一个节点并处理其邻居
- 非递归实现,适合大规模网格
4.2 岛屿最大面积的BFS实现
python复制from collections import deque
def maxAreaOfIsland(grid):
if not grid:
return 0
max_area = 0
rows, cols = len(grid), len(grid[0])
for r in range(rows):
for c in range(cols):
if grid[r][c] == 1:
grid[r][c] = 0
queue = deque([(r, c)])
area = 1
while queue:
row, col = queue.popleft()
for dr, dc in [(-1,0),(1,0),(0,-1),(0,1)]:
nr, nc = row + dr, col + dc
if 0 <= nr < rows and 0 <= nc < cols and grid[nr][nc] == 1:
grid[nr][nc] = 0
area += 1
queue.append((nr, nc))
max_area = max(max_area, area)
return max_area
面积计算技巧:
- 初始化当前岛屿面积为1(当前格子)
- 每发现一个新陆地格子就增加面积计数
- 完成BFS后更新全局最大值
5. 性能优化与变种问题
5.1 访问标记的优化策略
原始方法通过修改输入网格来标记访问状态,这在某些情况下可能不被允许。替代方案:
- 使用单独的二维visited数组:
python复制visited = [[False for _ in range(cols)] for _ in range(rows)]
空间复杂度加倍但保持输入不变
- 使用集合存储已访问坐标:
python复制visited = set()
visited.add((r,c))
适合稀疏网格,节省空间
5.2 常见变种问题
- 岛屿周长问题:
- 只需统计陆地与水域或边界的相邻边数
- 技巧:每个陆地格子初始贡献4条边,每有一个相邻陆地就减1
- 封闭岛屿数量:
- 要求岛屿完全被水包围(不接触网格边界)
- 需要额外检查是否触及边界
- 不同形状岛屿计数:
- 需要记录或哈希岛屿形状
- 可通过标准化坐标或序列化形状来实现
5.3 并行计算优化
对于超大规模网格,可以考虑:
- 将网格分块处理
- 使用多线程或分布式计算
- 注意边界区域的合并处理
6. 常见错误与调试技巧
6.1 典型错误案例
- 忘记标记已访问节点:
- 导致无限循环和栈溢出
- 表现为程序卡死或超时
- 边界条件检查不全:
- 数组越界访问
- 特别容易发生在递归实现的边界返回条件
- 方向向量定义错误:
- 漏掉某些方向
- 对角线方向是否应该包含要看题意
6.2 调试建议
- 小规模测试用例:
python复制grid = [
["1","1","0","0","0"],
["1","1","0","0","0"],
["0","0","1","0","0"],
["0","0","0","1","1"]
]
预期结果:岛屿数量3,最大面积4
- 极端情况测试:
- 空网格
- 全陆地网格
- 全水域网格
- 单行或单列网格
- 可视化调试:
- 打印每次DFS/BFS后的网格状态
- 用不同符号标记已访问区域
7. 工程实践中的应用
岛屿问题不仅是算法题,在实际工程中也有广泛应用:
- 图像处理:
- 连通区域分析
- 图像分割预处理
- 游戏开发:
- 地图区域划分
- 可通行区域计算
- GIS系统:
- 陆地地块识别
- 湖泊水域计算
- 社交网络:
- 社区发现
- 关联用户群体识别
掌握这类网格遍历算法,能够帮助开发者快速解决许多实际场景中的连通性问题。我建议在理解基础解法后,多尝试一些变种问题来巩固思维模式。