Hot 100 --- 买卖股票的最佳时机

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


一、题目


二、题目分析

1. 题目要求

给定一个数组 prices,第 i 个元素代表第 i 天的股票价格。只能选择某一天买入 ,并在它之后的某一天卖出,求能获得的最大利润。如果不能获利,返回 0。

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

复制代码
0  1  2  3  4  5        ← 索引(天)
7  1  5  3  6  4        ← 价格
   ↑           ↑
  买入        卖出
(第1天,1)  (第4天,6)

最大利润 = 6 - 1 = 5

注意两条规则

  • 卖出必须在买入之后(不能在买入前卖出)
  • 只能交易一次

比如不能算 7 - 1 = 6:7 在第 0 天,1 在第 1 天,这是"先卖后买",不合法。

2. 暴力思路:枚举所有买入/卖出组合

由于卖出只能在买入之后,那就从第一天开始,枚举每个买入日、再枚举它后面的每个卖出日,算出所有利润取最大:

java 复制代码
for (int i = 0; i < n; i++) {          // 买入日
    for (int j = i + 1; j < n; j++) {  // 卖出日
        maxProfit = Math.max(maxProfit, prices[j] - prices[i]);
    }
}

时间复杂度 O(n²):两层循环,每个买入日都要往后扫一遍所有卖出日。

3. 核心观察:暴力遍历中的无效计算

暴力慢在哪?慢在大量根本不可能成为答案的买入日也被拿来反复比较。

关键观察 :如果第二天的价格比第一天低,那就肯定不会在第一天买入。为什么?后面无论哪天卖出,只要卖出价高于第一天的买入价,就必然高于第二天的价格,此时第二天买入的利润必然大于第一天买入的利润------第一天买入的所有方案都被第二天全覆盖且更差。

把这个观察推广到一般情况:

对于每一个卖出日 i,最优的买入日一定是 0, i-1 中价格最低的那一天。

既然如此,我们根本不需要枚举买入日。只需一直记录遍历到当天为止,前面所有天里的最低股票价格,然后用当天价格减去这个最低价,就得到了"以当天为卖出日"的最大利润:

  • 利润更大 → 替换 maxProfit
  • 利润不大 → 不替换

这就是贪心思想:每一步都取当前能拿到的最优解(历史最低价买入、当天卖出),最终合起来就是全局最优。这题是可以用贪心保证正确性的。


三、思路概览

java 复制代码
public int maxProfit(int[] prices) {
    int maxProfit = 0;
    int minPrice = prices[0];
    for (int i = 1; i < prices.length; i++) {
        // 当天价格更低 → 更新历史最低价,它不可能是卖出日(没利润可算)
        if (prices[i] < minPrice) {
            minPrice = prices[i];
        } else {
            // 当天价格更高 → 用历史最低价算利润,尝试更新答案
            maxProfit = Math.max(maxProfit, prices[i] - minPrice);
        }
    }
    return maxProfit;
}

思路简要说明:

  1. 两个变量minPrice 记录遍历到当天为止的历史最低价(初始为第 0 天价格),maxProfit 记录最大利润(初始 0,保证不获利时返回 0)
  2. 一次遍历:每个新价格只有两种情况------比历史最低价还低(更新最低价),或者更高(算利润、更新答案)
  3. 贪心本质:每个卖出日的最优买入日就是前面的历史最低价日,所以只需维护一个最低价
  4. 时间复杂度 O(n):只扫一遍数组,O(n²) 降到 O(n)

四、思路详解

第一步:暴力到底重复算了什么?

把暴力解在 [7, 1, 5, 3, 6, 4] 上的部分计算列出来:

复制代码
买入日 0(价格7):卖出日 1..5 → 利润 -6, -2, -4, -1, -3
买入日 1(价格1):卖出日 2..5 → 利润  4,  2,  5,  3
买入日 2(价格5):卖出日 3..5 → 利润 -2,  1, -1
...

注意买入日 0 这一整行:价格 7 比第 1 天的 1 高,它算出来的所有利润都惨不忍睹。而买入日 1 的那一行,每一项都比买入日 0 对应的项正好大 6------同样的卖出日,换个更便宜的买入日,利润全面碾压。

结论:买入日价格更高的方案,被"更便宜的买入日"全面覆盖。一个卖出日真正需要配的买入日,只有它前面价格最低的那一天。枚举其它买入日全是无效计算。

第二步:为什么"历史最低价"可以代表所有买入日?

对每个卖出日 i,我们声称最优买入日是 [0, i-1] 中价格最低的那天。证明很简单:

复制代码
假设卖出日 i,买入日 j 和 j'(都在 i 之前),
且 prices[j'] < prices[j]

利润(j)  = prices[i] - prices[j]
利润(j') = prices[i] - prices[j'] > 利润(j)

卖出价 prices[i] 是同一个数,买入价越低利润越大。所以比较所有买入日等价于只看最低的那个买入价 ------这就是 minPrice 一个变量能顶掉内层整个循环的原因。

第三步:一次遍历怎么同时维护两个信息?

遍历到第 i 天时,需要的信息有两个:

  1. [0, i-1] 的最低价 → 用来当买入价
  2. prices[i] - 最低价 → 以第 i 天为卖出日的最大利润

这两个信息可以顺路一起维护

复制代码
来到第 i 天:
  if  prices[i] < minPrice:
        说明第 i 天是新的历史最低 → 更新 minPrice
        (此时第 i 天自己当卖出日无利可图,跳过利润计算)
  else:
        prices[i] >= minPrice → 用 prices[i] - minPrice 算利润
        尝试更新 maxProfit

if-else 的巧妙之处:两个分支互斥且刚好覆盖所有情况------

  • 当天是新低,它不可能贡献利润(价格 - 自己 = 0,不大于已有的 maxProfit),但它是未来卖出日的最优买入价,所以只需更新 minPrice
  • 当天不是新低,minPrice 保持不变,当天价格减 minPrice 就是它作为卖出日的最优利润

不需要任何嵌套循环,一遍走完,minPrice 始终是"今天之前的最便宜买入价",maxProfit 始终是"到目前为止的最大利润"。

完整执行过程

prices = [7, 1, 5, 3, 6, 4] 为例:

复制代码
初始:minPrice = 7(第0天),maxProfit = 0

i=1, 价格1:1 < 7 → 更新 minPrice = 1
  minPrice = 1, maxProfit = 0

i=2, 价格5:5 > 1 → 利润 5 - 1 = 4 > 0 → 更新
  minPrice = 1, maxProfit = 4

i=3, 价格3:3 > 1 → 利润 3 - 1 = 2 < 4 → 不更新
  minPrice = 1, maxProfit = 4

i=4, 价格6:6 > 1 → 利润 6 - 1 = 5 > 4 → 更新
  minPrice = 1, maxProfit = 5

i=5, 价格4:4 > 1 → 利润 4 - 1 = 3 < 5 → 不更新
  minPrice = 1, maxProfit = 5

遍历结束 → 返回 5 ✓
(对应第1天买入价1,第4天卖出价6)

再走一个递减数组 [7, 6, 4, 3, 1] 的例子(验证不获利的情况):

复制代码
初始:minPrice = 7,maxProfit = 0

i=1, 价格6:6 < 7 → minPrice = 6
i=2, 价格4:4 < 6 → minPrice = 4
i=3, 价格3:3 < 4 → minPrice = 3
i=4, 价格1:1 < 3 → minPrice = 1

全程走的是 if 分支,maxProfit 从未被更新 → 返回 0 ✓
(一直跌,怎么买都亏,不如不交易)
复杂度分析

时间复杂度 O(n):只遍历数组一遍,每个元素只做一次比较(要么更新 minPrice,要么更新 maxProfit)。

空间复杂度 O(1):只用了两个变量。

对比暴力解:O(n²) → O(n),空间不变。这是"用观察换循环"的典型例子------发现"最优买入日 = 历史最低价日"之后,内层循环整个被一个变量取代。


五、总结

买卖股票的最佳时机的核心思路:

  1. 规则:买入必须在卖出之前,只能交易一次
  2. 暴力:枚举所有(买入日, 卖出日)组合,O(n²)
  3. 核心观察 :买入价更高的方案被更便宜的买入日全面覆盖 → 每个卖出日的最优买入日,就是它前面的历史最低价日
  4. 贪心策略 :一次遍历,同时维护历史最低价 minPrice 和最大利润 maxProfit
  5. 分支互斥:当天是新低就只更新 minPrice(它是未来卖出日的最佳买入价);当天更高就算利润更新答案

贪心思想在本题的体现:每一步(每个卖出日)都取局部最优(配前面的最低价),由于"卖出价固定时买入价越低利润越大"这个单调关系,局部最优叠加起来就是全局最优。

这个"维护前缀最小值"的套路非常常用:遇到"枚举 j,求 a[j] - min(a[0..j-1]) 型"的问题,都可以用同样的方式把 O(n²) 压成 O(n)。

相关推荐
凤山老林1 小时前
企业级文件服务中台:Spring Boot 集成 MinIO 实现分片存储、生命周期管理与多租户隔离
java·spring boot·后端·minio·文件服务
程序员良辰1 小时前
Eclipse Memory Analyzer(MAT)入门教程:从生成 Heap Dump 到定位 Java 内存问题
java·ide·eclipse·tomcat·idea
深圳市益普科技有限公司1 小时前
半导体MES的数字孪生:虚拟调试如何把上线风险提前清零
java·开发语言
星栖与芯1 小时前
STM32MP157 M4 指针避坑(三):生命周期与内存踩踏——HardFault 重灾区
java·网络·stm32
Maynor9961 小时前
「原子弹爆炸」级别:Astra 复刻游戏合集(含实机截图)
java·linux·运维·数据库·gpt·游戏
wlkdb1 小时前
大模型/Agent 时代下的视觉算法
图像处理·算法·计算机视觉
CV艺术家1 小时前
openssL生成免费的证书
java·linux·服务器
sali-tec1 小时前
C# 基于OpenCv的视觉工作流-章107-光流追踪
图像处理·人工智能·opencv·算法·计算机视觉
全速向光1 小时前
手机玩我的世界Java版:FCL启动器下载使用指南
java·智能手机·游戏程序