当前位置: 首页 > news >正文

从LeetCode 938(二叉搜索树范围和)到200(岛屿数量):一套DFS模板刷通两类高频题

从LeetCode 938到200:一套DFS模板解决树与图的经典问题

在算法竞赛和面试准备中,深度优先搜索(DFS)是解决树形结构和图论问题的利器。许多初学者在面对不同场景时往往会陷入"学一道题只会一道题"的困境,实际上,通过抽象出通用的DFS框架,可以举一反三解决大量问题。本文将以LeetCode 938(二叉搜索树范围和)和200(岛屿数量)为例,展示如何用同一套DFS思维模型应对树和图这两类看似迥异的数据结构。

1. DFS核心框架解析

深度优先搜索的本质是"一条路走到底"的探索策略。无论是处理二叉树还是网格图,其核心都可以抽象为三个关键步骤:

  1. 处理当前节点:对当前访问的节点执行业务逻辑(如判断值范围、标记访问等)
  2. 递归子问题:按照特定顺序访问相邻节点(二叉树是左右子节点,网格是四/八邻域)
  3. 处理返回值:将子问题的结果与当前节点信息整合后返回

这个通用模板可以用伪代码表示为:

def dfs(node, ...): # 终止条件判断 if not node: return base_case # 处理当前节点(前序位置) process_current_node(node) # 递归子问题 left_result = dfs(node.left, ...) right_result = dfs(node.right, ...) # 整合结果(后序位置) return combine_results(node, left_result, right_result)

1.1 二叉树场景的DFS特点

在二叉树问题中,DFS通常表现出以下特征:

  • 访问顺序灵活:前序、中序、后序遍历对应不同处理时机
  • 递归方向明确:只需处理左右两个子节点
  • 天然无环:不需要额外记录访问状态

以LeetCode 938为例,二叉搜索树的特性允许我们在递归过程中进行剪枝:

def rangeSumBST(root, low, high): if not root: return 0 if root.val > high: # 只需搜索左子树 return rangeSumBST(root.left, low, high) if root.val < low: # 只需搜索右子树 return rangeSumBST(root.right, low, high) # 当前节点在范围内,累加三部分 return (root.val + rangeSumBST(root.left, low, high) + rangeSumBST(root.right, low, high))

1.2 网格图场景的DFS特点

网格图(二维矩阵)中的DFS则有所不同:

  • 四/八方向扩展:需要处理多个相邻单元格
  • 必须记录访问状态:防止重复访问形成死循环
  • 边界检查必要:确保不会越界访问

LeetCode 200的岛屿计数问题展示了这些特点:

def numIslands(grid): if not grid: return 0 count = 0 for i in range(len(grid)): for j in range(len(grid[0])): if grid[i][j] == '1': count += 1 dfs(grid, i, j) return count def dfs(grid, i, j): # 边界条件和终止条件 if (i < 0 or j < 0 or i >= len(grid) or j >= len(grid[0]) or grid[i][j] != '1'): return grid[i][j] = '0' # 标记为已访问 # 四方向递归 dfs(grid, i+1, j) dfs(grid, i-1, j) dfs(grid, i, j+1) dfs(grid, i, j-1)

2. 两类问题的对比与转换

虽然树和图的问题看起来差异很大,但从DFS视角可以找到诸多共性:

特性二叉树 (LeetCode 938)网格图 (LeetCode 200)
节点表示TreeNode对象矩阵坐标(i,j)
相邻节点访问固定的left/right子节点动态的四/八邻域坐标
访问标记不需要(天然无环)必须修改原矩阵或使用额外空间记录
递归终止条件节点为None越界或不符合条件
剪枝优化利用BST特性提前终止分支无特殊优化
结果聚合累加符合条件的节点值统计连通区域数量

2.1 访问标记的差异处理

在树结构中,由于父子关系的单向性,我们不需要担心重复访问问题。但在网格图中,必须对已访问的节点进行标记,否则会陷入无限循环。例如在岛屿问题中,将访问过的'1'改为'0'就是典型的"就地标记"技巧。

2.2 递归方向的动态调整

二叉树的递归方向是固定的(先左后右),而网格图的递归方向可以根据问题需求灵活调整。某些问题可能只需要检查两个方向(如向右和向下),而大多数连通性问题需要检查四个基本方向。

方向数组是处理网格类问题的实用技巧:

# 四方向 directions = [(-1,0), (1,0), (0,-1), (0,1)] # 八方向 directions = [(-1,-1), (-1,0), (-1,1), (0,-1), (0,1), (1,-1), (1,0), (1,1)] def dfs(grid, i, j): grid[i][j] = '0' for di, dj in directions: ni, nj = i + di, j + dj if 0 <= ni < len(grid) and 0 <= nj < len(grid[0]): if grid[ni][nj] == '1': dfs(grid, ni, nj)

3. 调试DFS算法的实用技巧

DFS算法常见的调试难点包括递归深度过大、条件判断不全、状态标记错误等。以下是一些实用技巧:

3.1 可视化递归过程

在二叉树问题中,可以打印缩进来显示递归层级:

def rangeSumBST(root, low, high, depth=0): print(' '*depth + f'Visit {root.val if root else None}') if not root: return 0 # 其余逻辑不变...

对于网格问题,可以在每次递归前后打印矩阵状态:

def dfs(grid, i, j): print(f'\nBefore visit ({i},{j}):') for row in grid: print(row) # 处理逻辑... print(f'\nAfter visit ({i},{j}):') for row in grid: print(row)

3.2 边界条件检查清单

编写DFS时,建议系统性地检查以下边界条件:

  • 树问题

    • 空节点处理
    • 单节点树
    • 完全倾斜的树(如只有左子树)
  • 网格问题

    • 空矩阵或1x1矩阵
    • 全'0'或全'1'的情况
    • 边界上的单元格访问

3.3 递归深度限制问题

Python默认递归深度限制约为1000层,对于大规模问题可能需要改为迭代实现。二叉树问题中,迭代DFS可以使用显式栈:

def rangeSumBST_iter(root, low, high): stack = [] total = 0 while root or stack: while root: stack.append(root) root = root.left root = stack.pop() if low <= root.val <= high: total += root.val root = root.right return total

网格问题同样可以改为栈实现的DFS:

def numIslands_iter(grid): if not grid: return 0 stack = [] count = 0 for i in range(len(grid)): for j in range(len(grid[0])): if grid[i][j] == '1': count += 1 stack.append((i,j)) while stack: x, y = stack.pop() if (0 <= x < len(grid) and 0 <= y < len(grid[0]) and grid[x][y] == '1'): grid[x][y] = '0' stack.append((x+1,y)) stack.append((x-1,y)) stack.append((x,y+1)) stack.append((x,y-1)) return count

4. DFS模板的扩展应用

掌握基础DFS框架后,可以解决更多变种问题。以下是几个典型例子:

4.1 二叉树路径问题

LeetCode 112(路径总和)要求判断是否存在从根到叶子的路径和等于目标值:

def hasPathSum(root, target): if not root: return False if not root.left and not root.right: return root.val == target return (hasPathSum(root.left, target - root.val) or hasPathSum(root.right, target - root.val))

4.2 网格的复杂变种

LeetCode 695(岛屿最大面积)在基础岛屿问题上增加了面积计算:

def maxAreaOfIsland(grid): max_area = 0 def dfs(i, j): if (i < 0 or j < 0 or i >= len(grid) or j >= len(grid[0]) or grid[i][j] != 1): return 0 grid[i][j] = 0 return (1 + dfs(i+1,j) + dfs(i-1,j) + dfs(i,j+1) + dfs(i,j-1)) for i in range(len(grid)): for j in range(len(grid[0])): if grid[i][j] == 1: max_area = max(max_area, dfs(i,j)) return max_area

4.3 记忆化搜索优化

对于存在重复子问题的情况(如LeetCode 337打家劫舍III),可以结合记忆化技术:

def rob(root): memo = {} def dfs(node): if not node: return 0 if node in memo: return memo[node] # 抢当前节点 rob_it = node.val if node.left: rob_it += dfs(node.left.left) + dfs(node.left.right) if node.right: rob_it += dfs(node.right.left) + dfs(node.right.right) # 不抢当前节点 not_rob = dfs(node.left) + dfs(node.right) memo[node] = max(rob_it, not_rob) return memo[node] return dfs(root)

5. 性能优化与注意事项

虽然DFS模板通用性强,但在实际应用中仍需注意以下性能问题:

5.1 时间复杂度分析

  • 二叉树DFS:O(N),每个节点访问一次
  • 网格DFS:O(M×N),最坏情况下访问所有单元格

对于特定问题,可能需要进行更精确的分析。例如在二叉搜索树范围和中,利用BST特性可以实现剪枝,平均时间复杂度可能优于O(N)。

5.2 空间复杂度考量

  • 递归调用栈:二叉树最坏O(H)(H为树高),网格最坏O(M×N)(如蛇形矩阵)
  • 标记存储:网格问题可能需要额外O(M×N)空间(如果不修改原矩阵)

5.3 常见错误规避

  1. 忘记终止条件:导致无限递归
  2. 错误的状态回退:回溯问题中忘记恢复状态
  3. 重复计算:未使用记忆化技术优化
  4. 错误的方向处理:网格问题中遗漏某些方向

在竞赛和面试场景中,建议先写出基础DFS解法,再根据问题特点进行优化。对于特别大规模的数据,可能需要考虑迭代实现或更高级的算法。

http://www.cnnetsun.cn/news/2830968.html

相关文章:

  • 如何快速掌握Reloaded-II:终极游戏Mod加载器完全指南
  • GetQzonehistory:守护你的数字青春,5分钟永久备份QQ空间所有记忆
  • 告别B站弹幕烦恼:5分钟学会批量管理屏蔽词,打造纯净观看体验
  • CarMaker 10.2 新手避坑:从‘路都连不上’到‘小车跑999秒’的完整闭环道路搭建实录
  • PySyft联邦学习实战:隐私计算全链路解析
  • 深度解析novel-downloader规则扩展架构:3步实现自定义网站支持
  • 8155单片机+DS18B20实现8位LED温度监控与声光报警系统
  • UKI.js终极指南:10分钟掌握轻量级Web应用UI工具包
  • 智能CAN收发器硬件设计与软件配置实战:以TJA1446/TJA1466为例
  • Linux 组调度的未来演进:更精细的资源控制与多维度隔离
  • 5步解锁电视盒子潜力:从娱乐终端到全能服务器的技术蜕变 [特殊字符]
  • Mac Mouse Fix 终极指南:让普通鼠标在macOS上发挥专业级性能的完整教程
  • 完整教程:go2rtc视频流转发工具从入门到精通
  • XCOM 2模组管理器终极指南:5个简单步骤掌握AML启动器
  • 如何让老旧Mac重获新生:OpenCore Legacy Patcher完整升级指南
  • 突破性智慧教育平台电子课本解析方案:一站式PDF教材智能下载工具
  • 千万级存量复杂文档,如何进入企业知识库和大模型应用?
  • MSC8101 HDI16引导加载:从硬件连接到软件实现的嵌入式DSP启动指南
  • Mengzi-T5-Base性能评测:在8大中文NLP任务中的表现分析
  • 从Markdown到API文档:手把手教你用Doxygen + GitHub Actions打造自动化文档流水线
  • 终极指南:如何10分钟完成黑苹果OpenCore EFI配置
  • 如何永久保存微信聊天记录?WeChatMsg三步实现数据自主掌控
  • 如何用Platinum-MD让经典MiniDisc设备焕发新生:完整免费开源音乐传输指南
  • Polygon Shredder中的Curl Noise算法详解:创建自然粒子流动的终极教程
  • hh-lol-prophet:基于LCU API的智能队友分析系统,排位胜率提升30%的实战工具
  • 如何在手机上轻松管理宝可梦存档?PKHeX.Mobile全攻略
  • NXP KW45蓝牙与Wi-Fi硬件共存机制详解与工程实践
  • 适合股票信息整理与研究记录的AI工具梳理
  • Winhance中文版终极指南:如何让Windows系统优化变得简单又高效
  • 鸿蒙 PC 多屏协同:架构解析 + 代码示例