周末的限时自助餐厅里,小周端着托盘站在取餐台前。厨师告诉他:现在 90 分钟,托盘大小固定,拿了就不能反悔。小周没有翻菜谱、没有规划每一口,而是每次绕场一圈,挑眼前看起来最值得的一盘,拿完就继续走向下一个窗口。这种“现在看着最好就选它,不回头、不重来”的决策方式,就是贪心算法。 贪心算法的英文是 Greedy,算法世界里的“贪心侠”。它和穷举、动态规划最大的不同,是不做全局推演,只靠一条清晰规则步步递进。可问题是:凭什么“每次选当下最好”最后就能得到全局最好?如果能回答这个问题,你就在面试里占了先机。接下来的内容里,我会用小周吃自助餐的故事,把贪心算法的直觉、证明、代码框架、经典题型和面试避坑讲清楚。
掌握贪心算法,就等于掌握算法面试中最常见的一类“秒杀题”套路:先判断性质,再排序扫描,快速写出高效解。这是 AI 工程师算法笔试中最容易拿分的板块之一。
目录
可视化演示
展示贪心算法局部最优选择过程,活动安排经典问题求解,以及最优子结构性质的递推验证。
本节配有交互动效(进入视口后自动加载,约 3KB)。
贪心选择性质 每步选择当前最优解,不考虑全局后果 贪心≠最优 贪心策略在满足贪心选择性质时可得全局最优
贪心算法直觉:先拿最顺眼的菜
小周走进自助餐厅时,其实根本没想好 90 分钟里要吃什么。他的规则很简单:每轮绕场一圈,从眼前还没拿过的菜里,挑“当下最想吃的”那一盘。拿完就走,绝不回头,也不因后面出现了更好的菜而后悔。这就是贪心算法最核心的直觉:用当前可见的最优选择,代替对未来的全盘规划。
在算法里,这道“最顺眼的菜”通常用一个评价函数来量化。比如做区间调度时,我们评价“结束时间最早”;做跳跃游戏时,我们评价“能到达的最远位置”。每一轮都在候选集里选出这个评价最高的元素,然后更新状态,重复执行。贪心不像暴力搜索那样试遍所有路径,它只走一条不回头的主线。
这个策略听起来很“莽”,但它恰恰解决了一大批面试题。许多贪心题的第一步是排序:把候选元素按某个键排好,再从头到尾扫一遍。排序的目的,就是让“每次选当前最优”变成“当前指针指向的位置天然就是最优”。
小周之所以敢这么吃,是因为在这家餐厅,先拿哪道菜不会影响后面菜品的出现顺序,也不会占用额外资源。真实题目也一样:如果“当前拿了什么”会影响后续决策,贪心就未必成立;如果影响不大,贪心往往就是最快的答案。
- 候选集合 \(C_t\)这是第 \(t\) 步时“还没被拿走过”的全部选择,相当于自助餐里还没取过的菜。
- 评价函数 \(v(a)\)给每个候选元素打分。分数越高,代表“当下越想要”。
- \(\arg\max\) 操作在所有候选中找出让分数最高的那个元素,而不是把最高分本身取出来。
- 结果 \(a_t^{\ast}\)第 \(t\) 步真正被选走的最优候选,也就是小周这一轮端走的盘子。
两大支柱性质:为什么这顿饭能吃饱
小周试了几次发现,光靠“当下喜欢”还不够,除非餐厅有两个隐藏机制:第一,某道菜一定出现在某种最优吃法中;第二,拿走这道菜后,剩下的问题依然是“从剩余菜里选最优”,子问题和原问题长得一模一样。在算法里,这两个机制就是贪心选择性质和最优子结构。
以“会议室安排”为例:有若干会议,每个会议有开始时间 \(a_i\) 和结束时间 \(b_i\),要选出最多互不冲突的会议。贪心策略是每次选结束时间最早的会议。为什么这样对?因为结束时间最早,就给后面的会议留下了最大的剩余空间。如果某个最优解第一个选的是会议 \(j\),而贪心选的会议 \(g\) 结束更早,那把 \(j\) 换成 \(g\) 后,后面所有会议依然可以原样安排,不会变得更差。
这种“把最优解里的元素换成贪心元素”的操作,叫做交换论证。交换后解不劣,就说明贪心选择是有资格进入某个最优解的。接着再用数学归纳法或反证法补完,就构成了完整的正确性证明。这部分面试中不一定要求手写全证明,但至少要说得出思路。
有了这两根支柱,贪心就不是“赌运气”,而是“有证明的快速算法”。面试时如果能主动说出“这题满足贪心选择性质和最优子结构”,就已经比大多数候选人高一个段位。
- \(f_i\) 表示结束时间每个候选区间 \(i\) 都有自己的结束边界,类比每道菜的“下线时间”。
- \(\min_{i\in I} f_i\)在整个候选集合里取结束时间最小的那个区间,也就是“最早退场的菜”。
- \(g\) 表示贪心选中的元素它因为结束最早而被选中,成为当前这一步的策略答案。
- \(\exists\, O^{}g\in O^{}\):数学上严谨地说,“存在”一个最优解包含 \(g\)。这给了贪心继续走下去的资格证。
实现框架复杂度:先排队再打饭
小周后来总结出自己吃自助餐的标准动作:第一步,先绕着餐厅走一圈,把所有菜按“心动程度”从高到低排个序;第二步,从心动值最高的菜开始,一盘一盘看,能拿就拿;第三步,每拿一盘,更新一下托盘剩余空间。这就是贪心算法的三段式框架:排序、循环、更新。
大部分贪心题的实现代码都极短,往往不超过二十行。真正决定效率的,常常是排序这一步。假设候选集合大小为 \(n\),排序的代价是 \(O(n\log n)\),之后只需线性扫描一遍。更新状态的代价通常是常数或对数级。因此,贪心算法的总复杂度主要由排序决定。
面试里写代码之前,先说出复杂度,会让答案显得更完整。通常说完 \(O(n\log n)\) 之后,还要补一句“空间复杂度是 \(O(n)\),因为要存结果或临时列表”。这样面试官就知道你不只会背模板,还清楚每一部分的开销。
其实很多贪心题在排序之前毫无头绪,一旦排序键想清楚了,代码就是顺水推舟。小周也发现,提前“排队”能让他不再纠结:菜已经按心动值排好,直接照着顺序拿就行。
- 排序耗时 \(O(n\log n)\)把候选集合按排序键排列,这是贪心题里最常见的预处理。
- 扫描耗时 \(O(n)\)排好序后,从头到尾检查每个候选是否可行,总体是线性开销。
- 总耗时 \(T(n)\)两部分相加,保留增长最快的量级,得到最终复杂度。
- 最终化简因为 \(n\log n\) 比 \(n\) 增长更快,所以舍去线性项。
区间调度实战:让会议室高效转起来
有一类面试题会让小周特别熟练:公司只有一个会议室,各部门提交了使用时间段,问最多能排几场会议。这里的候选元素是若干区间,每个区间有开始时间 \(a_i\) 和结束时间 \(b_i\),目标是选出的区间两两不重叠,并且数量最多。
贪心解法是:把所有区间按结束时间从小到大排序,然后从第一个开始扫描;只要当前区间的开始时间不小于上一个被选区间的结束时间,就选它,否则跳过。这样选出的区间数量就是最大化的。为什么不是按开始时间排序?因为一个区间结束得越早,越不会遮挡后面的会议,相当于小周在自助餐里先拿走“占时最短”的菜,给后面留出更多胃口。
这道题也是“分发饼干”“最少硬币”等经典贪心题的“母题”:先确定排序键,再确定可行性判断条件,最后更新状态。区间调度本身还会变形为“最多观看节目数”“最多完成工作数”,但解法骨架完全一致。
小周在自助餐厅也遇到过类似场景:有些菜需要排队等很久,如果先拿等待时间短的菜,反而能在同样时间里吃到更多种类。区间调度题的精髓,就是把“怎么选最多”变成“选最早结束的”,一次扫描,答案自现。
- \(k^{\ast}\) 表示最优数量最终能安排下的最大会议数。
- 选中的区间序列 \(g_1\prec g_2\prec\cdots\prec g_k\)这些区间按某种顺序排列成一条链,对应小周按顺序一连串取菜。
- 不重叠条件 \(b_{g_t}\le a_{g_{t+1}}\)前一个区间的结束时间不能晚于下一个区间的开始时间,会议才能接得上。
- 最大化 \(k\)在所有可行链中挑出最长的一条,这就是贪心最终的目标。
跳跃游戏实战:看得见的最远一步
小周吃完饭,在公园里玩“跳石头”游戏。每块石头上写着一个数字,表示“从这里最多还能往前跳多远”,他想知道能不能跳到对岸。最笨的办法是枚举每一种跳法,但小周只用记住一件事:到目前为止,能跳到的最远石头是哪一个。
贪心解法也很简单:从左往右遍历每一块石头,不断更新“最远可达位置”。只要当前位置不超过最远可达位置,就说明当前石头是够得到的;如果遍历过程中最远可达位置已经覆盖了终点,就说明可以跳到。这个思路在“最少跳跃次数”的变体中同样有效:每次走到当前覆盖范围的边界时,跳跃次数加一,同时把边界更新成当前能到达的最远位置。
这个问题的绝妙之处在于,它不需要记录具体路径,只需要一个变量持续滚动更新。很多看起来需要回溯的题,其实都可以用这种“边界+最远可达”的贪心框架解决。
小周的移动方式看似走一步看一步,但他其实一直在计算“最远能到哪儿”。面试里遇到跳跃游戏,先想清楚边界变量,再写循环,代码很快就能落地,而且不容易漏掉边界条件。
- \(r_{i-1}\) 表示历史最远遍历到 \(i\) 之前,已经通过其他石头达到的最远位置。
- 当前位置 \(i\)现在正踩着的这块石头,是下标位置。
- 跳跃能力 \(\mathrm{nums}[i]\)当前石头上写的数字,代表最多还能跳多远。
- 更新公式 \(\max(r_{i-1}, i+\mathrm{nums}[i])\)把历史记录和当前可能合在一起,取更远的那一个。
贪心与动态规划:自助餐与全桌计算
如果自助餐规则变成“拿了一道菜,就会改变其他菜的味道”,小周的“不回头”策略就可能失败。这时他需要另一种思路:像服务员一样,把菜单上所有搭配都算一遍,再挑最好的一组。这就是动态规划。动态规划用一张表记录所有子问题的答案,虽然慢,但一定安全。
贪心与动态规划最典型的对比是 0-1 背包问题:有若干物品,每个有重量 \(w_i\) 和价值 \(v_i\),背包容量为 \(W\),问最多能带走多少价值。按“单位重量价值”做贪心在有些用例上会失败,因为物品不可分割,选了一个大而轻的物品,可能挤掉两个小而重的组合。动态规划则通过填表枚举每一种容量下的最优组合。
面试时,如果一时分不清该用哪个,可以先试探贪心:构造一个反例,看它会不会翻车。找不到反例,再试着用交换论证说明贪心成立;如果明显存在反例,就果断转动态规划。贪心像小周凭感觉快速吃一轮,DP 像后厨把所有菜谱都算一遍。
贪心并不是动态规划的劣化版,而是“快速通道”。一旦题目确实满足贪心性质,它的实现和时空开销都远优于 DP。小周把这两种方法放在餐厅场景里理解:贪心是边走边选,DP 是先把整个菜单翻开算一遍;面试官更希望你先判断清楚该走哪条路。
- 状态 \(dp[i][w]\)只考虑前 \(i\) 个物品、背包容量为 \(w\) 时的最大价值。
- 不拿当前物品第 \(i\) 个物品直接跳过,价值继承上一行的结果。
- 拿当前物品先减去它的重量 \(w_i\),再加上它的价值 \(v_i\)。
- 取最大值在“不拿”和“拿”两个选项里选更好的,这就是子问题最优的积累。
面试套路与避坑:五步检票流程
小周在模拟面试中总结出一套“过检票口”的流程:看到题目,先不急着写代码,而是问自己三个问题:候选集合是什么?排序键是什么?可行性判断条件是什么?这三个问题想明白,贪心题就解决了一半。
第二步是构造反例。拿硬币面值 \(\{1,3,4\}\) 凑 6 元举例:如果每次优先拿面值最大的,贪心会拿到 4、1、1 共三枚;但最优解是 3 和 3 两枚。这说明贪心的排序键不是万能的。面试中,一个反例就能让你避免写出注定错误的代码。第三步是检查边界,比如列表为空、只有一个元素、所有区间完全重叠、金额为 0 等情况。最后再分析时间复杂度和空间复杂度,给面试官一个完整方案。
小周把反例当作“验票员”:通过验票,贪心方案才允许进入下一轮;通不过,就换用动态规划或其他方法。面试中最大的加分项,往往不是你一次写对,而是你能主动说出“这里我考虑过反例,所以选择更稳妥的解法”。
- 目标金额 6我们的任务是用给定面值凑出 6 元。
- 贪心选择的 \(4+1+1\)每次都拿不超过剩余金额的最大面值,最终是 3 枚硬币。
- 最优选择的 \(3+3\)绕过贪心直觉,直接用两枚 3 元硬币凑出目标。
- 对比两个方案两边金额相等,币数却不同,说明贪心在此题不成立。
AI 应用场景
GPU 训练任务调度
阶段:训练资源管理与调度
解决的问题:多个深度学习训练任务同时向集群申请 GPU,传统“先到先得”会导致短任务被长任务堵住,GPU 窗口利用率低,单位时间完成的任务数少。用贪心区间调度思路,把每个任务看作一个时间段,按“预期结束时间”排序,优先安排能最早释放资源的任务,可显著提高任务完成数量。
具体案例:某 MLOps 平台每天有几十个模型微调任务排队。调度器只保留“当前最可能及时完成的候选任务”,让短任务先跑,长任务顺延,最终线上 GPU 利用率提升了约 20%。
特征选择的贪心前向搜索
阶段:特征工程
解决的问题:高维数据下特征数量过多,模型训练慢且容易过拟合。全量特征组合搜索会爆炸,贪心前向搜索每次只选一个“当前对验证集提升最大”的特征加入集合,选完不再回头,快速得到一个可用的特征子集。
具体案例:广告点击率预估项目中,原始特征有 2000 维。算法工程师用贪心筛选逐步选出 120 维核心特征,模型 AUC 提高了约 3%,训练时间缩短了一半。
在线推理预算分配
阶段:在线服务与资源优化
解决的问题:大模型推理服务有预算约束,无法为所有请求都分配最长生成时间。按“生成收益/计算成本”的性价比排序,贪心地把预算优先分配给高价值请求,可以在有限资源下获得更高总收益。
具体案例:某智能客服系统为不同用户问题分配 token 预算,优先保证高价值问题得到最长回答,低价值问题快速收敛,整体用户满意度保持稳定的同时,推理成本下降了约 30%。
Python 演示
def max_non_overlap(intervals): intervals.sort(key=lambda x: x[1]) picked = [] last_end = float('-inf') for start, end in intervals: if start >= last_end: picked.append((start, end)) last_end = end return picked def min_jumps(nums): n = len(nums) if n <= 1: return 0 jumps = 0 cur_end = 0 farthest = 0 for i in range(n - 1): farthest = max(farthest, i + nums[i]) if i == cur_end: jumps += 1 cur_end = farthest if cur_end >= n - 1: break return jumps def greedy_coin_count(coins, amount): coins.sort(reverse=True) count = 0 rest = amount for coin in coins: while rest >= coin: rest -= coin count += 1 return count if rest == 0 else -1 if __name__ == '__main__': meetings = [(1, 3), (2, 5), (4, 6), (5, 8), (7, 9)] print(max_non_overlap(meetings)) print(min_jumps([2, 3, 1, 1, 4])) print(greedy_coin_count([1, 3, 4], 6))
intervals.sort(key=lambda x: x[1])这是区间调度的关键:按结束时间从小到大排序,让贪心选择天然可见。if start >= last_end:判断当前会议会不会和上一个被选会议冲突,不冲突才收下。picked.append((start, end))把选中的区间加入结果列表,同时后续更新last_end。farthest = max(farthest, i + nums[i])跳跃游戏的核心更新:不断刷新最远可达位置。if i == cur_end:走到当前边界时,必须再跳一次,同时把边界更新成farthest。coins.sort(reverse=True)硬币找零的贪心排序:面值从大到小尝试。while rest >= coin:只要还能用当前面值,就尽量多拿,这是典型的“当下最优”动作。return count if rest == 0 else -1如果剩余金额不为 0,说明贪心在这个面值组合下失败。
常见误区
误区贪心算法不就是每次选最大、最贵、最长的那个吗?我在面试里看到“选最多”就按最大价值排序,结果经常出错。
正解贪心的核心是“选当前最优”,但“最优”的标准因题而异。区间调度里,最优不是选最长的区间,而是选结束时间最早的区间;跳跃游戏里,最优不是跳得远就无脑跳,而是维护最远可达。排序键才是贪心的灵魂,选错了键,后面的步骤再规范也没用。
为何常见:生活里“贪心”一词会让人联想到“拿最多的好东西”,于是初学者本能地按最大值、最大面值排序,忽略了题目真正的约束条件。
误区只要每一步都选当前最好的,最后结果就一定是最好的。所以我做贪心题从来不需要考虑反例。
正解局部最优形成全局最优,必须满足贪心选择性质和最优子结构。硬币面值 \(\{1,3,4\}\) 凑 6 就是一个反例:贪心选 4 后再选两个 1,共 3 枚;最优解是两个 3,共 2 枚。不能证明性质时,必须用反例验证。
为何常见:教材里现成的贪心经典题,比如哈夫曼编码、最小生成树,都已经验证过正确性,学习者容易产生“所有贪心都对”的错觉。
误区代码能跑通用例就行,面试里直接写答案,没必要讲什么是交换论证、数学归纳和反证法。
正解面试官考察的不仅是代码,更是决策依据。如果题目本身贪心不成立,你跑过几个用例也救不回来。真正专业的回答是:先说“这题我尝试用贪心”,再讲清楚排序依据,再用交换论证说明贪心元素可以进入某个最优解,最后分析复杂度。
为何常见:在线评测系统只判断对错,不判断推理过程,导致很多人习惯了“跑通万岁”的工程思维。
误区动态规划更高级、更安全,贪心只是 DP 的偷懒版,能不用就不用。
正解两者是并列的算法思想。贪心一旦成立,往往比 DP 快一个量级,而且代码更简洁;DP 是全局可靠的兜底方案,但时间和空间代价更高。面试时先判断贪心,再用 DP 兜底,才是高效的解题策略。
为何常见:动态规划听起来更“硬核”,很多学习者把算法按印象分排序,低估了贪心的理论价值和面试价值。
面试题
Q1. 贪心算法的核心思想是什么?
- A.每次选择当前看起来最优的解← 正确答案
- B.先枚举所有可能的全局解
- C.随机选择之后逐步修正
- D.先求出所有子问题最优解
答案:A。贪心强调“当前最优、不回头”,不做全局枚举,也不回溯修正。B 是暴力搜索,C 是随机优化,D 是动态规划的思路。
Q2. 区间调度通常按哪个顺序排序后贪心?
- A.按区间左端点从大到小
- B.按区间右端点从小到大← 正确答案
- C.按区间长度从短到长
- D.按区间中点从近到远
答案:B。按右端点从小到大排序,才能让每次选择为后续留出最大空间。这就像自助餐里先拿“最早结束”的菜,才能腾出更多时间吃别的。
Q3. 硬币面值 {1,3,4},凑 6 元贪心为什么会失败?
- A.贪心先取 4 导致最终多一枚← 正确答案
- B.贪心无法处理零钱找零
- C.1 元面值没有被排序
- D.金额必须是 2 的倍数
答案:A。贪心先选面值 4,剩余 2 只能用两枚 1 元,共 3 枚;而最优解是 3+3,只需 2 枚。问题出在贪心选择面值 4 后破坏了后续的凑币能力,说明局部最优没有通向全局最优。
Q4. 跳跃游戏中贪心主要维护什么?
- A.当前能到达的最远位置← 正确答案
- B.当前走过的所有步数
- C.每次起跳的最短距离
- D.全部元素的总和值
答案:A。跳跃游戏只需维护“最远可达位置”,不断用当前下标加上跳跃力去更新它。一旦最远位置覆盖终点,就说明可达,这正是贪心“只记边界”的体现。
Q5. 关于最优子结构,下列说法正确的是?
- A.全局最优包含子问题的最优← 正确答案
- B.所有子问题都可以直接忽略
- C.贪心的选择结果一定唯一
- D.不需要对结构做任何验证
答案:A。最优子结构要求全局最优可以由子问题最优组合而成。如果子问题不可独立求解,贪心就无法递推。贪心选择不一定唯一,子问题更不能忽略,所以 A 正确。
Q6. 下列哪些常用于证明贪心正确?
- A.交换论证法← 正确答案
- B.数学归纳法← 正确答案
- C.反证法← 正确答案
- D.蒙特卡洛模拟
答案:ABC。交换论证把最优解中的元素替换成贪心元素,证明不会变差;数学归纳法把贪心步骤逐层推进;反证法假设贪心不是最优,再推出矛盾。蒙特卡洛模拟只能抽样验证,不能给出严格证明。
Q7. 关于贪心与动态规划,以下说法正确的是?
- A.贪心速度快但不保证正确← 正确答案
- B.动态规划可以保证得到最优← 正确答案
- C.贪心正确时通常效率更高← 正确答案
- D.贪心一定比动态规划差
答案:ABC。贪心时间复杂度通常很低,但必须满足性质才正确;DP 通过状态表枚举所有可能,通常能得到全局最优。贪心和 DP 是两种工具,不存在“一定差”的关系。
Q8. 区间调度用贪心相比动态规划的主要优势是什么?
- A.时间可降到 O(n log n) 量级← 正确答案
- B.时间仍为 O(n^3) 量级
- C.时间会降到 O(2^n) 量级
- D.时间会是 O(n!) 量级
答案:A。贪心先排序再扫描,时间复杂度为 \(O(n\log n)\)。而动态规划在区间问题上通常需要排序后做二阶甚至更高阶枚举,复杂度更高。这就是“正确贪心远快于 DP”的体现。
Q9. 面试中使用贪心解法的完整步骤包括?
- A.确定排序依据← 正确答案
- B.尝试构造反例← 正确答案
- C.跳过任何证明
- D.检查边界复杂度← 正确答案
答案:ABD。靠谱的贪心流程是先确定排序键,再构造反例验证,随后分析边界和复杂度。跳过证明和反例,直接写代码是面试中最容易翻车的做法。
Q10. 贪心选择性质和最优子结构同时满足时可以怎么判断?
- A.贪心基本正确且常是最优← 正确答案
- B.只需要满足其中一个
- C.最优子结构可以忽略
- D.与排序依据无关
答案:A。两个性质同时满足,贪心就具备了完整的理论支撑,通常可以得到最优解。只满足其中一个仍可能失败;最优子结构不可忽略;排序依据反而决定了贪心选择性质是否成立。