一、 什么是贪心算法
贪心的本体是**"局部最优推导全局最优"。它假设世界(或问题空间)是可切分的、独立且具有单向因果律**的。
举个例子:
面前有10 堆麦粒,但有的麦粒大,有的麦粒小,有的还掺了沙子。
你手里有一个只能装一勺的袋子,而且你只能从 10 堆里选 3 堆各舀一勺。
你的目标是:让这三勺麦粒总重量最重。
你会怎么做?
肯定每一步都选那堆看起来最饱满、最沉的麦粒舀
当你舀第二勺的时候,第一勺的重量,会影响你判断"现在哪一堆最沉"吗?
不会。我只要看剩下的堆里哪堆最沉就行了。
这就是 贪心。
-
它的特点: 现在的选择(选最沉的),不依赖于过去的具体数值。
-
它的行为: 不需要纸笔记录历史,只需要一双盯着目标的眼睛。
-
它的本质: 只看当前的最优选择。
贪心算法和动态规划的区别是什么?
贪心算法
-
前提: 它假设"局部最优"一定能推导出"全局最优"。
-
逻辑: 我不管以后怎么办,我现在就要拿那个最大的。
动态规划
-
前提: 问题的最优解包含子问题的最优解(最优子结构),且子问题重叠。
-
逻辑: 虽然现在选这个看起来不错,但我要算一下,选了它之后,后面的路好不好走。
|-----------|----------------------------|------------------------------------|
| 维度 | 贪心算法 (Greedy) | 动态规划 (DP) |
| 选择方式 | 自顶向下。每一步直接做决定,不回头。 | 自底向上。先求子问题的解,再组合成大问题的解。 |
| 回溯/反悔 | 不可反悔。选了就定了。 | 穷举/比较。虽然不回溯,但它通过状态转移方程考虑了所有路径。 |
| 效率 | 极高。 | 较低 |
| 难点 | 证明难。你很难证明局部最优一定能到全局最优。 | 找状态方程难。如何定义 dp[i] 是最头疼的。 |
| 子问题关系 | 子问题是独立的。 | 子问题是重叠的(重复计算)。 |
解题步骤:
确定"贪心选择性质"
要给每一步 定一个评价标准
标准就是观察,如何能通过这个标准的局部积累得到结果
维护两个核心变量:
结果变量
存储最终结果
约束边界
何时停止
二、 经典题型解析
1.买卖股票的最佳时机
给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。
你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。
返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0 。

解题:
贪心选择性质
买入的标准: 到今天为止,我见过的最低价格是多少?
卖出的标准: 如果我今天卖掉,我能赚多少?
两个核心变量:
结果变量
到目前为止,我能赚到的最大利润。
约束边界
到今天为止,我见过的最低买入价格。
java
class Solution {
public int maxProfit(int[] prices) {
int minPrice = Integer.MAX_VALUE;
int maxProfit = 0;
for (int price : prices) {
if (price < minPrice) {
minPrice = price; // 更新最低价
} else if (price - minPrice > maxProfit) {
maxProfit = price - minPrice; // 更新最大利润
}
}
return maxProfit;
}
}
2.跳跃游戏
给你一个非负整数数组 nums ,你最初位于数组的 第一个下标 。数组中的每个元素代表你在该位置可以跳跃的最大长度。
判断你是否能够到达最后一个下标,如果可以,返回 true ;否则,返回 false 。

解题:
贪心选择性质
到i格为止,我能跳多远?
两个核心变量:
结果变量
到目前为止,我能跳的最远距离。
约束边界
如果当前的下标 i 已经大于了 能跳的最远距离,说明你之前的潜力已经耗尽了,你根本走不到这一格。
java
class Solution {
public boolean canJump(int[] nums) {
int maxReach = 0; // 记录当前能够到达的最远下标
int n = nums.length;
for (int i = 0; i < n; i++) {
// 如果当前位置已经超过了之前能跳到的最远距离,说明中间有 0 挡住了,无法到达
if (i > maxReach) {
return false;
}
// 在当前位置,尝试更新最远能跳到的位置
maxReach = Math.max(maxReach, i + nums[i]);
// 提前优化:如果最远距离已经能覆盖最后一个下标,直接返回 true
if (maxReach >= n - 1) {
return true;
}
}
return true;
}
}
3.跳跃游戏II
给定一个长度为 n 的 0 索引 整数数组 nums。初始位置在下标 0。
每个元素 nums[i] 表示从索引 i 向后跳转的最大长度。换句话说,如果你在索引 i 处,你可以跳转到任意 (i + j) 处:
0 <= j <= nums[i]且i + j < n
返回到达 n - 1 的最小跳跃次数。测试用例保证可以到达 n - 1。

解题:
贪心选择性质
当前这一跳 能覆盖的范围内,哪一个点能让我下一跳跳得最远?"
两个核心变量:
结果变量
真正跳出去的次数。
约束边界
能支撑你走到的最远下标。
java
class Solution {
public int jump(int[] nums) {
int n = nums.length;
// 如果数组只有一个元素,你已经在终点了,跳跃次数为 0
if (n < 2) return 0;
int jumps = 0; // 真正跳跃的次数
int currentEnd = 0; // 当前这一跳能达到的最远边界
int farthest = 0; // 在当前边界内,我们物色到的"下一跳"最远潜力
for (int i = 0; i < n - 1; i++) {
// 每一个落点,我们都看一眼它能跳多远
farthest = Math.max(farthest, i + nums[i]);
// 当我们走到了当前这一跳的尽头(有效期休完)
if (i == currentEnd) {
jumps++; // 没办法了,必须真正"跳"一下了
currentEnd = farthest; // 这一跳,直接把我们送到刚才物色到的最远点
// 提前退出:如果现在的潜力已经能摸到终点,就没必要再走下去了
if (currentEnd >= n - 1) {
break;
}
}
}
return jumps;
}
}
4.划分字母区间
给你一个字符串 s 。我们要把这个字符串划分为尽可能多的片段,同一字母最多出现在一个片段中。例如,字符串 "ababcc" 能够被分为 ["abab", "cc"],但类似 ["aba", "bcc"] 或 ["ab", "ab", "cc"] 的划分是非法的。
注意,划分结果需要满足:将所有划分结果按顺序连接,得到的字符串仍然是 s 。
返回一个表示每个字符串片段的长度的列表。

解题:
贪心选择性质
每个字符最后一次出现的下标。
两个核心变量:
结果变量
当当前的下标 i 刚好等于 end 时。
约束边界
这是当前片段不得不延伸到的最远位置。
java
class Solution {
public List<Integer> partitionLabels(String s) {
// 1. 预处理:记录每个字母最后出现的下标
int[] lastPos = new int[26];
for (int i = 0; i < s.length(); i++) {
lastPos[s.charAt(i) - 'a'] = i;
}
List<Integer> result = new ArrayList<>();
int start = 0; // 当前片段的起始位置
int end = 0; // 潜力变量:当前片段的最远边界
// 2. 线性扫描
for (int i = 0; i < s.length(); i++) {
// 更新潜力:我必须包含当前字符的最远位置
end = Math.max(end, lastPos[s.charAt(i) - 'a']);
// 3. 触发结算:当我走到了目前所有字符的最远边界
if (i == end) {
result.add(end - start + 1); // 记录片段长度
start = i + 1; // 更新下一个片段的起始点
}
}
return result;
}
}
贪心算法的局限与伟大
为什么贪心算法往往是错的?
因为现实世界往往是**高度耦合(Coupled)且具有非线反馈(Non-linear Feedback)**的:
-
全局相关性: 很多时候,为了全局最优,你必须在局部做出"次优"甚至"极差"的选择(比如下棋中的"弃子保大局")。
-
路径依赖: 贪心算法忽略了路径间的相互干扰。
为什么我们依然热爱贪心算法?
-
极高的效率: 相比于暴力搜索(穷举所有路径)或动态规划(记录所有状态),贪心算法通常是
O(n)O(n)或
O(nlogn)O(nlogn)的复杂度。它是用精度换取速度 的极致代表。
-
近似解的价值: 在很多 NP-hard 问题中,虽然贪心拿不到 100 分的完美解,但能以极低的成本拿到 80-90 分的近似解。
-
特殊的真理: 在某些特殊的数学结构中(如拟阵 Matroid ),局部最优确实严格等价于全局最优。
总结
贪心算法本质上是一种**"线性思维"的极致应用**。它假设世界是一个可以被拆解、叠加的机械系统,而不是一个相互纠缠、牵一发而动全身的复杂系统。当世界恰好符合这种"简单性"时,贪心算法就是神;当世界展现出复杂性时,贪心算法就是一种危险的短视。