LeetCode121/55/45/763 贪心算法理论与经典题解析

目录

一、贪心算法核心理论

[1. 什么是贪心算法?](#1. 什么是贪心算法?)

[2. 适用条件](#2. 适用条件)

[3. 贪心算法步骤](#3. 贪心算法步骤)

[4. 优缺点](#4. 优缺点)

[5. LeetCode典型应用场景](#5. LeetCode典型应用场景)

[6. 贪心 vs 动态规划](#6. 贪心 vs 动态规划)

二、四道经典题详细解析

[1. 买卖股票的最佳时机(LeetCode 121)](#1. 买卖股票的最佳时机(LeetCode 121))

题目内容

思路分析

Java实现

过程分析(以[7,1,5,3,6,4]为例)

易错点/难点

[2. 跳跃游戏(LeetCode 55)](#2. 跳跃游戏(LeetCode 55))

题目内容

思路分析

Java实现

过程分析(以[2,3,1,1,4]为例)

易错点/难点

[3. 跳跃游戏 II(LeetCode 45)](#3. 跳跃游戏 II(LeetCode 45))

题目内容

思路分析

方法一:贪心算法(推荐)

方法二:动态规划(不推荐)

Java实现

贪心解法(O(n)时间)

DP解法(O(n²)时间,仅作对比)

[过程分析(以[2,3,1,1,4]为例 - 贪心解法)](#过程分析(以[2,3,1,1,4]为例 - 贪心解法))

易错点/难点

[4. 划分字母区间(LeetCode 763)](#4. 划分字母区间(LeetCode 763))

题目内容

思路分析

Java实现

过程分析(以"ababcc"为例)

易错点/难点

三、总结与对比

[1. 贪心算法共性](#1. 贪心算法共性)

[2. 为什么这些题适合贪心?](#2. 为什么这些题适合贪心?)

[3. 贪心 vs DP 选择指南](#3. 贪心 vs DP 选择指南)

[4. 建议](#4. 建议)


一、贪心算法核心理论

1. 什么是贪心算法?

贪心算法(Greedy Algorithm)是一种在每一步决策中都采取当前状态下最优的选择 ,期望通过局部最优解达到全局最优解的算法策略。它不从整体最优考虑,而是做出在当前看来最好的选择。

2. 适用条件

贪心算法成功应用需满足两个关键性质:

  • 贪心选择性质:全局最优解可以通过局部最优选择得到
  • 最优子结构:问题的最优解包含子问题的最优解

3. 贪心算法步骤

  1. 定义问题模型:明确输入输出和约束条件
  2. 确定贪心策略:设计局部最优选择规则
  3. 证明正确性:验证贪心选择能导致全局最优
  4. 实现算法:通常通过单次遍历完成

4. 优缺点

优点 缺点
时间复杂度低(通常O(n)) 不一定得到全局最优解
空间复杂度低 需要严格证明正确性
代码简洁易实现 问题需满足贪心性质

5. LeetCode典型应用场景

  • 区间问题:活动选择、区间合并
  • 跳跃问题:跳跃游戏系列
  • 字符串划分:划分字母区间
  • 股票买卖:最佳买卖时机
  • 背包问题:分数背包问题

6. 贪心 vs 动态规划

特性 贪心算法 动态规划
决策方式 每步做局部最优选择 考虑所有可能选择
时间复杂度 通常O(n) 通常O(n²)或更高
空间复杂度 通常O(1) 通常O(n)
正确性证明 需要证明贪心性质 通过状态转移方程保证
适用场景 问题满足贪心选择性质 问题具有最优子结构

关键区别:贪心算法"只看眼前",动态规划"考虑历史"。当问题满足贪心性质时,贪心算法是更优选择。


二、四道经典题详细解析

1. 买卖股票的最佳时机(LeetCode 121)

题目内容

给定一个整数数组 prices,其中 prices[i] 表示某只股票第 i 天的价格。你只能选择某一天买入 这只股票,并选择在未来的某一个不同的日子卖出 该股票,设计一个算法来计算你所能获取的最大利润。不能在买入前卖出股票

示例

输入: [7,1,5,3,6,4]

输出: 5

解释: 在第2天(价格=1)买入,在第5天(价格=6)卖出,最大利润 = 6-1 = 5

思路分析
  • 贪心策略:遍历数组时,始终记录当前遇到的最小价格(minPrice),并计算当前价格与minPrice的差值(利润),更新最大利润
  • 为什么贪心有效
    • 买入点必须在卖出点之前
    • 每次遇到更小的价格,都可能是未来最大利润的起点
    • 局部最优(当前最小买入价)能导致全局最优(最大利润)
Java实现
java 复制代码
class Solution {

    public int maxProfit(int[] prices) {

        if (prices == null || prices.length < 2) return 0;

        

        int minPrice = prices[0]; // 初始化最小价格

        int maxProfit = 0;        // 初始化最大利润

        

        for (int i = 1; i < prices.length; i++) {

            // 更新最小价格

            if (prices[i] < minPrice) {

                minPrice = prices[i];

            } 

            // 计算当前利润并更新最大利润

            else {

                int profit = prices[i] - minPrice;

                if (profit > maxProfit) {

                    maxProfit = profit;

                }

            }

        }

        return maxProfit;

    }

}
过程分析(以[7,1,5,3,6,4]为例)
索引 价格 minPrice 利润计算 maxProfit
0 7 7 - 0
1 1 1 - 0
2 5 1 5-1=4 4
3 3 1 3-1=2 4
4 6 1 6-1=5 5
5 4 1 4-1=3 5
易错点/难点
  1. 初始值设置:minPrice应设为prices[0],maxProfit设为0
  2. 边界条件:空数组或单元素数组应返回0
  3. 利润计算时机:只有当当前价格大于minPrice时才计算利润
  4. 常见错误:试图用两个指针同时移动(会导致O(n²)复杂度)

关键优化:单次遍历即可完成,不需要额外空间,时间复杂度O(n),空间复杂度O(1)


2. 跳跃游戏(LeetCode 55)

题目内容

给定一个非负整数数组 nums,你最初位于数组的第一个下标。数组中的每个元素代表你在该位置可以跳跃的最大长度。判断你是否能够到达最后一个下标。

示例

输入: [2,3,1,1,4]

输出: true

解释: 可以先跳 1 步,从下标 0 到达下标 1,然后从下标 1 跳 3 步到达最后一个下标

思路分析
  • 贪心策略:维护当前能到达的最远位置(farthest),遍历过程中更新该值
  • 为什么贪心有效
    • 如果在某点i,i已经超过了当前能到达的最远位置,则无法继续前进
    • 每次更新最远位置时,都考虑了从0到i所有位置能到达的最远点
    • 局部最优(当前最远可达位置)能决定全局是否可达
Java实现
java 复制代码
class Solution {

    public boolean canJump(int[] nums) {

        int n = nums.length;

        int farthest = 0; // 当前能到达的最远位置

        

        for (int i = 0; i < n; i++) {

            // 如果当前位置已经超过当前最远可达位置,无法继续前进

            if (i > farthest) {

                return false;

            }

            // 更新最远可达位置

            farthest = Math.max(farthest, i + nums[i]);

            // 提前终止:如果已能到达最后一个位置

            if (farthest >= n - 1) {

                return true;

            }

        }

        return farthest >= n - 1;

    }

}
过程分析(以[2,3,1,1,4]为例)
索引 nums[i] farthest 检查i > farthest 是否提前终止
0 2 0+2=2 0 > 0? 否
1 3 max(2,1+3)=4 1 > 2? 否 (4≥4)
易错点/难点
  1. 边界条件:当数组长度为1时,应直接返回true
  2. 提前终止:当farthest≥n-1时可提前返回true
  3. 更新逻辑:farthest = max(farthest, i + nums[i])
  4. 常见错误
    • 误用DP导致O(n²)复杂度
    • 未处理空数组或单元素数组
    • 错误地在循环内返回true(应先更新farthest)

关键点:不需要实际模拟跳跃路径,只需维护"最远可达位置"即可


3. 跳跃游戏 II(LeetCode 45)

题目内容

给定一个长度为 n 的 0 索引整数数组 nums。初始位置在下标 0。每个元素 nums[i] 表示从索引 i 向后跳转的最大长度。返回到达 n-1 的最小跳跃次数。

示例

输入: [2,3,1,1,4]

输出: 2

解释: 从下标0跳到下标1,再跳3步到达最后一个位置

思路分析
方法一:贪心算法(推荐)
  • 贪心策略
    • currentEnd:当前覆盖的最远边界
    • nextEnd:下一步能覆盖的最远边界
    • jumps:跳跃次数
    • 当遍历到currentEnd时,必须跳跃,将currentEnd更新为nextEnd
  • 为什么贪心有效
    • 每次跳跃都选择能覆盖最远距离的路径
    • 局部最优(每次跳跃覆盖最远)导致全局最优(最小跳跃次数)
方法二:动态规划(不推荐)
  • DP策略
    • dp[i]表示到达位置i的最小跳跃次数
    • 状态转移:dp[j] = min(dp[j], dp[i] + 1),当i + nums[i] >= j
  • 缺点
    • 时间复杂度O(n²),在n较大时会超时
    • 空间复杂度O(n)
Java实现
贪心解法(O(n)时间)
java 复制代码
class Solution {

    public int jump(int[] nums) {

        int n = nums.length;

        if (n == 1) return 0; // 边界:只有一个元素

        

        int jumps = 0;        // 跳跃次数

        int currentEnd = 0;   // 当前覆盖的最远位置

        int nextEnd = 0;      // 下一步能覆盖的最远位置

        

        for (int i = 0; i < n - 1; i++) {

            nextEnd = Math.max(nextEnd, i + nums[i]);

            // 到达当前覆盖边界,必须跳跃

            if (i == currentEnd) {

                jumps++;

                currentEnd = nextEnd;

                // 提前终止:如果已能到达最后一个位置

                if (currentEnd >= n - 1) break;

            }

        }

        return jumps;

    }

}
DP解法(O(n²)时间,仅作对比)
java 复制代码
class Solution {

    public int jump(int[] nums) {

        int n = nums.length;

        int[] dp = new int[n];

        Arrays.fill(dp, n + 1); // 用n+1表示不可达

        

        dp[0] = 0;

        for (int j = 1; j < n; j++) {

            for (int k = 0; k < j; k++) {

                if (dp[k] < n + 1 && k + nums[k] >= j) {

                    dp[j] = Math.min(dp[j], dp[k] + 1);

                }

            }

        }

        return dp[n - 1];

    }

}
过程分析(以[2,3,1,1,4]为例 - 贪心解法)
索引 nums[i] nextEnd i == currentEnd? jumps currentEnd
0 2 2 是(初始) 1 2
1 3 max(2,4)=4 1 2
2 1 4 2 4
3 1 4 2 4
4 4 - - - -
易错点/难点
  1. 边界条件:n=1时应返回0(无需跳跃)
  2. 计数时机:在到达currentEnd时才增加跳跃次数
  3. 提前终止:currentEnd≥n-1时可提前结束
  4. DP的局限性
    • O(n²)复杂度在n=10^5时会超时
    • 需要初始化dp数组为"不可达"值
  5. 常见错误
    • 在循环内错误地增加跳跃次数
    • 未正确更新currentEnd和nextEnd
    • 混淆了索引和跳跃长度

关键结论:贪心解法是本题最优解,DP解法仅作对比,实际应用中应避免


4. 划分字母区间(LeetCode 763)

题目内容

给定一个字符串 s,将它划分为尽可能多的片段,使同一字母最多出现在一个片段中。返回一个表示每个片段长度的列表。

示例

输入: "ababcbacadefegdehijhklij"

输出: [9,7,8]

解释: 划分结果为 "ababcbaca", "defegde", "hijhklij"

思路分析
  • 贪心策略
    1. 预处理:记录每个字符最后出现的位置
    2. 遍历字符串:维护当前片段的起始位置和最远边界
    3. 当索引到达最远边界时,切割片段并重置起始位置
  • 为什么贪心有效
    • 每个片段必须覆盖所有已出现字符的最后位置
    • 局部最优(刚好覆盖所有字符的最后位置)保证了片段数量最大化
Java实现
java 复制代码
class Solution {

    public List<Integer> partitionLabels(String s) {

        // 1. 记录每个字符的最后出现位置

        int[] last = new int[26];

        for (int i = 0; i < s.length(); i++) {

            last[s.charAt(i) - 'a'] = i;

        }

        

        // 2. 动态划边界

        List<Integer> result = new ArrayList<>();

        int start = 0;      // 当前片段起始位置

        int end = 0;        // 当前片段最远边界

        

        for (int i = 0; i < s.length(); i++) {

            end = Math.max(end, last[s.charAt(i) - 'a']);

            // 到达当前片段边界,切割

            if (i == end) {

                result.add(end - start + 1);

                start = i + 1; // 下一段起始位置

            }

        }

        return result;

    }

}
过程分析(以"ababcc"为例)
  1. 预处理最后位置

    • a: 3, b: 2, c: 5
  2. 动态划界

    索引 字符 last[char] end i==end? 操作
    0 a 3 3 -
    1 b 2 3 -
    2 a 3 3 -
    3 b 2 3 添加长度4,start=4
    4 c 5 5 -
    5 c 5 5 添加长度2

    结果:[4,2]

易错点/难点
  1. 最后位置记录:必须遍历完整个字符串才能确定最后位置
  2. 片段长度计算end - start + 1(包含两端)
  3. 起始位置重置 :切割后start应设为i+1,而非i
  4. 边界条件
    • 空字符串应返回空列表
    • 单字符字符串应返回[1]
  5. 常见错误
    • 误将end - start作为长度(缺少+1)
    • 未正确更新end(未取max)
    • 混淆了索引和长度

关键优化:两次遍历即可解决,时间O(n),空间O(1)(26个字母的固定空间)


三、总结与对比

1. 贪心算法共性

问题 贪心策略 维护状态 核心思想
买卖股票 记录最小价格 minPrice, maxProfit "低买高卖"
跳跃游戏 记录最远可达位置 farthest "能跳多远跳多远"
跳跃游戏II 维护当前/下一步边界 currentEnd, nextEnd "跳得最远"
划分字母 记录字符最后位置 start, end "跟着最远家门走"

2. 为什么这些题适合贪心?

  1. 局部最优能推全局最优
    • 买卖股票:当前最小价格决定未来最大利润
    • 跳跃游戏:当前最远位置决定是否可达
    • 划分字母:字符最后位置决定片段边界
  2. 无后效性
    • 之前的决策不影响后续选择(股票买卖只关心最小价)
    • 不需要回溯(跳跃游戏只需维护最远位置)

3. 贪心 vs DP 选择指南

问题类型 适用算法 原因
需要全局最优解 贪心 满足贪心性质时效率更高
需要记录所有路径 DP 贪心无法回溯
状态空间大 贪心 DP可能超时/超内存
问题有明确边界 贪心 如跳跃边界、字符边界

4. 建议

  1. 先尝试贪心:遇到最优化问题,先思考是否存在局部最优选择
  2. 验证贪心性质:通过小规模测试验证贪心策略是否有效
  3. 对比DP解法:理解为什么贪心更优(通常更高效)
  4. 掌握典型模式
    • "维护最远边界":跳跃游戏、划分字母
    • "记录历史最优":买卖股票

关键心得 :贪心算法的精髓在于找到正确的"局部最优"定义。这四道题都通过维护一个"边界"变量(最远位置、最小价格、片段边界)实现了高效的贪心解法。掌握这种思维模式,能解决大量类似的最优化问题。

2026.新年快乐!

相关推荐
墨白曦煜1 天前
Lombok 速查指南:从基础注解到避坑实录
java
ss2731 天前
线程安全三剑客:无状态、加锁与CAS
java·jvm·数据库
The Sheep 20231 天前
可视化命中测试
java·服务器·前端
梭七y1 天前
【力扣hot100题】(119)搜索二维矩阵 II
算法·leetcode·矩阵
闻缺陷则喜何志丹1 天前
【二分 寻找尾端】P7971 [KSN2021] Colouring Balls|普及+
c++·算法·二分·洛谷·寻找首端
漫随流水1 天前
leetcode算法(150.逆波兰表达式求值)
数据结构·算法·leetcode·
小小工匠1 天前
Vibe Coding - Claude Code 做 Java 项目 AI 结对编程最佳实践
java·结对编程·claude code
源码获取_wx:Fegn08951 天前
基于springboot + vue酒店预约系统
java·vue.js·spring boot·后端·spring
__万波__1 天前
二十三种设计模式(十九)--备忘录模式
java·设计模式·备忘录模式