当前:LC121 · 买卖股票的最佳时机 · 首次出现于 Day 40 · 路径:顶栏「56天打卡」→ 点击 LC 题号 → 逐题动画

LC121 · Best Time to Buy and Sell Stock · 动态规划

买卖股票 I:记下历史最低,今天卖出差最大

每天只当卖出日。买点用它之前出现过的最低价。边扫边更新最低,利润只升不降。

从左到右扫 prices:minPrice 是截至目前的最低价,profit 是截至目前的最大利润。若今天更低,只更新 minPrice;否则用今天价减 minPrice 挑战 profit。卖出天然在买入之后。时间 O(n),空间 O(1)。

时间 O(n)空间 O(1)结论先行 · 全文约 6 节
导读

数组 prices[i] 是第 i 天的股价。你只能选择某一天买入,之后某一天卖出,做一笔交易。卖出必须严格在买入之后。利润是卖出价减买入价;做不了赚钱的交易就返回 0。

主例 prices = [7, 1, 5, 3, 6, 4]。第 2 天以 1 买入,第 5 天以 6 卖出,利润 5。第 1 天 7 之后再没有更高的卖点能超过这笔。不能先卖后买,也不能同一天买卖来凑 0 以外的数。

枚举所有买入日和卖出日是平方级。本文要回答:固定卖出日之后为什么只缺一个「之前最低」,以及主例里利润从 4 刷新到 5,发生在哪一天。

边扫边记最低价

若今天卖出,要想利润最大,买入必须发生在今天之前、并且买在那段前缀的最低点。前缀最低只有一个。缺的不是「哪天卖」,而是扫到今天时手里还记不记得那个最低买点。两个变量就够:minPrice 和 profit。

minPrice 先垫成第一天的价格,profit 垫成 0。之后每天先问:今天是不是更便宜?是,只更新 minPrice——今天当卖出日赚不到正利润,但它可能成为后面的更好买点。不是,就用今天价减 minPrice,比 profit 大就刷新。顺序保证卖出下标不小于买入下标;同一天两者相等,利润 0,不会写进更大的 profit。

用手走主例。第 0 天 7:minPrice=7,profit=0。第 1 天 1:比 7 低,minPrice 改成 1,1−7 是负的,profit 不动。第 2 天 5:5−1=4,profit=4。第 3 天 3:3−1=2,打不过 4。第 4 天 6:6−1=5,profit 刷新成 5。第 5 天 4:4−1=3,仍是 5。演示场景每一步都标着当前最低和当前利润,最后停在 5。

若以为「先找全局最低、再找它右边的最高」,主例碰巧对:最低是 1,右边最高是 6。把数组换成 [2, 5, 1, 3],全局最低 1 右边最高 3,利润 2;真正的最优是 2 买 5 卖,利润 3。最低出现在后面时,它左边那段上涨就被丢掉了。所以必须边走边比,而不是先定位一个全局谷再找峰。

股价一路下跌,每天都在更新 minPrice,profit 一直是 0,正确。只有两天,要么第二天更高取差值,要么返回 0。第一天之后没有交易日,也是 0。这些都被「从左扫到右、卖出价减历史最低」覆盖。

固定卖出日把第 i 天定为卖出日,最优买点只可能是 i 之前的最低价。这一个数就是全部历史。所以不必留整段前缀,也不必枚举买入日。
prices = [7,1,5,3,6,4]
7最低
[0]
1
[1]
5
[2]
3
[3]
6
[4]
4
[5]
最低 7利润 0
i=0 · 7 元,min=7,无利润

Go:一次遍历

solution.goGo
func maxProfit(prices []int) int {
minPrice := prices[0]
profit := 0
for _, p := range prices {
if p < minPrice {
minPrice = p
} else if p-minPrice > profit {
profit = p - minPrice
}
}
return profit
}

1minPrice 从第一天起记。主例第二天把它从 7 改成 1。

2今天不是新低,才用今天减 minPrice 挑战 profit。主例第 5 天 6−1=5。

3先判断是否更低,再算利润,同一天不会用「刚更新的最低」卖出得到正利润。

总结

历史最低当买点,今天价减它当卖出利润。主例 1 买 6 卖,利润 5。

  • 固定卖出日,买点只剩「之前最低」这一个数。
  • 先找全局最低再找右边最高,会在 [2,5,1,3] 上错成 2,漏掉前面的 3。
  • 一路下跌时 profit 停在 0,不能初始化成负数以外的哨兵来「必须交易」。
同族题目
LC122买卖股票的最佳时机 IILC53最大子数组和LC188买卖股票的最佳时机 IV