在算法面试中,“岛屿计数”是连通性问题的经典代表,也是区分DFS(深度优先搜索)与BFS(广度优先搜索)应用场景的绝佳案例。本文将从题目背景出发,完整拆解两种解法的核心逻辑,通过全方位对比明确适用场景,再提炼记忆口诀和解题触发点,帮你彻底掌握“连通性问题”的解题思维。
一、题目背景:明确问题核心
题目描述
给定一个由 1(陆地)和 0(水)组成的二维矩阵,计算岛屿的数量。岛屿由水平或垂直方向上相邻的陆地连接而成,四周均被水域包围(矩阵外也视为水)。
输入输出示例
- 输入:
3 3
1 1 0
1 1 0
0 0 1
- 输出:
2(两个独立岛屿:左上角4块陆地+右下角1块陆地)
核心矛盾
本质是寻找独立的连通块——即“未被访问过的陆地”及其所有相邻陆地组成的集合,核心需求是:
- 遍历所有单元格,不遗漏任何岛屿;
- 标记已访问的陆地,避免重复计数;
- 高效遍历相邻节点(上下左右),确保连通块完整标记。
二、两种核心解决方案:DFS与BFS
无论是DFS还是BFS,核心逻辑完全一致:「外层遍历找起点 → 内层遍历标记连通块 → 计数独立起点」,仅“内层标记连通块”的实现方式不同。
方案1:DFS(深度优先搜索)—— 一条路走到头
核心思路
类比“走迷宫”:从起点出发,沿着一个方向走到尽头(遇到水或边界),再回溯到上一个岔路口,换方向继续探索,直到所有相连陆地都被标记。
- 数据结构:递归栈(隐式,Python默认递归深度有限);
- 关键动作:递归遍历上下左右,标记已访问的陆地。
完整代码实现
def dfs(i, j, N, M, graph):
# 边界判断:越界、非陆地(0=水或已访问)直接返回
if i < 0 or j < 0 or i >= N or j >= M or graph[i][j] != 1:
return
graph[i][j] = 0 # 标记为已访问(破坏原矩阵,无额外空间)
# 递归探索上下左右四个方向
dfs(i+1, j, N, M, graph) # 下
dfs(i-1, j, N, M, graph) # 上
dfs(i, j+1, N, M, graph) # 右
dfs(i, j-1, N, M, graph) # 左
def count_islands_dfs():
N, M = map(int, input().strip().split())
graph = [list(map(int, input().strip().split())) for _ in range(N)]
count = 0
# 外层遍历:找所有未访问的陆地起点
for i in range(N):
for j in range(M):
if graph[i][j] == 1:
count += 1 # 新岛屿计数
dfs(i, j, N, M, graph) # 标记整个连通块
print(count)
if __name__ == '__main__':
count_islands_dfs()
关键细节
- 标记已访问:直接将陆地(1)改为水(0),无需额外
visited数组,空间更优; - 递归终止条件:越界或非陆地时停止,避免无效递归;
- 外层遍历:确保每个独立岛屿的起点都被找到,不遗漏。
方案2:BFS(广度优先搜索)—— 逐层扩散探索
核心思路
类比“水波扩散”:从起点出发,先探索所有紧邻的陆地(第一层邻居),标记后加入队列;再依次探索每个邻居的紧邻陆地(第二层邻居),直到队列空(所有相连陆地标记完毕)。
- 数据结构:队列(
deque,先进先出,保证逐层扩散); - 关键动作:迭代取出队列元素,遍历上下左右,标记并加入新陆地。
完整代码实现
from collections import deque
def bfs(x, y, N, M, graph):
queue = deque()
queue.append((x, y)) # 起点入队
graph[x][y] = 0 # 标记已访问
# 方向偏移量:上下左右(简化代码)
directions = [(-1, 0), (1, 0), (0, -1), (0, 1)]
while queue:
curr_x, curr_y = queue.popleft() # 取出队首元素
for dx, dy in directions:
nx, ny = curr_x + dx, curr_y + dy
# 有效性判断:不越界 + 未访问的陆地
if 0 <= nx < N and 0 <= ny < M and graph[nx][ny] == 1:
graph[nx][ny] = 0 # 标记
queue.append((nx, ny)) # 加入队列,后续探索
def count_islands_bfs():
N, M = map(int, input().strip().split())
graph = [list(map(int, input().strip().split())) for _ in range(N)]
count = 0
# 外层遍历:找所有未访问的陆地起点
for i in range(N):
for j in range(M):
if graph[i][j] == 1:
count += 1 # 新岛屿计数
bfs(i, j, N, M, graph) # 标记整个连通块
print(count)
if __name__ == '__main__':
count_islands_bfs()
关键细节
- 队列选择:用
deque的popleft()(O(1)时间),避免list.pop(0)(O(n)时间)的低效; - 方向偏移量:将四个方向封装为列表,简化遍历代码;
- 迭代实现:无递归栈溢出风险,适合大矩阵。
三、DFS与BFS全方位对比(核心维度)
| 对比维度 | DFS(深度优先搜索) | BFS(广度优先搜索) | 关键补充说明 |
|---|---|---|---|
| 核心思想 | 深度优先,回溯探索(一条路走到头) | 广度优先,逐层扩散(先邻居后远亲) | DFS靠“回溯”,BFS靠“队列管理” |
| 核心数据结构 | 递归栈(隐式)/ 手动栈(显式) | 队列(deque,显式) |
递归栈是Python自带,无需手动维护 |
| 遍历顺序 | 纵向优先(先深后广) | 横向优先(先广后深) | 例:1→2→4→5→3(DFS) vs 1→2→3→4→5(BFS) |
| 空间复杂度 | 最坏O(NM)(全陆地,递归深度=NM) | 最坏O(min(N,M))(队列最大为矩阵最短边) | 窄长矩阵BFS更省空间,正方形矩阵两者接近 |
| 时间复杂度 | O(N*M)(每个单元格仅访问1次) | O(N*M)(每个单元格仅访问1次) | 时间复杂度完全一致,无差异 |
| 代码实现 | 递归简洁,无额外依赖 | 需导入deque,迭代代码稍长 |
DFS代码行数更少,上手更快 |
| 稳定性 | 可能栈溢出(矩阵过大时) | 无栈溢出风险(迭代实现) | Python默认递归深度≈1000,超大会报错 |
| 适用场景 | 1. 矩阵较小(无栈溢出风险); 2. 连通性判断、路径存在性 |
1. 矩阵较大(怕栈溢出); 2. 最短路径、层相关问题 |
岛屿计数两者都能用,BFS更通用 |
| 标记逻辑 | 递归中实时标记,避免重复递归 | 入队时标记,避免重复入队 | 标记时机不同,但核心都是“避免重复” |
关键差异补充
- 栈溢出问题:DFS的递归实现对大矩阵不友好,比如1000x1000的全陆地矩阵,递归深度会达到1e6,远超Python默认递归深度(约1000),直接报错;而BFS的迭代实现无此问题。
- 代码简洁度:DFS递归代码比BFS更短,无需处理队列的入队出队,适合面试时快速手写。
- 延伸场景适配:如果题目变种为“求岛屿的最大面积”“求从起点到终点的最短路径”,BFS更易扩展(BFS天然适合层相关、最短路径问题),而DFS需要额外维护路径长度或面积变量。
四、两者的核心联系:本质是“连通块遍历”的两种实现
尽管DFS和BFS的遍历方式不同,但它们的核心逻辑、解题框架完全一致,都是“连通性问题”的标准解法:
- 「找起点」:外层遍历矩阵,发现未访问的陆地(1)即为新岛屿起点;
- 「计数」:每找到一个起点,岛屿数+1;
- 「标记连通块」:用DFS/BFS遍历所有相连的陆地,标记为已访问(避免重复);
- 「终止条件」:所有单元格遍历完毕,计数结束。
可以说:DFS和BFS是“同一解题框架下的不同遍历工具”,核心目标都是“完整标记连通块”,只是工具的使用方式不同。
五、一句话记忆:快速区分与选择
- DFS:“递归深探无队列,代码简洁怕溢出,小矩阵首选”;
- BFS:“队列扩散不递归,稳定通用无风险,大矩阵必备”;
- 通用口诀:“连通块问题二选一,小矩阵DFS省代码,大矩阵BFS保稳定”。
六、下次见到本题的解题Trigger(触发点)
遇到“岛屿计数”或类似连通性问题时,按以下步骤快速决策:
- 判断问题类型:是否为“找独立连通块”(如:岛屿数量、省份数量、水域面积等)?→ 直接锁定DFS/BFS;
- 看矩阵规模:
- 矩阵较小(如≤100x100):优先选DFS,代码简洁,手写快;
- 矩阵较大(如≥1000x1000):必选BFS,避免栈溢出;
- 看题目变种:
- 纯计数(无额外要求):DFS/BFS均可,按个人习惯;
- 求最短路径、层相关(如“岛屿的最大层数”):必选BFS;
- 求路径存在性、连通性:DFS更简洁;
- 标记方式选择:
- 允许修改原矩阵:直接将1→0(省空间);
- 不允许修改原矩阵:创建
visited二维数组(额外空间O(N*M))。
常见变种问题Trigger
- 变种1:求岛屿的最大面积 → DFS/BFS均可,遍历中记录面积;
- 变种2:求从左上角到右下角的最短路径(仅走陆地) → 必选BFS(层遍历天然求最短路径);
- 变种3:判断两个陆地是否连通 → 选DFS,找到一个陆地后递归探索是否能到达另一个。
七、总结:解题思维升华
岛屿计数问题的核心不是“选DFS还是BFS”,而是“理解连通块遍历的本质”——找到起点→标记所有相连节点→计数。DFS和BFS只是实现这一本质的两种工具,选择的关键在于“问题场景”而非“算法优劣”。
面试中遇到这类问题时,建议:
- 先快速手写DFS(代码简洁,易拿分),并说明“如果矩阵较大,可改用BFS避免栈溢出”;
- 解释标记逻辑:“标记已访问是为了避免重复计数和死循环”;
- 对比两种方法的优劣:体现你的思维全面性。
记住:算法学习的核心是“理解框架+灵活选择”,而非死记硬背代码。掌握了“连通块遍历”的框架,不仅能解决岛屿计数,还能应对所有类似的连通性问题(如省份数量、朋友圈、水域面积等),真正做到举一反三。
浙公网安备 33010602011771号