Posted in

Go语言BFS与DFS模板大全:力扣树与图问题终极解决方案

第一章:Go语言BFS与DFS模板大全:力扣树与图问题终极解决方案

核心思想对比

广度优先搜索(BFS)与深度优先搜索(DFS)是解决树与图类算法问题的两大基石。BFS 适用于求解最短路径、层级遍历等问题,通常借助队列实现;DFS 更适合路径探索、回溯、连通性判断等场景,常用递归或栈实现。

BFS 基础模板(树的层序遍历)

func bfs(root *TreeNode) []int {
    if root == nil {
        return []int{}
    }
    var res []int
    queue := []*TreeNode{root} // 初始化队列

    for len(queue) > 0 {
        node := queue[0]       // 取出队首
        queue = queue[1:]
        res = append(res, node.Val)

        if node.Left != nil {
            queue = append(queue, node.Left)
        }
        if node.Right != nil {
            queue = append(queue, node.Right)
        }
    }
    return res
}

该模板按层级从上到下、从左到右遍历二叉树,适用于「力扣102. 二叉树的层序遍历」等问题。

DFS 递归模板(前序遍历为例)

func dfs(root *TreeNode, res *[]int) {
    if root == nil {
        return
    }
    *res = append(*res, root.Val)   // 访问当前节点
    dfs(root.Left, res)             // 遍历左子树
    dfs(root.Right, res)            // 遍历右子树
}

调用方式:result := []int{}; dfs(root, &result)。此结构可轻松调整为中序或后序遍历。

模板适用场景对照表

问题类型 推荐方法 典型力扣题号
层序遍历 BFS 102, 103
路径总和 DFS 112, 113
图的连通分量 DFS/BFS 200
最短路径(无权图) BFS 127, 994

掌握这两个模板并灵活变形,可高效攻克绝大多数树与图的遍历类题目。

第二章:广度优先搜索(BFS)核心原理与Go实现

2.1 BFS算法思想与队列数据结构设计

广度优先搜索的核心思想

BFS(Breadth-First Search)通过逐层扩展的方式遍历图或树结构,优先访问当前节点的所有邻接节点,再进入下一层。该策略确保首次到达目标节点时路径最短,适用于无权图的最短路径求解。

队列在BFS中的关键作用

使用先进先出(FIFO)的队列结构管理待访问节点,保证节点按层次顺序处理。每出队一个节点,将其未访问的邻接节点入队,实现层级扩散。

数据结构 特性 用途
队列 FIFO 存储待处理节点
访问标记数组 布尔型 防止重复访问
from collections import deque
def bfs(graph, start):
    visited = set()
    queue = deque([start])  # 初始化队列
    visited.add(start)

    while queue:
        node = queue.popleft()      # 取出队首节点
        for neighbor in graph[node]:
            if neighbor not in visited:
                visited.add(neighbor)
                queue.append(neighbor)  # 邻接节点入队

代码中 deque 提供高效的出队与入队操作,visited 集合避免环路导致的无限循环,确保每个节点仅被处理一次。

2.2 层序遍历在二叉树中的经典应用

层序遍历,又称广度优先遍历,按树的层级逐层访问节点,是处理树形结构中与“层次”相关问题的核心手段。

层序遍历基础实现

from collections import deque

def level_order(root):
    if not root:
        return []
    result, queue = [], deque([root])
    while queue:
        node = queue.popleft()
        result.append(node.val)
        if node.left: queue.append(node.left)
        if node.right: queue.append(node.right)
    return result

逻辑分析:使用队列维护待访问节点,先入先出保证从左到右、从上到下的访问顺序。popleft()取出当前层节点,子节点依次入队,实现层级扩展。

经典应用场景

  • 按层打印二叉树
  • 计算二叉树的最小深度
  • 判断完全二叉树
  • 找每一层的最大值

层级分组遍历

通过记录每层节点数,可实现分层输出: 层级 节点值
1 [3]
2 [9, 20]
3 [15, 7]
graph TD
    A[根节点入队]
    B{队列非空?}
    C[取出队首节点]
    D[访问节点值]
    E[左子入队]
    F[右子入队]
    G[继续循环]
    A --> B --> C --> D --> E --> F --> G --> B

2.3 多源BFS与最短路径问题实战解析

在图论中,单源BFS可求解无权图的最短路径,而多源BFS则扩展了这一思想——从多个起点同时发起搜索,适用于“多个起点到任意终点”的最短距离场景。

应用场景分析

典型应用包括:

  • 矩阵中多个‘0’到每个‘1’的最近距离
  • 多个服务器节点向客户端广播的最小跳数
  • 图像处理中的多种子点扩散算法

算法实现核心

通过将所有源点一次性加入队列,统一进行层次遍历:

from collections import deque

def multi_source_bfs(grid):
    m, n = len(grid), len(grid[0])
    dist = [[-1] * n for _ in range(m)]
    q = deque()

    # 将所有源点入队(如值为0的位置)
    for i in range(m):
        for j in range(n):
            if grid[i][j] == 0:
                q.append((i, j))
                dist[i][j] = 0

    # BFS扩展
    directions = [(1,0), (-1,0), (0,1), (0,-1)]
    while q:
        x, y = q.popleft()
        for dx, dy in directions:
            nx, ny = x + dx, y + dy
            if 0 <= nx < m and 0 <= ny < n and dist[nx][ny] == -1:
                dist[nx][ny] = dist[x][y] + 1
                q.append((nx, ny))
    return dist

逻辑说明:初始化阶段将所有源点距离设为0并入队,后续每轮扩展更新未访问节点的距离。由于BFS按层推进,首次访问即为最短路径,确保结果正确性。

方法 时间复杂度 空间复杂度 适用场景
单源BFS O(V+E) O(V) 单一起点
多源BFS O(V+E) O(V) 多个起点统一计算

执行流程示意

graph TD
    A[初始化所有源点] --> B{加入队列}
    B --> C[出队当前节点]
    C --> D[遍历四个方向]
    D --> E[若合法且未访问]
    E --> F[更新距离并入队]
    F --> C

2.4 使用BFS解决力扣典型图论题目

广度优先搜索(BFS)是图遍历的重要方法,尤其适用于求解最短路径类问题。其核心思想是以起始节点为中心,逐层向外扩展,直到访问到目标节点。

层序遍历与队列应用

BFS通常借助队列实现,保证先进先出的访问顺序。以下为标准BFS框架:

from collections import deque

def bfs(graph, start):
    visited = set()
    queue = deque([start])
    visited.add(start)

    while queue:
        node = queue.popleft()
        for neighbor in graph[node]:
            if neighbor not in visited:
                visited.add(neighbor)
                queue.append(neighbor)

逻辑分析visited 集合防止重复访问;deque 提供高效出队操作;每轮从队列取出当前层节点,并将其未访问邻居加入下一层。

典型应用场景对比

题目类型 是否适合BFS 原因说明
无权图最短路径 BFS首次到达即为最短距离
连通分量计数 可完整遍历连通区域
拓扑排序 应使用DFS或Kahn算法

多源BFS示例:腐烂的橘子

当存在多个起始点时,可将所有起点同时入队,进行多源BFS,有效模拟扩散过程。

2.5 优化技巧:状态去重与提前终止策略

在搜索与图遍历算法中,状态去重是避免重复计算的关键手段。通过维护一个已访问状态的集合,可显著减少冗余路径的探索。

使用哈希集合实现状态去重

visited = set()
state = (x, y, fuel)
if state in visited:
    continue
visited.add(state)

该代码片段将坐标与资源状态组合为元组,利用集合的 $O(1)$ 查询特性快速判断是否已处理。适用于状态空间有限的问题场景。

提前终止策略

当目标状态明确时,一旦找到解即可立即返回:

  • 在 BFS 中首次到达目标即为最短路径
  • 利用优先队列(如 Dijkstra)可在高优先级路径命中时中断

性能对比表

策略 时间复杂度 空间开销 适用场景
无优化 $O(b^d)$ 小规模问题
状态去重 $O(V + E)$ 图结构明确
提前终止 视情况而定 目标导向搜索

结合使用可大幅提升执行效率。

第三章:深度优先搜索(DFS)递归与回溯框架

3.1 DFS基础模型与递归三要素分析

深度优先搜索(DFS)是一种用于遍历或搜索树与图的算法,其核心思想是沿着一条路径深入到底,再回溯尝试其他路径。实现上通常借助递归完成,关键在于理解“递归三要素”:递归终止条件、当前层处理逻辑、递归调用与回溯

核心代码结构

def dfs(node, visited, graph):
    if node in visited:  # 终止条件:已访问则返回
        return
    visited.add(node)    # 处理当前节点
    for neighbor in graph[node]:
        dfs(neighbor, visited, graph)  # 递归访问邻居

上述代码中,visited 集合避免重复访问,防止无限递归;graph 表示邻接表存储的图结构。每次进入新节点即标记为已访问,确保每个节点仅被深入一次。

递归三要素解析

  • 终止条件:节点已在 visited 中,防止循环;
  • 当前处理:将当前节点加入访问集合;
  • 递归与回溯:遍历相邻节点并递归调用,隐式利用函数栈实现路径回溯。

算法执行流程示意

graph TD
    A[开始] --> B{节点已访问?}
    B -->|是| C[返回]
    B -->|否| D[标记为已访问]
    D --> E[遍历邻居]
    E --> F[递归调用DFS]
    F --> B

3.2 回溯法在组合与排列问题中的运用

回溯法通过系统地搜索所有可能的解空间,是解决组合与排列问题的核心算法策略。其本质是在递归过程中维护一个路径状态,尝试每一种选择并在不满足条件时撤销选择(即“回溯”)。

组合问题示例

以从数组中选出所有大小为 k 的子集为例,使用回溯避免重复枚举:

def combine(n, k):
    result = []
    def backtrack(start, path):
        if len(path) == k:
            result.append(path[:])
            return
        for i in range(start, n + 1):
            path.append(i)          # 做选择
            backtrack(i + 1, path)  # 递归进入下一层
            path.pop()              # 撤销选择
    backtrack(1, [])
    return result

上述代码中,start 参数确保元素不重复选取,path 记录当前路径,pop() 实现状态回退。

排列问题差异

排列需考虑顺序,因此每次从全部未使用元素中选值,需借助布尔数组标记使用状态。

问题类型 是否有序 选择范围控制
组合 起始索引递增
排列 使用标记数组

决策树结构可视化

使用 Mermaid 展现回溯决策过程:

graph TD
    A[开始] --> B[选1]
    A --> C[选2]
    A --> D[选3]
    B --> E[选2]
    B --> F[选3]
    E --> G[选3]

该树形结构清晰反映回溯在不同分支间的探索与剪枝能力。

3.3 力扣高频DFS题型模式归纳与解法对比

常见DFS题型分类

力扣中深度优先搜索(DFS)高频题主要集中在路径搜索、组合枚举与树/图遍历三类。路径搜索如岛屿数量,组合问题如全排列,树结构则常见于二叉树最大深度。

典型模板代码

def dfs(i, path):
    if base_condition:  # 如达到目标长度
        result.append(path[:])
        return
    for choice in choices:
        if not visited[choice]:
            visited[choice] = True
            path.append(choice)
            dfs(i + 1, path)
            path.pop()  # 回溯
            visited[choice] = False

该模板通过递归探索所有分支,path记录当前路径,回溯时恢复状态以保证正确性。

模式对比分析

题型 状态变量 终止条件 是否需visited
排列组合 当前路径path 路径达指定长度
树路径求和 当前节点+累加值 叶子节点
岛屿问题 坐标(x,y) 越界或为海洋 原地修改标记

优化方向

使用剪枝减少无效搜索,例如在组合总和II中跳过重复元素:

graph TD
    A[开始DFS] --> B{是否满足边界条件?}
    B -->|是| C[加入结果集]
    B -->|否| D[遍历可选分支]
    D --> E{是否剪枝?}
    E -->|是| F[跳过]
    E -->|否| G[标记并递归]
    G --> H[回溯状态]

第四章:树与图的典型力扣题目实战演练

4.1 二叉树的最大深度与路径和问题

二叉树的遍历是理解递归结构的关键。最大深度问题可通过深度优先搜索(DFS)求解,路径和问题则要求从根到叶的节点值之和等于目标值。

最大深度计算

def maxDepth(root):
    if not root:
        return 0
    left_depth = maxDepth(root.left)
    right_depth = maxDepth(root.right)
    return max(left_depth, right_depth) + 1

该函数递归计算左右子树深度,返回较大值加1。时间复杂度为 O(n),n 为节点数。

路径和判断

使用先序遍历跟踪当前路径和:

  • 若到达叶子节点且路径和等于目标,则返回 True;
  • 否则继续递归左右子树,传递剩余目标值 targetSum - node.val

算法对比

问题类型 时间复杂度 空间复杂度(最坏) 核心思想
最大深度 O(n) O(h),h为树高 分治递归
路径和 O(n) O(h) 回溯剪枝

递归调用流程示意

graph TD
    A[根节点] --> B[左子树深度]
    A --> C[右子树深度]
    B --> D[叶子返回0]
    C --> E[叶子返回0]
    D --> F[逐层+1回传]
    E --> F
    F --> G[最终取最大值]

4.2 被围绕的区域与岛屿数量等网格题

在二维网格问题中,常见的一类题目是识别“被围绕的区域”或统计“岛屿数量”。这类问题通常建模为在 m x n 的二进制网格中,1 表示陆地, 表示水域,通过深度优先搜索(DFS)或广度优先搜索(BFS)遍历连通区域。

岛屿数量求解思路

使用 DFS 遍历每个未访问的陆地格子,每发现一个新岛屿则计数加一,并将其所有相连陆地标记为已访问。

def numIslands(grid):
    if not grid: return 0
    m, n = len(grid), len(grid[0])
    count = 0

    def dfs(i, j):
        if 0 <= i < m and 0 <= j < n and grid[i][j] == '1':
            grid[i][j] = '0'  # 标记为已访问
            dfs(i+1, j)
            dfs(i-1, j)
            dfs(i, j+1)
            dfs(i, j-1)

    for i in range(m):
        for j in range(n):
            if grid[i][j] == '1':
                dfs(i, j)
                count += 1
    return count

逻辑分析:外层循环遍历每个格子;一旦发现陆地(’1’),启动 DFS 淹没整个岛屿,防止重复计数。时间复杂度为 O(mn),因为每个格子最多访问一次。

被围绕的区域处理策略

边缘的 ‘O’ 不会被包围,可从边界出发用 BFS/DFS 标记所有连通的 ‘O’ 为安全区域,剩余内部 ‘O’ 即为被包围区域。

方法 时间复杂度 空间复杂度 适用场景
DFS O(mn) O(mn) 连通性判断
BFS O(mn) O(mn) 层次遍历需求

4.3 拓扑排序与有向图环检测实现

拓扑排序用于确定有向无环图(DAG)中节点的线性顺序,广泛应用于任务调度、依赖解析等场景。若图中存在环,则无法完成拓扑排序,因此可借助该性质进行环检测。

基于深度优先搜索的实现

def topological_sort(graph):
    visited = set()
    stack = []
    has_cycle = False

    def dfs(node):
        nonlocal has_cycle
        if node in visited: return
        visited.add(node)
        for neighbor in graph.get(node, []):
            if neighbor not in visited:
                dfs(neighbor)
            elif neighbor not in stack:  # 节点已访问但未入栈,说明存在环
                has_cycle = True
        stack.append(node)

    for node in graph:
        if node not in visited:
            dfs(node)

    return [] if has_cycle else stack[::-1]

逻辑分析:使用 visited 记录已访问节点,stack 存储拓扑序列。在 DFS 回溯时将节点入栈,若遍历过程中发现邻接节点已被访问但未完成(即不在栈中),则说明存在后向边,构成环。

算法流程示意

graph TD
    A[开始遍历每个节点]
    B{节点已访问?}
    C[标记为已访问]
    D[递归访问所有邻接节点]
    E[当前节点入栈]
    F[返回逆序栈作为结果]

    A --> B
    B -- 否 --> C
    C --> D
    D --> E
    E --> F
    B -- 是 --> F

4.4 从根节点到叶子节点的所有路径生成

在树形结构中,路径生成是遍历操作的重要应用。从根节点出发,递归探索每条通往叶子节点的路径,能有效支持诸如文件系统遍历、XML解析等场景。

路径搜索的递归实现

def find_paths(root, path=[], result=[]):
    if not root:
        return result
    path.append(root.val)
    if not root.left and not root.right:  # 叶子节点
        result.append("->".join(map(str, path)))
    else:
        find_paths(root.left, path, result)   # 遍历左子树
        find_paths(root.right, path, result)  # 遍历右子树
    path.pop()  # 回溯
    return result

上述代码通过深度优先搜索维护当前路径 path,当到达叶子节点时将路径字符串加入结果集。参数 root 表示当前节点,path 记录从根到当前节点的值序列,result 收集所有完整路径。

算法执行流程可视化

graph TD
    A[根节点] --> B[左子树]
    A --> C[右子树]
    B --> D[叶子]
    C --> E[叶子]
    C --> F[叶子]

该流程图展示了从根出发的分支路径,最终生成三条独立路径:A→B→D、A→C→E 和 A→C→F。

第五章:总结与算法思维提升建议

在长期参与开源项目和企业级系统开发的过程中,许多工程师发现,真正决定代码质量的并非对某个算法的熟练记忆,而是背后所体现的算法思维。这种思维不是一蹴而就的,它建立在持续实践、模式识别与问题抽象的基础之上。以下结合真实开发场景,提出可落地的提升路径。

深入理解数据结构的本质而非API使用

例如,在处理高频交易系统的订单簿时,仅知道HashMap的增删查改是远远不够的。实际需求要求在O(1)时间内完成价格级别更新,并在O(log n)内获取最优买卖价。此时,结合TreeMap(红黑树实现)与双向链表构建多级价格队列,才能满足性能要求。关键在于理解:数据结构是为约束条件服务的

在真实项目中刻意练习分解问题

以电商平台的推荐系统为例,表面看是“猜用户喜欢什么”,实则可拆解为:

  1. 用户行为序列建模(可用滑动窗口+哈希统计)
  2. 商品相似度计算(Jaccard / 余弦相似性)
  3. 实时召回排序(Top-K堆优化)

通过将大问题转化为多个可验证的小模块,逐步用算法组件替换启发式规则,实现从“能用”到“高效”的跃迁。

建立个人算法案例库

场景 算法模型 性能收益
日志去重 Bloom Filter 内存减少76%
路由调度 一致性哈希 扩容时80%节点不变
库存扣减 分段锁 + CAS QPS提升至3倍

定期复盘线上问题,记录决策过程与替代方案对比,形成可复用的经验资产。

利用可视化工具强化直觉

graph TD
    A[原始请求流] --> B{是否热点Key?}
    B -->|是| C[本地缓存 + 异步合并写]
    B -->|否| D[直接访问Redis集群]
    C --> E[使用LRU淘汰策略]
    D --> F[读写分离 + 连接池]

如上图所示,在高并发查询场景中,通过流量分类决策路径,结合缓存穿透防护策略,显著降低后端压力。绘制此类流程图有助于识别瓶颈点。

参与代码重构与性能评审

某次支付网关响应延迟从120ms降至35ms,核心改动包括:

  • 将线性搜索替换为二分查找(输入有序)
  • 使用位运算替代取模操作:index & (size - 1) 替代 index % size
  • 预分配对象池避免频繁GC

这些优化均源于对底层执行成本的敏感度,而这种敏感度只能在真实压测环境中培养。

关注系统设计与高可用架构,思考技术的长期演进。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注