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

贪心 vs 动态规划:从 3 道 LeetCode 经典题看算法选择与性能差异

贪心 vs 动态规划:从 3 道 LeetCode 经典题看算法选择与性能差异

在算法学习过程中,贪心算法和动态规划(DP)是两种非常重要的解题思路。很多同学在刷题时常常困惑:什么时候该用贪心?什么时候该用动态规划?这两种算法在性能上又有何差异?本文将通过三道经典LeetCode题目,深入分析这两种算法的本质区别、适用场景和性能表现。

1. 算法思想本质对比

贪心算法和动态规划都是解决最优化问题的常用方法,但它们的思考方式有着根本的不同。

1.1 贪心算法的核心思想

贪心算法采用自顶向下的思考方式,在每一步都做出当前看起来最优的选择,希望这些局部最优选择能够导致全局最优解。贪心算法通常不需要考虑子问题的解,也不依赖于将来的选择。

贪心算法有效的两个关键性质:

  • 贪心选择性质:每一步的局部最优选择能导致全局最优解
  • 最优子结构:问题的最优解包含其子问题的最优解
# 贪心算法的典型结构 def greedy_algorithm(): sort_input() # 通常需要先排序 result = 0 for item in sorted_items: if is_valid(item): # 根据贪心策略判断 result += process(item) return result

1.2 动态规划的核心思想

动态规划采用自底向上的思考方式,通过将问题分解为相互重叠的子问题,存储子问题的解来避免重复计算。动态规划通常需要考虑所有可能的子问题解,然后做出选择。

动态规划适用的两个关键条件:

  • 重叠子问题:问题可以被分解为多个重复的子问题
  • 最优子结构:问题的最优解可以由子问题的最优解构造出来
# 动态规划的典型结构 def dynamic_programming(): dp = initialize_dp_array() # 初始化DP数组 for i in range(1, n): for j in range(m): dp[i] = optimal_choice(dp[i-1], dp[j]) # 根据状态转移方程更新 return dp[n-1]

1.3 关键区别对比表

特性贪心算法动态规划
思考方向自顶向下自底向上
子问题不保存子问题解保存子问题解
时间复杂度通常O(n)或O(nlogn)通常O(n²)或O(nm)
空间复杂度通常O(1)通常O(n)或O(nm)
证明难度需要严格证明状态转移方程明确
适用问题具有贪心选择性质具有最优子结构

2. 经典题目双解法对比

下面我们通过三道经典题目,分别展示贪心和动态规划的解法,并分析它们的差异。

2.1 买卖股票的最佳时机 II(LeetCode 122)

问题描述:给定股票每天的价格,可以多次买卖,但必须卖出后才能再买,求最大利润。

贪心解法
def maxProfit(prices): profit = 0 for i in range(1, len(prices)): if prices[i] > prices[i-1]: profit += prices[i] - prices[i-1] return profit

贪心思路:只要今天价格比昨天高就卖出,累积所有正收益。

复杂度分析

  • 时间复杂度:O(n)
  • 空间复杂度:O(1)
动态规划解法
def maxProfit(prices): n = len(prices) dp = [[0] * 2 for _ in range(n)] dp[0][0] = -prices[0] # 持有股票 dp[0][1] = 0 # 不持有股票 for i in range(1, n): # 第i天持有:前一天已持有 或 今天买入 dp[i][0] = max(dp[i-1][0], dp[i-1][1] - prices[i]) # 第i天不持有:前一天不持有 或 今天卖出 dp[i][1] = max(dp[i-1][1], dp[i-1][0] + prices[i]) return dp[-1][1]

DP思路:用dp[i][0]和dp[i][1]分别表示第i天持有/不持有股票的最大收益。

复杂度分析

  • 时间复杂度:O(n)
  • 空间复杂度:O(n)(可优化到O(1))

对比分析

  • 贪心解法更简洁高效,但需要问题具有贪心选择性质
  • DP解法更通用,可以解决更复杂变种(如含手续费、冷冻期等)

2.2 跳跃游戏 II(LeetCode 45)

问题描述:给定非负整数数组,每个元素代表在该位置可以跳跃的最大长度,求到达最后位置的最小跳跃次数。

贪心解法
def jump(nums): jumps = 0 current_end = 0 farthest = 0 for i in range(len(nums)-1): farthest = max(farthest, i + nums[i]) if i == current_end: jumps += 1 current_end = farthest return jumps

贪心思路:在每一步的覆盖范围内选择能跳最远的位置作为下一跳。

复杂度分析

  • 时间复杂度:O(n)
  • 空间复杂度:O(1)
动态规划解法
def jump(nums): n = len(nums) dp = [float('inf')] * n dp[0] = 0 for i in range(n): for j in range(1, nums[i] + 1): if i + j < n: dp[i + j] = min(dp[i + j], dp[i] + 1) return dp[-1]

DP思路:dp[i]表示到达位置i的最小跳跃次数,遍历每个位置更新能到达的位置。

复杂度分析

  • 时间复杂度:O(n²)
  • 空间复杂度:O(n)

对比分析

  • 贪心解法明显更优,利用了问题的特殊性质
  • DP解法会超时,仅作为理解问题使用

2.3 最大子数组和(LeetCode 53)

问题描述:给定整数数组,找到具有最大和的连续子数组。

贪心解法
def maxSubArray(nums): current_sum = max_sum = nums[0] for num in nums[1:]: current_sum = max(num, current_sum + num) max_sum = max(max_sum, current_sum) return max_sum

贪心思路:当前子数组和为负数时,从下一个元素重新开始计算。

复杂度分析

  • 时间复杂度:O(n)
  • 空间复杂度:O(1)
动态规划解法
def maxSubArray(nums): n = len(nums) dp = [0] * n dp[0] = nums[0] for i in range(1, n): dp[i] = max(nums[i], dp[i-1] + nums[i]) return max(dp)

DP思路:dp[i]表示以nums[i]结尾的最大子数组和。

复杂度分析

  • 时间复杂度:O(n)
  • 空间复杂度:O(n)(可优化到O(1))

对比分析

  • 两种解法本质相同,只是表述方式不同
  • 贪心版本更简洁,空间效率更高

3. 算法选择决策流程

如何判断一个问题该用贪心还是动态规划?以下是决策流程图:

开始 │ ├─ 问题可以分解为子问题 → 是 → 子问题重叠? → 是 → 使用动态规划 │ │ │ ↓ │ 否 → 具有贪心选择性质? → 是 → 使用贪心算法 │ │ │ ↓ │ 否 → 可能需要回溯或其他方法 │ └─ 问题不能分解为子问题 → 考虑其他算法(如排序、双指针等)

3.1 选择贪心算法的场景

  1. 活动选择问题:如无重叠区间、用最少数量的箭引爆气球
  2. 分配问题:如分发饼干、分发糖果
  3. 可以分解为独立步骤:如买卖股票II、柠檬水找零
  4. 覆盖问题:如跳跃游戏、监控二叉树

3.2 选择动态规划的场景

  1. 需要保存中间结果:如最长递增子序列、编辑距离
  2. 有多个约束条件:如背包问题、买卖股票含手续费
  3. 子问题重叠明显:如斐波那契数列、爬楼梯
  4. 需要回溯所有可能性:如通配符匹配、正则表达式匹配

4. 性能对比与优化

4.1 时间复杂度对比

题目贪心时间复杂度DP时间复杂度
买卖股票IIO(n)O(n)
跳跃游戏IIO(n)O(n²)
最大子数组和O(n)O(n)

4.2 空间复杂度对比

题目贪心空间复杂度DP空间复杂度
买卖股票IIO(1)O(n) → 可优化到O(1)
跳跃游戏IIO(1)O(n)
最大子数组和O(1)O(n) → 可优化到O(1)

4.3 优化技巧

贪心算法优化

  1. 预处理时进行合适的排序
  2. 使用双指针减少不必要的遍历
  3. 利用数学性质简化判断条件

动态规划优化

  1. 状态压缩(如滚动数组)
  2. 记忆化搜索替代DP表
  3. 分析状态转移的依赖关系,减少不必要的计算

5. 实战建议与常见误区

5.1 贪心算法的常见误区

  1. 盲目使用贪心:没有验证问题是否具有贪心选择性质
    • 反例:LeetCode 135.分发糖果必须用双向贪心
  2. 错误排序:贪心通常需要先排序,但错误的排序标准会导致错误
    • 示例:LeetCode 452.用最少数量的箭引爆气球需要按结束位置排序
  3. 忽略边界条件:如跳跃游戏中的初始条件处理

5.2 动态规划的常见误区

  1. 错误的状态定义:导致状态转移方程复杂或错误
    • 建议:从简单子问题开始,逐步扩展
  2. 不必要的状态存储:没有进行空间优化
    • 示例:斐波那契数列只需保存前两个状态
  3. 混淆遍历顺序:如背包问题中物品和容量的遍历顺序

5.3 混合使用的情况

有些问题可以结合贪心和动态规划:

  1. 贪心预处理+DP:先通过贪心减少问题规模
  2. DP+贪心优化:在DP状态转移中使用贪心策略选择最优子结构

示例:LeetCode 968.监控二叉树,使用贪心思想确定摄像头放置策略,但实现上采用树形DP。

http://www.jsqmd.com/news/1183464/

相关文章:

  • 2026西安雁塔区代理记账公司甄选完整指南,持证实体门店本地优选 - 速递信息
  • 【小程序毕业设计】智慧校园食堂餐饮点餐服务平台的设计与实现 基于 SpringBoot 的食堂菜品管理与在线点餐系统(源码+文档+远程调试,全bao定制等)
  • 亲身探访广州宝玑官方售后服务中心|完整热线电话与网点地址(2026年7月最新) - 亨得利钟表维修中心
  • 常州武进区养殖场想打井,怎么保证水质干净没污染?养殖场打井要注意什么? - 瑞溪泉水利
  • 2026 长葛黄金回收避坑指南:三家本土实体门店实地测评,本地变现透明交易全攻略 - 福金阁黄金回收
  • ENSP USG6000V NAT 配置:3种源NAT模式(no-pat/napt/easy-ip)实战对比
  • NumPy数组思维实战:绕过axis和广播陷阱的工程指南
  • 2026年上海澳洲留学申请时间规划指南:五家优选品牌深度解析 - 科技焦点
  • 不容错过的2026汕头婚纱摄影攻略:新人必看影楼选购指南 - 企业品牌
  • 数据血缘三层穿透:物理、逻辑与语义的全栈可追溯体系
  • zipgrep 与 zgrep 命令对比:Linux 下 2 种 zip 搜索方案性能与适用场景实测
  • 2026寄大件快递避坑:主流品牌优缺点深度评测,哪个性价比高? - 快递物流资讯
  • Krea2-Attention注入技术:基于ComfyUI实现精准风格迁移
  • 现代C++高性能编程:从语法安全到系统级优化的工程实践
  • Windows更新关闭全攻略:从原理到实践的完整解决方案
  • 2026找全自动吹塑机优质厂家可关注的几个实际场景 - 起跑123
  • 编译程序四元式生成:3种表达式(算术、布尔、赋值)的语义动作详解
  • eNSP USG5500 实验环境搭建:4设备组网与2种管理方式(CLI/Web)对比
  • SpringBoot @Async 异步任务与 @Transactional 事务实战
  • Frp vs Ngrok vs NPS:3款内网穿透工具在安全测试中的横向对比
  • 2026深圳电缆回收按吨还是按米划算?行业数据与正规选型全解析 - 再生资源回收资讯
  • 2026年07月 哈尔滨市悦源钢结构彩板有限公司——聚焦东北寒冷气候的钢结构与彩板定制化服务商解析 - 甄选服务推荐
  • Rancher 2.6.4 监控配置实战:3步创建 ServiceMonitor 监控 Java 应用 JMX 指标
  • 【大数据毕业设计】基于 SpringBoot + 小程序的运动数据记录与分析系统的设计与实现(源码+文档+远程调试,全bao定制等)
  • C++微服务通信:五种高效消息传递方案深度解析与实战选型
  • 图同构判定算法 C++ 实现:邻接矩阵全排列法 O(n²×n!) 复杂度分析
  • 2026年7月寄快递上门取件哪个比较便宜?热门平台深度测评与避坑指南 - 快递物流资讯
  • 2026年7月最新南通雅典官方售后客服服务电话及地址网点大全 - 亨得利官方服务中心
  • PlantUML 工具实战:5分钟自动生成UML类图与序列图
  • Skill 技能系统完全指南(五):实战 Skill——代码质量