题目描述
思路解析
一句话答案:LeetCode 121 买卖股票的最佳时机的最优解是一次遍历:从左到右扫价格数组,边走边维护「历史最低买入价」,对每一天假设今天卖出、用当天价减历史最低价更新最大利润。每天只做两次比较,时间 O(n)、空间 O(1),天然保证卖出在买入之后。
这道题真正在问什么
题目给一个数组 prices,prices[i] 是第 i 天的股价,只允许完成一笔交易——买一次、卖一次,且卖出必须在买入之后。求能拿到的最大利润,如果怎么交易都亏,就返回 0。隐藏的约束有两条:第一,交易只有一笔,不能反复低买高卖;第二,时间有方向,「卖在买之后」这条顺序限制正是本题和「找数组最大值减最小值」的本质区别。
为什么不能用全局最高价减全局最低价
一个诱人的错误想法是:最大利润不就是数组里的最大值减最小值吗?反例很快能构造出来——如果最高价出现在最低价之前,比如价格先冲到 9 再跌到 1,那这笔「先卖后买」的交易根本不合法。差值必须带方向:只能拿某一天的价格,去减它之前(含当天)出现过的最低价。
另一个直觉做法是两层循环,枚举所有买入日和卖出日的组合,取差值最大的一对。它是对的,但 n 天要试大约 n²/2 对,时间复杂度 O(n²),价格数组一长就慢得不可接受。
关键观察:固定卖出日,最优买入日是确定的
换个角度想:假如已经决定第 i 天卖出,那买入日该选哪天?答案是唯一的——第 0 到第 i 天里价格最低的那天,因为卖价固定时,买得越便宜赚得越多。这样问题就从「枚举所有买卖组合」坍缩成「对每个卖出日,查一下它之前的最低价」。
而「之前的最低价」这个量,恰好可以在从左到右扫描时顺手维护:走到第 i 天时,用当天价格去更新一个变量 min_price,它就始终等于前 i 天的最低价,也就是所谓的前缀最小值。原来内层循环要做的整段查找,被压缩成一次比较。
一次遍历的每一步为什么成立
算法保持两个不变量:min_price 永远是「到目前为止见过的最低价」,best 永远是「到目前为止能实现的最大利润」。每到新的一天,先用当天价更新 min_price,再计算「假设今天卖」的利润 p - min_price 去更新 best。因为 min_price 只由今天及之前的价格构成,用它算出的每笔利润都自动满足「先买后卖」,不需要额外检查顺序。
有人会担心:先更新 min_price 再算利润,万一今天恰好是新低,算出的利润不就是 0 吗?没关系——今天买今天卖利润本来就是 0,不会污染 best;而真正的收益要等之后某天价格高于这个新低时才兑现,届时自然会被更新进去。
复杂度怎么算,哪些边界会翻车
时间 O(n):数组只扫一遍,每天做一次取最小、一次取最大,都是常数操作。空间 O(1):全程只用 min_price 和 best 两个变量,不需要任何额外数组。
两个真会翻车的边界:一是价格一路下跌时,任何交易都亏,正确答案是 0 而不是负数——把 best 初始化为 0、亏了就当不交易,这条就自动满足;二是别把本题和 LeetCode 122 混淆,122 允许多次买卖、贪心吃下每段上涨即可,而本题只许一笔交易,找的是历史最低点之后的最大单段涨幅。
▶ 动画逐步走查(文字版)——想跟着上方动画一帧帧对照就展开
记住这条「维护历史最低买价、每天假设今天卖、更新最大利润」,下面每一天都在套它。
第 0 天价 7,比之前见过的都便宜,刷新历史最低买点(绿色格子移到这里)。买点越低,后面越可能赚得多。
假设今天卖:7 − 7 = 0,不如已有的最大利润 0,保持不变。
第 1 天价 1,比之前见过的都便宜,刷新历史最低买点(绿色格子移到这里)。买点越低,后面越可能赚得多。
假设今天卖:1 − 1 = 0,不如已有的最大利润 0,保持不变。
第 2 天价 5,没有比历史最低 1(第 1 天)更便宜,买点不动,绿色仍停在第 1 天。
假设今天卖:5 − 1 = 4,比旧的最大利润 0 更高,刷新最大利润为 4(第 1 天买、第 2 天卖)。
第 3 天价 3,没有比历史最低 1(第 1 天)更便宜,买点不动,绿色仍停在第 1 天。
假设今天卖:3 − 1 = 2,不如已有的最大利润 4,保持不变。
第 4 天价 6,没有比历史最低 1(第 1 天)更便宜,买点不动,绿色仍停在第 1 天。
假设今天卖:6 − 1 = 5,比旧的最大利润 4 更高,刷新最大利润为 5(第 1 天买、第 4 天卖)。
第 5 天价 4,没有比历史最低 1(第 1 天)更便宜,买点不动,绿色仍停在第 1 天。
假设今天卖:4 − 1 = 3,不如已有的最大利润 5,保持不变。
第 6 天价 2,没有比历史最低 1(第 1 天)更便宜,买点不动,绿色仍停在第 1 天。
假设今天卖:2 − 1 = 1,不如已有的最大利润 5,保持不变。
第 7 天价 8,没有比历史最低 1(第 1 天)更便宜,买点不动,绿色仍停在第 1 天。
假设今天卖:8 − 1 = 7,比旧的最大利润 5 更高,刷新最大利润为 7(第 1 天买、第 7 天卖)。
第 8 天价 1,没有比历史最低 1(第 1 天)更便宜,买点不动,绿色仍停在第 1 天。
假设今天卖:1 − 1 = 0,不如已有的最大利润 7,保持不变。
第 9 天价 9,没有比历史最低 1(第 1 天)更便宜,买点不动,绿色仍停在第 1 天。
假设今天卖:9 − 1 = 8,比旧的最大利润 7 更高,刷新最大利润为 8(第 1 天买、第 9 天卖)。
第 10 天价 4,没有比历史最低 1(第 1 天)更便宜,买点不动,绿色仍停在第 1 天。
假设今天卖:4 − 1 = 3,不如已有的最大利润 8,保持不变。
第 11 天价 10,没有比历史最低 1(第 1 天)更便宜,买点不动,绿色仍停在第 1 天。
假设今天卖:10 − 1 = 9,比旧的最大利润 8 更高,刷新最大利润为 9(第 1 天买、第 11 天卖)。
扫完全程,最优是第 1 天(价 1)买、第 11 天(价 10)卖,赚 9。注意卖点(11) 在买点(1) 之后——这正是「历史最低价只看今天及之前」自动保证的。
边界先想清:全跌为 0、全涨买在首日卖在末日、单天为 0。
两个高频追问,区分「限一次找单段最大」与「不限次贪心累加」。
参考代码
def maxProfit(prices): min_price = float("inf") # 历史最低买入价 best = 0 # 最大利润 for p in prices: min_price = min(min_price, p) # 更新最低买点 best = max(best, p - min_price) # 今天卖的利润 return best复杂度
- 时间:O(n),从头到尾扫一遍
- 空间:O(1),只用最低价、最大利润两个变量
易错点
面试追问把动画讲成自己的话
追问为什么一次遍历就够,不用枚举所有买卖对?
追问和「可多次买卖」(LC122) 有什么区别?
这道题到这就讲完了。动画和文字是同一套思路——别光看,关掉页面自己默写一遍。然后顺着主线继续:
无重复字符的最长子串
LeetCode 3 · 中等 · 沿着 滑动窗口 继续往下推进
把这道题真正学会,再走
图解算法年卡 ¥99 /年
- ✓本题每步动画的吴师兄语音讲解(全站陆续覆盖)
- ✓小欧带 8 步通关训练——追问到不看答案也能写对、能 30 秒讲给面试官听
- ✓学习报告,记录每道题的掌握程度
76k+ GitHub Star · 吴师兄开源图解算法,几十万开发者在看的算法讲解
想成体系刷透这类套路?去图解算法专题