LeetCode 121″买卖股票的最佳时机”是一道经典的数组与贪心题。题目看起来需要同时选择买入日和卖出日,但真正求解时并不需要枚举所有组合。只要在遍历过程中维护此前出现过的最低价格,就能用一次遍历得到最大利润。
本文将从暴力枚举入手,推导贪心解法,并重点解释”为什么最低买入价必须出现在卖出日之前”。
题目说明
给定数组 prices,其中 prices[i] 表示某只股票第 i 天的价格。只能选择一天买入,并在未来的某一天卖出,求一次交易能够获得的最大利润。如果无法获得正利润,返回 0。
输入:prices = [7, 1, 5, 3, 6, 4]
输出:5
解释:第 2 天以 1 买入,第 5 天以 6 卖出,利润为 6 - 1 = 5。
本题有三个关键限制:
- 最多只能完成一次买入和一次卖出
- 必须先买入,再卖出
- 可以选择不交易,此时利润为
0
暴力枚举
最直观的思路是枚举所有买卖组合:外层选择买入日,内层选择其后的卖出日,计算两天的价格差并更新最大利润。
class Solution {
public int maxProfit(int[] prices) {
int maxProfit = 0;
for (int buy = 0; buy < prices.length; buy++) {
for (int sell = buy + 1; sell < prices.length; sell++) {
maxProfit = Math.max(
maxProfit,
prices[sell] - prices[buy]
);
}
}
return maxProfit;
}
}
这种写法容易理解,但时间复杂度为 O(n²)。当数组长度较大时,重复比较会造成明显性能问题。
贪心算法的核心思想
假设遍历到第 i 天,并把当天价格 prices[i] 看作卖出价。为了让利润最大,买入价应该选择第 i 天之前出现过的最低价格。
因此只需要维护两个变量:
minBuyPrice:当前日期之前出现过的最低价格maxProfit:遍历过程中得到的最大利润
对于每一个当前价格,执行以下操作:
当前利润 = 当前价格 - 历史最低价格
最大利润 = max(最大利润, 当前利润)
历史最低价格 = min(历史最低价格, 当前价格)
这是一种贪心选择:当卖出日期固定时,更低的买入价格一定不会让结果变差。因此,只需要保留历史最低价,其他更高的买入价格都可以舍弃。
示例演算
以 prices = [7, 1, 5, 3, 6, 4] 为例:
| 当前价格 | 此前最低买入价 | 当前卖出利润 | 最大利润 |
|---|---|---|---|
| 7 | 7 | — | 0 |
| 1 | 7 | -6 | 0 |
| 5 | 1 | 4 | 4 |
| 3 | 1 | 2 | 4 |
| 6 | 1 | 5 | 5 |
| 4 | 1 | 3 | 5 |
遍历结束后,最大利润为 5,对应在价格为 1 时买入,在价格为 6 时卖出。
需要注意,价格 1 出现后,后续不需要再考虑价格 7 作为买入价。因为对于任何未来的卖出价格,以 1 买入都比以 7 买入更优。
为什么不能直接用最高价减最低价?
有人可能会先找到数组中的最低价和最高价,再计算二者之差。但这种方法忽略了”先买后卖”的时间约束。
例如:
prices = [7, 6, 4, 3, 1]
全局最高价是 7,最低价是 1,但 7 出现在 1 之前。如果计算 7 - 1 = 6,相当于先以 1 买入,再回到过去以 7 卖出,显然不符合题意。
正确做法是:每次把当天作为卖出日,只使用当天之前的最低价格计算利润。这样天然保证买入日期早于卖出日期。
为什么要先计算利润,再更新最低价?
代码从第二天开始遍历,并按照以下顺序执行:
- 使用历史最低价计算当天卖出的利润
- 更新最大利润
- 再使用当天价格更新最低价
这样写能让变量语义非常明确:计算第 i 天卖出利润时,minBuyPrice 只来自前 i - 1 天,严格满足”买卖发生在不同日期”的要求。
如果先更新最低价,在当前价格更低时会得到 currentProfit = 0,最终答案通常仍不会因此出错,但会把”同一天买入并卖出”混入状态含义。先计算利润再更新最低价,更符合题目约束,也更方便解释和维护。
Java 完整实现
class Solution {
public int maxProfit(int[] prices) {
if (prices == null || prices.length < 2) {
return 0;
}
int minBuyPrice = prices[0];
int maxProfit = 0;
for (int i = 1; i < prices.length; i++) {
// 当前价格作为卖出价,买入价只能来自此前日期
int currentProfit = prices[i] - minBuyPrice;
maxProfit = Math.max(maxProfit, currentProfit);
// 当前价格可作为未来某一天的买入价
minBuyPrice = Math.min(minBuyPrice, prices[i]);
}
return maxProfit;
}
}
代码只遍历数组一次。minBuyPrice 保存当前日期之前的最低价格,maxProfit 保存已经考察过的所有合法交易中的最大利润。
边界情况
价格持续下降 :
prices = [7, 6, 4, 3, 1]
所有卖出利润都小于或等于零。由于 maxProfit 初始化为 0,最终返回 0,表示不进行交易。
只有一个价格 :
prices = [5]
只有一天,无法在不同日期完成买入和卖出,因此返回 0。
价格相同 :
prices = [3, 3, 3]
任意合法交易的利润均为零,最终返回 0。
最低价出现在最后一天 :
prices = [5, 4, 3, 2, 1]
最后一天虽然是最低价,但后面没有可供卖出的日期,所以它无法产生利润。算法只会把它保存为潜在买入价,不会凭空构造未来卖出日。
复杂度分析
- 时间复杂度:
O(n),只遍历价格数组一次 - 空间复杂度:
O(1),只使用常数个变量
与暴力解法相比,贪心算法把时间复杂度从 O(n²) 降低到了 O(n)。