力扣hot100之121:买卖股票的最佳时机
力扣hot100之121:买卖股票的最佳时机

LeetCode 121:买卖股票的最佳时机(Best Time to Buy and Sell Stock)题解

Best Time to Buy and Sell Stock 是 LeetCode 里非常经典的一道数组题,也是很多人接触“贪心思想”时最早会做的一题。

这道题本身不难,代码甚至可以短到几行,但它很适合用来训练一种非常重要的思维方式:

在遍历过程中,动态维护“当前最优状态”。

一次遍历,同时维护最低买入成本和最大利润。

本文就系统讲一下这道题到底该怎么理解,为什么这样写是对的,以及这几行代码背后的思路是什么。


题目介绍

给定一个数组 prices,其中 prices[i] 表示某支股票在第 i 天的价格。

你只能选择:

  • 某一天买入股票

  • 在未来的某一天卖出股票

并且:

  • 只能买卖一次

  • 必须先买后卖

  • 如果无法获利,就返回 0

也就是说,这道题本质上是在问:

在所有合法的买入卖出方案中,最大利润是多少?

例如:

prices = [7,1,5,3,6,4]

最大利润是:

6 - 1 = 5

因为可以在价格为 1 的时候买入,在价格为 6 的时候卖出。


示例

示例 1

输入:
prices = [7,1,5,3,6,4]

输出:
5

解释:

  • 第 2 天价格是 1,买入

  • 第 5 天价格是 6,卖出

  • 最大利润为 5

示例 2

输入:
prices = [7,6,4,3,1]

输出:
0

解释:

这组数据一直在跌,没有任何一次买入卖出可以赚钱,所以返回 0


先想暴力解法

这道题最直接的想法其实很自然:

  • 枚举每一天作为买入点

  • 再枚举后面的某一天作为卖出点

  • 计算利润

  • 取最大值

也就是检查所有:

prices[j] - prices[i]   (j > i)

这种写法当然能做出来,但时间复杂度是:

O(n^2)

因为每个买入点都要去看后面所有卖出点。

当数组很大时,这样做效率就不够好了。

所以这道题真正要解决的问题是:

能不能只遍历一遍,就把答案求出来?

答案是可以。


这道题真正的关键

题目要求最大利润,而利润的公式是:

卖出价 - 买入价

如果我们从左到右遍历数组,假设当前来到某一天,价格是 price,并且把这一天当成“卖出日”去思考,那么我们要做的事情其实只有一件:

找到在它之前出现过的最低价格。

因为:

  • 卖出价已经固定为当前价格

  • 想让利润最大,就要让买入价尽可能小

所以对于当前这一天来说,最好的利润就是:

当前价格 - 之前出现过的最小价格

于是问题就变成了:

  1. 遍历到当前位置时,记录“历史最低价格”

  2. 用当前价格减去这个最低价格,得到当前可能利润

  3. 再用这个利润去更新全局最大利润

一遍扫描就够了。

这就是这道题的核心思想。


Java 代码

class Solution {
    public int maxProfit(int[] prices) {
        int cost = Integer.MAX_VALUE, profit = 0;
        for (int price : prices) {
            cost = Math.min(cost, price);
            profit = Math.max(profit, price - cost);
        }
        return profit;
    }
}

这段代码很短,但思路非常完整。


代码拆解

下面我们一行一行来看。

1. costprofit 分别表示什么?

int cost = Integer.MAX_VALUE, profit = 0;

这里定义了两个变量:

  • cost:到目前为止,股票出现过的最低价格,也就是最低买入成本

  • profit:到目前为止,能够获得的最大利润

为什么 cost 初始值要设成 Integer.MAX_VALUE

因为这样在第一次比较时:

cost = Math.min(cost, price);

无论第一个价格是多少,都会被更新成真实的股票价格。

profit 初始值设成 0,也很好理解:

因为题目说了,如果不能赚钱,就返回 0,所以利润最小就是 0,不可能是负数。


2. 为什么只需要一次遍历?

for (int price : prices) {

这句表示从左到右扫描整个价格数组。

关键在于: 每走到一天,我们只关心两件事:

  • 之前最便宜的价格是多少

  • 如果今天卖出,利润能有多少

这两个信息都可以在遍历过程中不断维护,因此不需要双重循环。


3. 为什么先更新 cost

cost = Math.min(cost, price);

这句代码的作用是:

把当前为止出现的最低价格记录下来。

比如价格序列是:

[7,1,5,3,6,4]

遍历过程中的 cost 变化是:

  • 第一天:7

  • 第二天:1

  • 后面一直保持 1

也就是说,cost 始终代表:

在当前这一天及之前,最低的买入价格是多少。

这一步是整道题的关键,因为后面的利润计算都依赖它。


4. 为什么利润写成 price - cost

profit = Math.max(profit, price - cost);

因为当前遍历到的 price,我们可以把它理解成:

今天如果卖出,能赚多少钱?

而能赚多少,取决于之前最低的买入价 cost

所以当前这一天的利润就是:

当前卖出价 - 历史最低买入价

然后再把这个利润和之前记录的最大利润比较,留下更大的那个。

于是 profit 始终表示:

截至当前这一天,最多能赚多少钱。


为什么这样写不会出错?

很多人第一次看到这段代码时,会有一个疑问:

先更新了 cost,再计算 price - cost,会不会出现“当天买入当天卖出”的问题?

答案是:不会影响正确性。

因为如果当前价格正好刷新了最低价,那么:

price - cost = 0

也就是“今天买今天卖,不赚钱”。

这不会让答案出错,因为真正有意义的利润,一定会在未来某一天价格更高时体现出来。

而且题目要求的是最大利润,保留 0 也符合题意。


手动模拟一遍

我们用题目的经典例子来完整走一遍:

prices = [7,1,5,3,6,4]

初始状态:

cost = ∞
profit = 0

第一天,价格 7

更新最低成本:

cost = min(∞, 7) = 7

更新利润:

profit = max(0, 7 - 7) = 0

当前状态:

cost = 7
profit = 0

第二天,价格 1

更新最低成本:

cost = min(7, 1) = 1

更新利润:

profit = max(0, 1 - 1) = 0

当前状态:

cost = 1
profit = 0

第三天,价格 5

更新最低成本:

cost = min(1, 5) = 1

更新利润:

profit = max(0, 5 - 1) = 4

当前状态:

cost = 1
profit = 4

第四天,价格 3

更新最低成本:

cost = min(1, 3) = 1

更新利润:

profit = max(4, 3 - 1) = 4

当前状态:

cost = 1
profit = 4

第五天,价格 6

更新最低成本:

cost = min(1, 6) = 1

更新利润:

profit = max(4, 6 - 1) = 5

当前状态:

cost = 1
profit = 5

第六天,价格 4

更新最低成本:

cost = min(1, 4) = 1

更新利润:

profit = max(5, 4 - 1) = 5

最终答案:

5

也就是在价格 1 时买入,在价格 6 时卖出。


再看一组无法获利的情况

例如:

prices = [7,6,4,3,1]

遍历过程中:

  • cost 会不断变小

  • 但是 price - cost 始终不会大于 0

所以最终:

profit = 0

这正好符合题意。


这道题本质上是在考什么?

这题表面上是股票题,但它真正考的并不是金融知识,而是数组中的“动态最优维护”。

第一,是否能把问题转换成局部信息维护

很多人一开始只会想着:

  • 买在哪一天

  • 卖在哪一天

  • 要把所有组合都试出来

但这题真正高效的做法是发现:

对于每一天作为卖出点来说,我只需要知道它前面最小的价格。

一旦想到这一点,双重循环就可以压缩成单次遍历。

第二,是否理解“遍历时顺手维护答案”

这道题非常典型地体现了一个技巧:

在扫描数组时,不断维护某个历史最优值。

这里维护的是:

  • 历史最低价格

  • 历史最大利润

以后你会在很多题里看到类似套路,比如:

  • 最大子数组和

  • 最高海拔

  • 最小前缀值 / 最大前缀值

  • 动态维护区间最优答案

第三,是否区分“状态”和“答案”

这题里:

  • cost 是状态,表示当前为止最低买入成本

  • profit 是答案,表示当前为止最大利润

把这两个变量分清楚,代码自然就会非常清晰。


复杂度分析

假设数组长度为 n

时间复杂度

整段代码只遍历了一次数组,每个元素只处理一次,所以时间复杂度是:

O(n)

空间复杂度

只用了两个额外变量:

  • cost

  • profit

所以空间复杂度是:

O(1)

这也是这道题最优的做法。


和动态规划有什么关系?

很多人刷到这题时,会听别人说这是“动态规划”,也有人说这是“贪心”。

其实这道题从不同角度都能解释。

从贪心角度看

我们始终贪心地维护:

到当前位置为止最便宜的买入价格。

这样一旦当前价格适合卖出,就能立刻得到当前位置的最优利润。

从动态规划角度看

也可以把它理解成:

  • cost 是前缀最小值

  • profit 是前缀最大利润

每一步都由前一步状态转移而来。

不过在实际面试或者写题解时,这题通常更常被归类为:

一次遍历 + 贪心维护最小值。

因为这种解释最直观,也最容易理解。


为什么这题很经典?

因为它短,但非常有代表性。

它代表了一类常见问题:

在遍历过程中,如何利用“历史信息”快速得到当前位置的最优答案?

本题中:

  • 历史信息是“之前最低价格”

  • 当前决策是“如果今天卖出,利润是多少”

把历史信息维护好,当前位置的答案就能立刻算出来。

这其实是一种非常常见的算法思想。


总结

这道题的核心可以概括成一句话:

从左到右遍历数组,始终维护历史最低买入价,并用当前价格减去它,来更新最大利润。

具体步骤就是:

  1. cost 记录遍历过程中出现过的最低价格

  2. 把当前价格当作卖出价

  3. 计算当前可能利润 price - cost

  4. profit 维护全局最大利润

  5. 遍历结束后返回结果

这道题非常适合作为“数组遍历维护最优解”的入门题。 如果你能真正理解它,后面再去看:

  • 买卖股票的最佳时机 II

  • 买卖股票的最佳时机 III

  • 买卖股票的最佳时机含冷冻期

  • 买卖股票的最佳时机含手续费

你会更容易意识到:

股票问题的本质,往往不是模拟买卖过程,而是设计状态并维护最优转移。

而这道题,就是整个股票系列里最基础、也最经典的一块起点。

如果对您有帮助的话,能否支持一下博主?
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇