【LeetCode Hot100 刷题日记 (77/100)】121. 买卖股票的最佳时机 —— 贪心算法 & 一次遍历优化 💰

2 阅读4分钟

📌 题目链接:121. 买卖股票的最佳时机 - 力扣(LeetCode)

🔍 难度:简单 | 🏷️ 标签:数组、贪心、动态规划思想、一次遍历

⏱️ 目标时间复杂度:O(n)

💾 空间复杂度:O(1)


🧠 题目分析

本题要求在仅允许一次买入和一次卖出的前提下,找出能获得的最大利润。关键约束如下:

  • 必须先买入再卖出(即 i < j);
  • 只能交易一次(不能多次买卖);
  • 若无法获利(如价格持续下跌),则返回 0

这看似是一个“找最大差值”的问题,但差值必须满足后项 > 前项且索引递增,因此不能简单用 max - min

💡 面试高频点:此题是“股票系列”第一题,后续还有 2~6 道变体(允许多次交易、含手续费、冷冻期等),掌握本题是理解整个系列的基础!


⚙️ 核心算法及代码讲解

✅ 核心思想:贪心 + 一次遍历

我们不需要知道具体哪天买、哪天卖,只需在遍历过程中:

  1. 记录到目前为止的最低价格(minprice) —— 这代表“如果我在今天之前买,最便宜是多少”;
  2. 计算今天卖出能获得的利润(price - minprice)
  3. 更新全局最大利润(maxprofit)

🌟 为什么这是贪心?
因为我们每一步都做出局部最优选择:假设“以历史最低价买入”,然后看今天卖出是否更优。虽然我们不知道未来价格,但通过不断更新 minpricemaxprofit,最终能得到全局最优解。

📌 与动态规划的关系
此解法可视为 DP 的空间优化版本。标准 DP 定义 dp[i][0/1] 表示第 i 天持有/不持有股票的最大利润,但本题状态转移只依赖前一天,故可压缩为两个变量。

🔍 代码逐行注释(C++)

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        // 初始化 minprice 为一个极大值(比题目中 prices[i] <= 1e4 更大)
        int inf = 1e9;
        int minprice = inf;   // 记录遍历到当前位置时的历史最低价格
        int maxprofit = 0;    // 记录全局最大利润

        // 遍历每一天的价格
        for (int price : prices) {
            // 先尝试在今天卖出:利润 = 当前价格 - 历史最低买入价
            maxprofit = max(maxprofit, price - minprice);
            // 再更新历史最低价(注意顺序!必须先计算利润再更新 minprice)
            minprice = min(price, minprice);
        }
        return maxprofit;
    }
};

⚠️ 关键细节
必须先计算 maxprofit,再更新 minprice
如果顺序颠倒,会导致当天价格既作为买入又作为卖出(即 price - price = 0),虽然不影响结果(因为 maxprofit 不会变小),但逻辑上错误。更重要的是,在某些变体题中(如限制交易次数),顺序错误会导致严重 bug。


🧩 解题思路(分步拆解)

  1. 初始化:设 minprice = +∞(确保第一天价格能更新它),maxprofit = 0

  2. 遍历每一天

    • 步骤 A:假设今天卖出,计算利润 price - minprice,并更新 maxprofit
    • 步骤 B:将今天的 priceminprice 比较,更新历史最低价。
  3. 返回结果:遍历结束后,maxprofit 即为答案。

📊 以示例 [7,1,5,3,6,4] 为例

DayPriceminprice (before update)profit (price - minprice)maxprofitminprice (after update)
077 - ∞ → 负数(忽略)07
1171 - 7 = -601
2515 - 1 = 441
3313 - 1 = 241
4616 - 1 = 551
5414 - 1 = 351

✅ 最终 maxprofit = 5,正确!


📈 算法分析

项目分析
时间复杂度O(n) :仅需一次遍历数组,每个元素访问一次。
空间复杂度O(1) :仅使用两个额外变量 minpricemaxprofit
稳定性稳定,无边界问题(因 maxprofit 初始为 0,即使全下跌也返回 0)。
扩展性此思想可推广至“最多 k 次交易”等问题(需结合 DP)。

💼 面试加分项

  • 能说出“这是贪心,因为每一步都基于当前最优假设”;
  • 能对比暴力法(O(n²))并说明为何一次遍历足够;
  • 能指出“顺序不能颠倒”的细节;
  • 能联系到后续股票问题(如 #122 无限次交易可用贪心累加正差值)。

💻 代码实现

C++(保留原模板结构)

#include <bits/stdc++.h>
using namespace std;
using ll = long long;

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        int inf = 1e9;
        int minprice = inf, maxprofit = 0;
        for (int price : prices) {
            maxprofit = max(maxprofit, price - minprice);
            minprice = min(price, minprice);
        }
        return maxprofit;
    }
};

// 测试
signed main(){
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    cout.tie(nullptr);

    Solution sol;
    vector<int> prices1 = {7,1,5,3,6,4};
    cout << sol.maxProfit(prices1) << "\n"; // 输出: 5

    vector<int> prices2 = {7,6,4,3,1};
    cout << sol.maxProfit(prices2) << "\n"; // 输出: 0

    return 0;
}

JavaScript

/**
 * @param {number[]} prices
 * @return {number}
 */
var maxProfit = function(prices) {
    let minprice = Infinity;
    let maxprofit = 0;
    for (const price of prices) {
        maxprofit = Math.max(maxprofit, price - minprice);
        minprice = Math.min(price, minprice);
    }
    return maxprofit;
};

// 测试
console.log(maxProfit([7,1,5,3,6,4])); // 5
console.log(maxProfit([7,6,4,3,1]));   // 0

🌟 本期完结,下期见!🔥

👉 点赞收藏加关注,新文更新不迷路。关注专栏【算法】LeetCode Hot100刷题日记,持续为你拆解每一道热题的底层逻辑与面试技巧!

💬 欢迎留言交流你的解法或疑问!一起进步,冲向 Offer!💪

📌 记住:当你在刷题时,不要只看答案,要像写这篇文章一样,深入思考每一步背后的原理、优化空间和面试价值。这才是真正提升算法能力的方式!