002 · Best Time to Buy and Sell Stock

algorithm
Published

April 16, 2026

My First Thoughts

最开始会把它理解成“找最大增幅”:从某一天买入,在之后某一天卖出,利润就是后面的价格减去前面的价格。这里有一个很关键的限制:必须先买后卖,所以差值不是简单的全局 max - min,而是有顺序要求的最大增幅。

最直接的解法还是遍历。从第一个元素出发,枚举它之后的所有价格,计算差值;每一轮都保留所有可能利润,最后选出最大值。如果最大值是负数,就返回 0,否则返回那个最大利润。

def max_profit(prices: list[int]) -> int:
    best = 0
    n = len(prices)

    for buy in range(n):
        for sell in range(buy + 1, n):
            best = max(best, prices[sell] - prices[buy])

    return best

这当然能得到正确答案,因为它检查了每一种先买后卖的可能。

进一步想,其实不需要真的把每一个差值都保存下来。每一轮只保留当前见过的最大利润即可,本质上相当于一边计算一边提前比较。

换句话说,所有合法组合大概是 \(A(n, 2)\) 个,我们是在这些组合里算差值,再求最大值。


Why That Is Not Enough

问题是复杂度太高。prices.length 最大可以到 \(10^5\),两层循环会变成 \(O(n^2)\),很容易超时。

接着我想到一个更激进的方向:既然是找最大差值,那能不能先算出极差,也就是全局 max - min

如果全局最小值在全局最大值前面,那么这个极差就是答案。但如果顺序不合法,就尝试围绕全局最大值和全局最小值切分:

  • 找到最大值的 index,看它前面区间的最小值,得到差值 a
  • 找到最小值的 index,看它后面区间的最大值,得到差值 b
  • 最后比较 ab

这个思路看起来是在修正全局极差的顺序问题,但它仍然不可靠。原因是:最优解不一定使用全局最大值,也不一定使用全局最小值

例如:

prices = [10, 7, 1, 9, 0, 5]

全局最大值是 10,全局最小值是 0,但它们的顺序不合法。真正的最大利润是 1 -> 9,利润为 8

如果只围绕全局最大值和全局最小值切一次,就可能得到 0 -> 5 的利润 5,从而漏掉中间的 1 -> 9

所以这题不能只看全局极差,也不能只围绕全局 maxmin 做局部修正。它必须尊重时间顺序,并且在最坏情况下需要扫描全部元素。


Final Idea

遍历到某一天时,如果今天卖出,那么最好的买入价格一定是今天之前出现过的最低价格。

因此只需要维护两个变量:

  • min_price:到当前为止见过的最低价格
  • best:到当前为止能得到的最大利润

从第二天开始,每看一个价格 price

  1. 先尝试用 price - min_price 更新最大利润
  2. 再用 price 更新历史最低价格

Why It Works

对任意一天来说,卖出日已经固定为今天。要让利润最大,买入日只能从今天之前选择,而最佳买入日就是过去价格最低的那一天。

所以我们不需要保存所有过去价格,也不需要枚举所有组合。只要在遍历过程中保存“过去最低价”,就已经保留了今天卖出所需的全部历史信息。


Code

def max_profit(prices: list[int]) -> int:
    min_price = prices[0]
    best = 0

    for i in range(1, len(prices)):
        price = prices[i]
        best = max(best, price - min_price)
        min_price = min(min_price, price)

    return best

Complexity

Time \(O(n)\) — 每个价格只看一次
Space \(O(1)\) — 只维护两个变量

Takeaway

当题目要求在有顺序约束的数组里找最大差值时,不要急着找全局 max - min。先固定当前位置作为“卖出点”,再问:如果今天卖,过去最好的买入价是什么?遍历时维护一个历史最低价,就能把所有合法组合压缩成一次扫描。


Quiz