Hot 100 --- 买卖股票的最佳时机
本文概览:本文讲解买卖股票的最佳时机的核心思路:每个卖出日的最优买入日就是它前面的历史最低价,一次遍历同时维护最小价格和最大利润,从 O(n²) 降到 O(n)
一、题目

二、题目分析
1. 题目要求
给定一个数组 prices,第 i 个元素代表第 i 天的股票价格。只能选择某一天买入,并在它之后的某一天卖出,求能获得的最大利润。如果不能获利,返回 0。
例子:prices = [7, 1, 5, 3, 6, 4]
1 | 0 1 2 3 4 5 ← 索引(天) |
注意两条规则:
- 卖出必须在买入之后(不能在买入前卖出)
- 只能交易一次
比如不能算 7 - 1 = 6:7 在第 0 天,1 在第 1 天,这是"先卖后买",不合法。
2. 暴力思路:枚举所有买入/卖出组合
由于卖出只能在买入之后,那就从第一天开始,枚举每个买入日、再枚举它后面的每个卖出日,算出所有利润取最大:
1 | for (int i = 0; i < n; i++) { // 买入日 |
**时间复杂度 O(n²)**:两层循环,每个买入日都要往后扫一遍所有卖出日。
3. 核心观察:暴力遍历中的无效计算
暴力慢在哪?慢在大量根本不可能成为答案的买入日也被拿来反复比较。
关键观察:如果第二天的价格比第一天低,那就肯定不会在第一天买入。为什么?后面无论哪天卖出,只要卖出价高于第一天的买入价,就必然高于第二天的价格,此时第二天买入的利润必然大于第一天买入的利润——第一天买入的所有方案都被第二天全覆盖且更差。
把这个观察推广到一般情况:
对于每一个卖出日 i,最优的买入日一定是 [0, i-1] 中价格最低的那一天。
既然如此,我们根本不需要枚举买入日。只需一直记录遍历到当天为止,前面所有天里的最低股票价格,然后用当天价格减去这个最低价,就得到了"以当天为卖出日"的最大利润:
- 利润更大 → 替换 maxProfit
- 利润不大 → 不替换
这就是贪心思想:每一步都取当前能拿到的最优解(历史最低价买入、当天卖出),最终合起来就是全局最优。这题是可以用贪心保证正确性的。
三、思路概览
1 | public int maxProfit(int[] prices) { |
思路简要说明:
- 两个变量:
minPrice记录遍历到当天为止的历史最低价(初始为第 0 天价格),maxProfit记录最大利润(初始 0,保证不获利时返回 0) - 一次遍历:每个新价格只有两种情况——比历史最低价还低(更新最低价),或者更高(算利润、更新答案)
- 贪心本质:每个卖出日的最优买入日就是前面的历史最低价日,所以只需维护一个最低价
- **时间复杂度 O(n)**:只扫一遍数组,O(n²) 降到 O(n)
四、思路详解
第一步:暴力到底重复算了什么?
把暴力解在 [7, 1, 5, 3, 6, 4] 上的部分计算列出来:
1 | 买入日 0(价格7):卖出日 1..5 → 利润 -6, -2, -4, -1, -3 |
注意买入日 0 这一整行:价格 7 比第 1 天的 1 高,它算出来的所有利润都惨不忍睹。而买入日 1 的那一行,每一项都比买入日 0 对应的项正好大 6——同样的卖出日,换个更便宜的买入日,利润全面碾压。
结论:买入日价格更高的方案,被"更便宜的买入日"全面覆盖。一个卖出日真正需要配的买入日,只有它前面价格最低的那一天。枚举其它买入日全是无效计算。
第二步:为什么"历史最低价"可以代表所有买入日?
对每个卖出日 i,我们声称最优买入日是 [0, i-1] 中价格最低的那天。证明很简单:
1 | 假设卖出日 i,买入日 j 和 j'(都在 i 之前), |
卖出价 prices[i] 是同一个数,买入价越低利润越大。所以比较所有买入日等价于只看最低的那个买入价——这就是 minPrice 一个变量能顶掉内层整个循环的原因。
第三步:一次遍历怎么同时维护两个信息?
遍历到第 i 天时,需要的信息有两个:
[0, i-1]的最低价 → 用来当买入价prices[i] - 最低价→ 以第 i 天为卖出日的最大利润
这两个信息可以顺路一起维护:
1 | 来到第 i 天: |
if-else 的巧妙之处:两个分支互斥且刚好覆盖所有情况——
- 当天是新低,它不可能贡献利润(价格 - 自己 = 0,不大于已有的 maxProfit),但它是未来卖出日的最优买入价,所以只需更新 minPrice
- 当天不是新低,minPrice 保持不变,当天价格减 minPrice 就是它作为卖出日的最优利润
不需要任何嵌套循环,一遍走完,minPrice 始终是"今天之前的最便宜买入价",maxProfit 始终是"到目前为止的最大利润"。
完整执行过程
以 prices = [7, 1, 5, 3, 6, 4] 为例:
1 | 初始:minPrice = 7(第0天),maxProfit = 0 |
再走一个递减数组 [7, 6, 4, 3, 1] 的例子(验证不获利的情况):
1 | 初始:minPrice = 7,maxProfit = 0 |
复杂度分析
**时间复杂度 O(n)**:只遍历数组一遍,每个元素只做一次比较(要么更新 minPrice,要么更新 maxProfit)。
**空间复杂度 O(1)**:只用了两个变量。
对比暴力解:O(n²) → O(n),空间不变。这是"用观察换循环"的典型例子——发现"最优买入日 = 历史最低价日"之后,内层循环整个被一个变量取代。
五、总结
买卖股票的最佳时机的核心思路:
- 规则:买入必须在卖出之前,只能交易一次
- 暴力:枚举所有(买入日, 卖出日)组合,O(n²)
- 核心观察:买入价更高的方案被更便宜的买入日全面覆盖 → 每个卖出日的最优买入日,就是它前面的历史最低价日
- 贪心策略:一次遍历,同时维护历史最低价
minPrice和最大利润maxProfit - 分支互斥:当天是新低就只更新 minPrice(它是未来卖出日的最佳买入价);当天更高就算利润更新答案
贪心思想在本题的体现:每一步(每个卖出日)都取局部最优(配前面的最低价),由于"卖出价固定时买入价越低利润越大"这个单调关系,局部最优叠加起来就是全局最优。
这个"维护前缀最小值"的套路非常常用:遇到"枚举 j,求 a[j] - min(a[0..j-1]) 型"的问题,都可以用同样的方式把 O(n²) 压成 O(n)。






