一句话总结贪心:每一步都选当前看起来最好的,结果往往就是全局最优的。
但别误会:它不是“见钱眼开”,而是“精准拿捏”。
在力扣(LeetCode)的世界里,有一种算法风格,既不暴力、也不回溯,更不像动态规划那样烧脑——它叫 贪心算法(Greedy Algorithm) 。
听起来是不是有点“自私”?其实不然。贪心不是“占便宜”,而是在局部最优选择中,恰好走向了全局最优。今天,我们就用 18 道经典贪心题,带你从“懵圈”到“真香”!
🧠 什么是贪心?先来个生活比喻
想象你去自助餐厅吃饭:
- 暴力解法:把所有菜尝一遍,选出最满意的组合(累死)。
- 动态规划:提前画一张“菜品搭配收益表”,再决定吃啥(太卷)。
- 贪心策略:看到眼前最香的那道菜,先夹!反正胃就这么大,先吃最值得的。
这就是贪心:当下最优,就是未来最优(前提是问题具备“贪心选择性质”)。
🎯 贪心的核心思想
- 局部最优 → 全局最优(关键!不是所有问题都成立)
- 无后效性:一旦做出选择,就不会后悔
- 通常配合排序:让“好东西”排前面,方便我们“贪”
下面,我们按 场景分类 + 代码精讲 + 幽默解读,带你刷透贪心!
🍪 场景一:分发与匹配 —— “谁最饿,先喂谁?”
✅ 455. 分发饼干
孩子有胃口
g,饼干有尺寸s,一块饼干只能给一个孩子,目标是满足最多孩子。
贪心策略:
- 把最贪的孩子和最小的饼干配对?NO!
- 正确姿势:小胃口配小饼干,大胃口留大饼。
→ 排序后,从大到小尝试匹配。
var findContentChildren = function(g, s) {
g.sort((a, b) => a - b);
s.sort((a, b) => a - b);
let res = 0, index = s.length - 1;
for (let i = g.length - 1; i >= 0; i--) {
if (index >= 0 && s[index] >= g[i]) {
res++; index--;
}
}
return res;
};
💡 幽默解读:这不是“偏心”,这是“资源最大化利用”。毕竟,给大胃王一块小饼干,等于浪费!
✅ 2410. 运动员和训练师的最大匹配数
能力值 ≤ 训练能力值才能匹配,求最大匹配数。
思路:和分饼干几乎一样!排序后,强运动员优先匹配强训练师,避免“高射炮打蚊子”。
players.sort((a, b) => a - b);
trainers.sort((a, b) => a - b);
// 然后从后往前匹配
🤣 笑点:如果让985学霸去教小学生加减法,虽然能教,但不如让他去带竞赛班——这就是贪心的“人才调度哲学”!
💰 场景二:交易与利润 —— “低买高卖,落袋为安”
✅ 122. 买卖股票的最佳时机 II
可以无限次买卖,求最大利润。
贪心妙招:只要明天比今天贵,今天就买,明天就卖!
var maxProfit = function(prices) {
let res = 0;
for (let i = 1; i < prices.length; i++) {
res += Math.max(prices[i] - prices[i-1], 0); // 赚钱就加,亏钱就0
}
return res;
};
📈 现实映射:这不就是“每天做T”?可惜现实中手续费会哭晕在厕所……
🎈 场景三:区间问题 —— “重叠?合并!气球?一箭穿心!”
✅ 452. 用最少数量的箭引爆气球
气球是区间
[x_start, x_end],一支箭可垂直射穿所有覆盖该 x 的气球。
贪心策略:
- 按左端点排序
- 尽量让箭射在当前气球的最右边界
- 如果下一个气球的左端 > 当前右边界 → 需要新箭
points.sort((a, b) => a[0] - b[0]);
let result = 1;
for (let i = 1; i < points.length; i++) {
if (points[i][0] > points[i-1][1]) {
result++;
} else {
points[i][1] = Math.min(points[i-1][1], points[i][1]); // 缩小右边界
}
}
🏹 画面感:就像你在玩“愤怒的小鸟”,一箭穿俩才是高手!
✅ 435. 无重叠区间
移除最少区间,使剩下的不重叠。
反向思考:保留最多不重叠区间 → 就是经典的“活动选择问题”。
策略:按右端点升序,越早结束的活动越优先选!
intervals.sort((a, b) => a[1] - b[1]); // 注意!按 end 排序
let end = intervals[0][1], count = 0;
for (let i = 1; i < intervals.length; i++) {
if (intervals[i][0] < end) count++; // 重叠,删掉
else end = intervals[i][1]; // 不重叠,更新 end
}
return count;
⏰ 职场启示:开会时间短的先开,效率最高!拖沓的会议?直接砍掉!
👥 场景四:排队与重建 —— “身高高的先站,矮的插队”
✅ 406. 根据身高重建队列
people[i] = [h_i, k_i]表示前面有k_i个 ≥ 自己身高的人。
神操作:
- 身高降序(高的先排)
- k 升序(同样高,k 小的先排)
- 然后按
k值插入队列
people.sort((a, b) => b[0] !== a[0] ? b[0] - a[0] : a[1] - b[1]);
let queue = [];
for (let p of people) {
queue.splice(p[1], 0, p); // 在 k 位置插入
}
🧍♂️🧍♀️ 场景还原:想象军训排队——先让1米9的站好,1米7的再根据“前面有几个比我高”来插队,完美!
🍋 场景五:找零与模拟 —— “收钱容易,找零难!”
✅ 860. 柠檬水找零
顾客付 5/10/20,一杯5元,能否给所有人找零?
贪心规则:
- 收5:开心存着
- 收10:必须找5
- 收20:优先找 10+5,其次找 5+5+5(因为10只能用于20找零,5更万能)
if (bill === 20) {
if (tenCount > 0 && fiveCount > 0) { ten--; five--; }
else if (fiveCount >= 3) { five -= 3; }
else return false;
}
💵 老板心声:“我宁愿收三个5块,也不想收一张20!”——因为20最难找零啊!
🔢 场景六:数字与字符串 —— “不够?那就变9!”
✅ 738. 单调递增的数字
找 ≤ n 的最大单调递增数字(如 332 → 299)
贪心技巧:
- 从右往左扫描,找到第一个
n[i-1] > n[i] n[i-1]--,后面全变9
for (let i = n.length - 1; i > 0; i--) {
if (n[i-1] > n[i]) {
n[i-1]--;
flag = i; // 标记从哪开始变9
}
}
for (let i = flag; i < n.length; i++) n[i] = '9';
🎯 精髓:宁可前面小一点,也要后面全是9!比如 332 → 299 比 329 大多了。
🌳 场景七:树与监控 —— “摄像头放哪最省?”
✅ 968. 监控二叉树
每个摄像头可监控父、自己、子,求最少摄像头数。
状态定义(后序遍历) :
0:该节点未被覆盖1:该节点有摄像头2:该节点已被覆盖
贪心策略:尽量在叶子节点的父节点放摄像头(覆盖三层)!
if (left === 0 || right === 0) { res++; return 1; } // 孩子没覆盖,我得装
if (left === 1 || right === 1) return 2; // 孩子有摄像头,我被覆盖
return 0; // 孩子都被覆盖,我不管
📹 家庭智慧:别在自己房间装监控,装在客厅,全家都能看!
🏁 其他经典贪心题速览
| 题号 | 题目 | 贪心点 |
|---|---|---|
| 53 | 最大子数组和 | 当前和 < 0 就扔掉,重新开始 |
| 55 | 跳跃游戏 | 维护最远可达位置 |
| 45 | 跳跃游戏 II | 每跳一次,更新下一次最远距离 |
| 134 | 加油站 | 总油 ≥ 总耗才可能;从“不可能起点”之后开始 |
| 135 | 分发糖果 | 两次遍历:左→右,右→左,取 max |
| 376 | 摆动序列 | 只记录“峰”和“谷”的数量 |
| 763 | 划分字母区间 | 记录每个字母最后出现位置,扩展右边界 |
✅ 贪心算法使用 checklist
- 问题是否具有最优子结构?
- 局部最优是否能导致全局最优?
- 是否可通过排序/优先队列/双指针等简化选择?
- 有没有反例?(比如某些背包问题就不能贪心)
❗记住:贪心很高效,但不万能。用之前,先证明!
🌈 结语:贪心,是一种生活智慧
贪心算法教会我们的,不仅是解题技巧,更是一种 “抓大放小、聚焦关键” 的思维方式。
- 面对选择时,先做最有价值的事
- 资源有限时,优先满足核心需求
- 遇到冲突时,用最小代价换取最大收益
所以,下次有人说你“贪心”,你可以微微一笑:“不,我只是在用贪心算法优化人生。”