LeetCode 121:买卖股票的最佳时机(贪心算法) —— 题解

👋 欢迎阅读

🎯 欢迎来到「买卖股票的最佳时机」题解之旅! 本文将带你从“寻找最佳买入卖出时机”这一投资场景出发,深入理解贪心 + 一次遍历的简洁解法.

在开始之前,建议你先:

  • 了解题目背景:这是 LeetCode 121 题,给定股票每日价格数组 prices,你只能买卖一次(先买后卖),求能获得的最大利润。本质上是找出一对 (买入日, 卖出日),使得卖出价与买入价的差值最大,且买入日在卖出日之前。

  • 明确学习目标:掌握如何用贪心法维护「历史最低价格」和「当前最大利润」,理解为什么只需一趟遍历即可得到最优解,并熟练处理价格下跌无利润(返回 0)的边界情况。

  • 准备好环境:建议在本地 IDE 或 LeetCode 在线编辑器中打开代码,边看边运行,亲手验证示例(如 prices = [7,1,5,3,6,4] 输出 5)。

本文将从问题转化、贪心策略设计、双变量模拟到代码实现,层层递进。即使你对贪心算法还不熟悉,我们也会从“每天更新最低买入价,并计算当天卖出的利润”这一直觉出发,让你轻松抓住核心思想——最低点买入,最高点卖出(但卖出要在买入之后)。现在,让我们一起在股票价格的起伏中抓住最佳交易时机吧! 📈💰


一、题目

121. 买卖股票的最佳时机 - 力扣(LeetCode)

二、做题思路

1. 问题分析(前置分析)

只能在 某一天买入,并在 未来的某一天卖出,求最大利润。核心是:对于每个卖出日,买入价应当是 之前所有天中的最低价格,这样当天的利润才最大。因此只需在遍历过程中维护历史最低价,并不断计算当前价格与历史最低价的差值,取最大值即可。


2. 贪心策略(核心决策规则)

  • 初始化:minPrice = prices[0](第一天价格作为初始最低价),maxProfit = 0

  • 从第二天开始遍历:

    • 计算 当天利润 = prices[i] - minPrice,更新 maxProfit = max(maxProfit, 当天利润)

    • 更新 历史最低价 minPrice = min(minPrice, prices[i]),供后续天数使用。

  • 遍历结束后,maxProfit 即为最大利润。


3. 正确性说明(简单版本)

对于每一天 i,若要在这一天卖出,那么最优的买入日必然是之前价格最低的那一天,这样利润最大。因此,只需在遍历中不断更新历史最低价,并计算当天卖出能获得的最大利润,就能覆盖所有可能的卖出时机。


4. 实现细节(边界防护)

  • 数组长度至少为 1,初始化 minPriceprices[0]

  • i=1 开始遍历,先更新 maxProfit,再更新 minPrice(顺序可互换,但先计算利润再更新最低价不影响结果)。

  • 若价格一直下跌,maxProfit 保持为 0,符合题意(允许不交易)。


5. 返回值(目标映射)

返回 maxProfit,即能够获得的最大利润

四、代码

class Solution 
{
public:
    int maxProfit(vector<int>& prices) 
    {
        // 贪心策略:在每一天,假设当天卖出股票,那么买入价应为之前所有天中的最低价,
        // 这样当天的利润 = 当天价格 - 历史最低价,取所有天利润的最大值即为答案。
    // 初始化:历史最低价为第一天的价格,最大利润初始为0(因为允许不交易)
    int minPrice = prices[0];   // 截止当前日期之前的最低买入价
    int maxProfit = 0;          // 当前能获得的最大利润
// 从第二天开始遍历(第一天无法卖出,利润为0)
for (int i = 1; i &amp;lt; prices.size(); i++) 
{
    // 计算如果今天卖出能获得的利润,并更新最大利润
    maxProfit = max(maxProfit, prices[i] - minPrice);
// 更新历史最低价(为后续天数的卖出做准备)
minPrice = min(minPrice, prices[i]);
}
// 返回最大利润
return maxProfit;
}
};

五、流程图

六、正确性说明(详细版)

步骤 1:符号与问题建模
+----------------------------------------------------+
|  输入价格数组 prices,长度 n                       |
|  交易规则:只能买卖一次,卖出日在买入日之后        |
|  目标:最大化卖出价 - 买入价(若为负则取 0)       |
+----------------------------------------------------+
                    |
                    v
+----------------------------------------------------+
|  贪心策略:遍历每一天,假设当天卖出股票,          |
|  买入价应为截止前一天为止的**历史最低价**。        |
|  更新最大利润 = max(最大利润, 当天价格 - 历史最低价) |
|  然后更新历史最低价 = min(历史最低价, 当天价格)。   |
|  贪心实质:每天选择之前最低价买入,是当天能获得的  |
|  最大利润,且该决策不会影响未来,因为买入价越低越好。|
+----------------------------------------------------+
  • minPrice 为截至当前日期(不包括当天)之前出现的最低价格。

  • maxProfit 为已遍历的天数中能获得的最大利润。

  • 对每一天 i,若当天卖出,则最大可能利润为 prices[i] - minPrice,取所有天数的最大值即为答案。

  • 贪心选择性质:在任意一天卖出时,最优买入价必然是之前所有价格中的最小值,因为利润 = 卖出价 - 买入价,卖出价固定时,买入价越小利润越大。因此每天维护历史最低价是合理的。


步骤 2:关键性质 —— 最优解必包含某一天的历史最低价买入(反证法)
+----------------------------------------------------+
|  性质 1:对任意一天 i,若当天卖出,则最优买入价   |
|  必须是 min{prices[0..i-1]}。                      |
|  反证:若买入价不是最低,则用更低价格替换买入价,  |
|  利润会严格增大,且仍满足买入日早于卖出日,矛盾。  |
+----------------------------------------------------+
                     |
                     v
+----------------------------------------------------+
|  性质 2:全局最优利润等于 max_{i} (prices[i] -     |
|  min_{j < i} prices[j])。                          |
|  因为最优交易必然在某一天卖出,而对应的最优买入价  |
|  就是该天之前的最小值。                            |
+----------------------------------------------------+
                     |
                     v
+----------------------------------------------------+
|  性质 3:贪心算法在扫描过程中逐步计算了所有 i      |
|  对应的上述表达式,并取最大值,因此不会遗漏任何    |
|  潜在的更优交易。                                  |
+----------------------------------------------------+

详细论证(结合反证法)

  • 反证:假设在某个最优解中,卖出日为 i,但买入价不是 prices[0..i-1] 中的最小值,设最小值出现在 jj < i)且 prices[j] < 当前买入价。用 prices[j] 替换原买入价,由于卖出价不变,新利润 = prices[i] - prices[j] > prices[i] - 原买入价,且 j < i,满足交易条件,故原解不是最优,矛盾。因此,对于任意固定的卖出日,最优买入价一定是之前的最小值。

  • 因此,全局最大利润必然等于所有卖出日对应的“当天价格 - 当日之前最低价”的最大值。贪心算法在遍历过程中,恰好通过 minPrice 维护了每个位置之前的最小值,并计算了每个位置的候选利润,再取最大值。所以,贪心算法的结果与最优解定义完全一致,不会遗漏任何可能。


步骤 3:归纳证明 —— 扫描过程逐步得到正确答案
+----------------------------------------------------+
|  初始化:minPrice = prices[0],maxProfit = 0。     |
|  假设已经处理完前 k 个元素(索引 0..k-1),        |
|  minPrice 为前 k 个元素的最小值,                  |
|  maxProfit 为前 k 个元素中能够获得的最大利润。      |
+----------------------------------------------------+
                     |
                     v
+----------------------------------------------------+
|  处理第 k 个元素(索引 k):                       |
|  ① 计算如果今天卖出,利润 = prices[k] - minPrice; |
|  ② 更新 maxProfit = max(maxProfit, 利润);        |
|  ③ 更新 minPrice = min(minPrice, prices[k])。     |
|  更新后,minPrice 为前 k+1 个元素的最小值,        |
|  maxProfit 为前 k+1 个元素的最大利润,             |
|  归纳假设保持。                                    |
+----------------------------------------------------+
                     |
                     v
+----------------------------------------------------+
|  遍历结束后,maxProfit 即为所有天数的最大利润。    |
|  因为每一步都正确维护了前 i 个元素的最优值,       |
|  最终结果与步骤 2 中的公式一致。                   |
+----------------------------------------------------+

详细论证

  • 归纳基础:在遍历前,minPrice = prices[0]maxProfit = 0。若数组长度为1,不能交易,返回0,正确。

  • 归纳步骤:假设已经处理完前 i 个元素(0..i-1),minPrice 是它们的最小值,maxProfit 是前 i 个元素内的最大利润(允许不交易)。现在考虑第 i 个元素(prices[i]):

    • 如果今天卖出,最大利润为 prices[i] - minPrice,因为 minPrice 是之前的最小买入价,由步骤2可知这是当天的最优卖出利润。

    • 更新 maxProfit 为原先最大值与今天候选值中的较大者,则 maxProfit 变为前 i+1 个元素的最大利润。

    • 更新 minPricemin(minPrice, prices[i]),则 minPrice 变为前 i+1 个元素的最小值。

  • 由于归纳假设成立,遍历结束后,maxProfit 即整个数组的最大利润。若所有价格递减,则 maxProfit 始终为0,正确。

🎯 闭幕

动态规划学习路径图

🎉 恭喜你完成了「买卖股票的最佳时机」问题的学习!

为了巩固知识并进一步拓展,建议你:

🚀 动手实践
在 LeetCode 上提交代码,尝试不同的测试用例。

💡 深入思考

  • 本题采用 贪心策略:遍历每一天,假设当天卖出,则之前的最低买入价就是最优买入价。为什么记录“历史最低价”就能保证当前卖出的利润最大? 如果买入价不是历史最低,利润会更大还是更小?

  • 代码中 maxProfit 初始化为 0,即使价格一直下跌,最后也会返回 0(表示不交易)。如果题目要求必须进行一次交易(买入和卖出),初始化应该怎么改?

  • 遍历从 i=1 开始,第一天只更新 minPrice 而不计算利润。如果数组长度为 1,代码能正确返回 0 吗?

  • 如果股票价格可能为 负数(虽然题目保证非负),贪心策略是否仍然有效?为什么?

📚 延伸挑战

  • 如果不仅要返回最大利润,还要返回 具体的买入和卖出日期,你如何在贪心过程中记录下标?

  • 对比本题与 最大子数组和(LeetCode 53)的相似之处:两者都是求最大差值(前者是价格差,后者是连续和),你能用最大子数组和的思想重新实现本题吗?(提示:将价格转换为每日涨跌幅)

如果你觉得本文对你有所帮助,欢迎:

👍 点赞 / 收藏
👤 关注作者,获取更多题解
💬 留言交流你的疑问或优化思路

祝你在 算法之路 上越走越稳,早日攻克每一道难题!下次见 🚀✨

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值