在算法面试中,“岛屿计数”是连通性问题的经典代表,也是区分DFS(深度优先搜索)与BFS(广度优先搜索)应用场景的绝佳案例。本文将从题目背景出发,完整拆解两种解法的核心逻辑,通过全方位对比明确适用场景,再提炼记忆口诀和解题触发点,帮你彻底掌握“连通性问题”的解题思维。

一、题目背景:明确问题核心

题目描述

给定一个由 1(陆地)和 0(水)组成的二维矩阵,计算岛屿的数量。岛屿由水平或垂直方向上相邻的陆地连接而成,四周均被水域包围(矩阵外也视为水)。

输入输出示例

  • 输入:
3 3
1 1 0
1 1 0
0 0 1
  • 输出:2(两个独立岛屿:左上角4块陆地+右下角1块陆地)

核心矛盾

本质是寻找独立的连通块——即“未被访问过的陆地”及其所有相邻陆地组成的集合,核心需求是:

  1. 遍历所有单元格,不遗漏任何岛屿;
  2. 标记已访问的陆地,避免重复计数;
  3. 高效遍历相邻节点(上下左右),确保连通块完整标记。

二、两种核心解决方案: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()

关键细节

  • 队列选择:用dequepopleft()(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更通用
标记逻辑 递归中实时标记,避免重复递归 入队时标记,避免重复入队 标记时机不同,但核心都是“避免重复”

关键差异补充

  1. 栈溢出问题:DFS的递归实现对大矩阵不友好,比如1000x1000的全陆地矩阵,递归深度会达到1e6,远超Python默认递归深度(约1000),直接报错;而BFS的迭代实现无此问题。
  2. 代码简洁度:DFS递归代码比BFS更短,无需处理队列的入队出队,适合面试时快速手写。
  3. 延伸场景适配:如果题目变种为“求岛屿的最大面积”“求从起点到终点的最短路径”,BFS更易扩展(BFS天然适合层相关、最短路径问题),而DFS需要额外维护路径长度或面积变量。

四、两者的核心联系:本质是“连通块遍历”的两种实现

尽管DFS和BFS的遍历方式不同,但它们的核心逻辑、解题框架完全一致,都是“连通性问题”的标准解法:

  1. 「找起点」:外层遍历矩阵,发现未访问的陆地(1)即为新岛屿起点;
  2. 「计数」:每找到一个起点,岛屿数+1;
  3. 「标记连通块」:用DFS/BFS遍历所有相连的陆地,标记为已访问(避免重复);
  4. 「终止条件」:所有单元格遍历完毕,计数结束。

可以说:DFS和BFS是“同一解题框架下的不同遍历工具”,核心目标都是“完整标记连通块”,只是工具的使用方式不同。

五、一句话记忆:快速区分与选择

  • DFS:“递归深探无队列,代码简洁怕溢出,小矩阵首选”;
  • BFS:“队列扩散不递归,稳定通用无风险,大矩阵必备”;
  • 通用口诀:“连通块问题二选一,小矩阵DFS省代码,大矩阵BFS保稳定”。

六、下次见到本题的解题Trigger(触发点)

遇到“岛屿计数”或类似连通性问题时,按以下步骤快速决策:

  1. 判断问题类型:是否为“找独立连通块”(如:岛屿数量、省份数量、水域面积等)?→ 直接锁定DFS/BFS;
  2. 看矩阵规模
    • 矩阵较小(如≤100x100):优先选DFS,代码简洁,手写快;
    • 矩阵较大(如≥1000x1000):必选BFS,避免栈溢出;
  3. 看题目变种
    • 纯计数(无额外要求):DFS/BFS均可,按个人习惯;
    • 求最短路径、层相关(如“岛屿的最大层数”):必选BFS;
    • 求路径存在性、连通性:DFS更简洁;
  4. 标记方式选择
    • 允许修改原矩阵:直接将1→0(省空间);
    • 不允许修改原矩阵:创建visited二维数组(额外空间O(N*M))。

常见变种问题Trigger

  • 变种1:求岛屿的最大面积 → DFS/BFS均可,遍历中记录面积;
  • 变种2:求从左上角到右下角的最短路径(仅走陆地) → 必选BFS(层遍历天然求最短路径);
  • 变种3:判断两个陆地是否连通 → 选DFS,找到一个陆地后递归探索是否能到达另一个。

七、总结:解题思维升华

岛屿计数问题的核心不是“选DFS还是BFS”,而是“理解连通块遍历的本质”——找到起点→标记所有相连节点→计数。DFS和BFS只是实现这一本质的两种工具,选择的关键在于“问题场景”而非“算法优劣”。

面试中遇到这类问题时,建议:

  1. 先快速手写DFS(代码简洁,易拿分),并说明“如果矩阵较大,可改用BFS避免栈溢出”;
  2. 解释标记逻辑:“标记已访问是为了避免重复计数和死循环”;
  3. 对比两种方法的优劣:体现你的思维全面性。

记住:算法学习的核心是“理解框架+灵活选择”,而非死记硬背代码。掌握了“连通块遍历”的框架,不仅能解决岛屿计数,还能应对所有类似的连通性问题(如省份数量、朋友圈、水域面积等),真正做到举一反三。