目录
[1. 什么是贪心算法?](#1. 什么是贪心算法?)
[2. 适用条件](#2. 适用条件)
[3. 贪心算法步骤](#3. 贪心算法步骤)
[4. 优缺点](#4. 优缺点)
[5. LeetCode典型应用场景](#5. LeetCode典型应用场景)
[6. 贪心 vs 动态规划](#6. 贪心 vs 动态规划)
[1. 买卖股票的最佳时机(LeetCode 121)](#1. 买卖股票的最佳时机(LeetCode 121))
[2. 跳跃游戏(LeetCode 55)](#2. 跳跃游戏(LeetCode 55))
[3. 跳跃游戏 II(LeetCode 45)](#3. 跳跃游戏 II(LeetCode 45))
[过程分析(以[2,3,1,1,4]为例 - 贪心解法)](#过程分析(以[2,3,1,1,4]为例 - 贪心解法))
[4. 划分字母区间(LeetCode 763)](#4. 划分字母区间(LeetCode 763))
[1. 贪心算法共性](#1. 贪心算法共性)
[2. 为什么这些题适合贪心?](#2. 为什么这些题适合贪心?)
[3. 贪心 vs DP 选择指南](#3. 贪心 vs DP 选择指南)
[4. 建议](#4. 建议)
一、贪心算法核心理论
1. 什么是贪心算法?
贪心算法(Greedy Algorithm)是一种在每一步决策中都采取当前状态下最优的选择 ,期望通过局部最优解达到全局最优解的算法策略。它不从整体最优考虑,而是做出在当前看来最好的选择。
2. 适用条件
贪心算法成功应用需满足两个关键性质:
- 贪心选择性质:全局最优解可以通过局部最优选择得到
- 最优子结构:问题的最优解包含子问题的最优解
3. 贪心算法步骤
- 定义问题模型:明确输入输出和约束条件
- 确定贪心策略:设计局部最优选择规则
- 证明正确性:验证贪心选择能导致全局最优
- 实现算法:通常通过单次遍历完成
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 |
易错点/难点
- 初始值设置:minPrice应设为prices[0],maxProfit设为0
- 边界条件:空数组或单元素数组应返回0
- 利润计算时机:只有当当前价格大于minPrice时才计算利润
- 常见错误:试图用两个指针同时移动(会导致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时,应直接返回true
- 提前终止:当farthest≥n-1时可提前返回true
- 更新逻辑:farthest = max(farthest, i + nums[i])
- 常见错误 :
- 误用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 | - | - | - | - |
易错点/难点
- 边界条件:n=1时应返回0(无需跳跃)
- 计数时机:在到达currentEnd时才增加跳跃次数
- 提前终止:currentEnd≥n-1时可提前结束
- DP的局限性 :
- O(n²)复杂度在n=10^5时会超时
- 需要初始化dp数组为"不可达"值
- 常见错误 :
- 在循环内错误地增加跳跃次数
- 未正确更新currentEnd和nextEnd
- 混淆了索引和跳跃长度
关键结论:贪心解法是本题最优解,DP解法仅作对比,实际应用中应避免
4. 划分字母区间(LeetCode 763)
题目内容
给定一个字符串 s,将它划分为尽可能多的片段,使同一字母最多出现在一个片段中。返回一个表示每个片段长度的列表。
示例:
输入: "ababcbacadefegdehijhklij"
输出: [9,7,8]
解释: 划分结果为 "ababcbaca", "defegde", "hijhklij"
思路分析
- 贪心策略 :
- 预处理:记录每个字符最后出现的位置
- 遍历字符串:维护当前片段的起始位置和最远边界
- 当索引到达最远边界时,切割片段并重置起始位置
- 为什么贪心有效 :
- 每个片段必须覆盖所有已出现字符的最后位置
- 局部最优(刚好覆盖所有字符的最后位置)保证了片段数量最大化
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"为例)
-
预处理最后位置 :
- a: 3, b: 2, c: 5
-
动态划界 :
索引 字符 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]
易错点/难点
- 最后位置记录:必须遍历完整个字符串才能确定最后位置
- 片段长度计算 :
end - start + 1(包含两端) - 起始位置重置 :切割后start应设为
i+1,而非i - 边界条件 :
- 空字符串应返回空列表
- 单字符字符串应返回[1]
- 常见错误 :
- 误将
end - start作为长度(缺少+1) - 未正确更新end(未取max)
- 混淆了索引和长度
- 误将
关键优化:两次遍历即可解决,时间O(n),空间O(1)(26个字母的固定空间)
三、总结与对比
1. 贪心算法共性
| 问题 | 贪心策略 | 维护状态 | 核心思想 |
|---|---|---|---|
| 买卖股票 | 记录最小价格 | minPrice, maxProfit | "低买高卖" |
| 跳跃游戏 | 记录最远可达位置 | farthest | "能跳多远跳多远" |
| 跳跃游戏II | 维护当前/下一步边界 | currentEnd, nextEnd | "跳得最远" |
| 划分字母 | 记录字符最后位置 | start, end | "跟着最远家门走" |
2. 为什么这些题适合贪心?
- 局部最优能推全局最优 :
- 买卖股票:当前最小价格决定未来最大利润
- 跳跃游戏:当前最远位置决定是否可达
- 划分字母:字符最后位置决定片段边界
- 无后效性 :
- 之前的决策不影响后续选择(股票买卖只关心最小价)
- 不需要回溯(跳跃游戏只需维护最远位置)
3. 贪心 vs DP 选择指南
| 问题类型 | 适用算法 | 原因 |
|---|---|---|
| 需要全局最优解 | 贪心 | 满足贪心性质时效率更高 |
| 需要记录所有路径 | DP | 贪心无法回溯 |
| 状态空间大 | 贪心 | DP可能超时/超内存 |
| 问题有明确边界 | 贪心 | 如跳跃边界、字符边界 |
4. 建议
- 先尝试贪心:遇到最优化问题,先思考是否存在局部最优选择
- 验证贪心性质:通过小规模测试验证贪心策略是否有效
- 对比DP解法:理解为什么贪心更优(通常更高效)
- 掌握典型模式 :
- "维护最远边界":跳跃游戏、划分字母
- "记录历史最优":买卖股票
关键心得 :贪心算法的精髓在于找到正确的"局部最优"定义。这四道题都通过维护一个"边界"变量(最远位置、最小价格、片段边界)实现了高效的贪心解法。掌握这种思维模式,能解决大量类似的最优化问题。

2026.新年快乐!