讲解
贪心算法每一步都取当前看起来最好的选择,指望局部最优的累积正好就是全局最优。它与 DP 的分水岭在于:DP 保留所有可能性、最后才拍板(枚举最后一次决策),贪心每一步就拍板、永不回头——所以贪心更快(通常 O(n log n),瓶颈常在排序),但只对满足「贪心选择性质」的问题正确。用错的贪心是安静的错误:小测试全过、特定构造的数据才翻车。
贪心的难点不在代码(往往十几行),而在证明「为什么局部最优足够」。两个常用论证工具。交换论证:假设存在一个最优解与贪心解不同,证明把它的某一步换成贪心选择不会变差,逐交换下去最优解就变成了贪心解——分发饼干(最小胃口配最小能满足它的饼干)用它最顺手。反证 + 区间端点论证:区间调度选「结束最早」的区间,因为选结束晚的只会压缩后续空间,不可能更优。面试中不要求形式化证明,但主动给出一句直觉论证(「因为……所以换掉它不会更差」)是高分表现。
三道招牌菜定型手感。分发饼干:双排序 + 双指针,小饼干先满足小胃口,一次扫描。跳跃游戏:不模拟每一步怎么跳,只维护「目前能到达的最远下标」,遍历中持续刷新——当前位置超出最远可达即失败。无重叠区间:按右端点排序,贪心保留结束最早的,被丢弃的就是答案——它和「会议室安排」「射气球」是同构家族,识别信号是「区间 + 最多保留 / 最少移除」。
示例
分发饼干、跳跃游戏、无重叠区间:
import assert from 'node:assert/strict';
function findContentChildren(g, s) {
g.sort((a, b) => a - b); // 胃口从小到大
s.sort((a, b) => a - b); // 饼干从小到大
let child = 0;
for (const cookie of s) {
if (child < g.length && cookie >= g[child]) child++; // 最小能满足的优先
}
return child;
}
function canJump(nums) {
let farthest = 0;
for (let i = 0; i < nums.length; i++) {
if (i > farthest) return false; // 当前位置不可达
farthest = Math.max(farthest, i + nums[i]);
if (farthest >= nums.length - 1) return true;
}
return true;
}
// 按右端点排序,保留结束最早的,其余移除
function eraseOverlapIntervals(intervals) {
intervals.sort((a, b) => a[1] - b[1]);
let kept = 0;
let end = -Infinity;
for (const [start, finish] of intervals) {
if (start >= end) {
kept++;
end = finish;
}
}
return intervals.length - kept;
}
assert.strictEqual(findContentChildren([1, 2, 3], [1, 1]), 1);
assert.strictEqual(findContentChildren([1, 2], [1, 2, 3]), 2);
assert.strictEqual(canJump([2, 3, 1, 1, 4]), true);
assert.strictEqual(canJump([3, 2, 1, 0, 4]), false);
assert.strictEqual(canJump([0]), true);
assert.strictEqual(
eraseOverlapIntervals([
[1, 2],
[2, 3],
[3, 4],
[1, 3],
]),
1,
);
assert.strictEqual(
eraseOverlapIntervals([
[1, 2],
[1, 2],
[1, 2],
]),
2,
);
console.log('贪心示例全部通过');
canJump 的「最远可达」视角值得回味:它不关心具体跳法(那是 DP 或回溯的思路),只维护一个标量——信息被压缩到极致,复杂度降到 O(n)。贪心解题常伴随这种「状态极简」的美感。
常见坑
- 凭直觉上贪心不验证:「看起来对」的贪心策略要用反例压力测试;能用 DP 解且数据规模允许时,贪心至少要有一个论证或大量对拍。
- 排序键选错:区间调度按右端点排,按左端点或长度排都有反例;写之前先想「哪个属性决定后续空间」。
- sort 不传比较函数:复习排序一章的坑——JS 默认按字符串排,数字数组必须 (a, b) => a - b。
- 跳跃游戏里模拟具体路径:逐跳模拟既慢又易错;维护最远可达下标即可,不要追踪路径。
- 边界忘记单元素:[0] 可达(已在终点)、[0, 1] 不可达——这类题首尾边界各测一例。
小结
贪心 = 每步局部最优且不回头;只对有贪心选择性质的问题成立;交换论证和端点论证是两个证明工具;代码常是排序 + 一次扫描。下一章进入位运算的紧凑世界。