LeetCode 121. 买卖股票的最佳时机:一次遍历搞定的贪心解法

0 阅读6分钟

大家好!今天来拆解 LeetCode 上一道经典的贪心算法题目——121. 买卖股票的最佳时机。这道题是股票系列题目的入门题,思路简洁但很有代表性,学会它能为后续解决更复杂的股票问题打下基础。下面我们从题目理解、解题思路、代码解析到边界情况分析,一步步把这道题吃透。

一、题目回顾

先明确题目要求,避免理解偏差:

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

  • 只能进行一次交易:选择某一天买入,然后在未来的某一个不同日子卖出(买入在前,卖出在后)。

  • 目标:计算能获取的最大利润;如果无法获取利润(比如股价一直下跌),返回 0。

举两个简单例子帮助理解:

  • 示例 1:输入 [7,1,5,3,6,4],输出 5。解释:第 2 天买入(价格 1),第 5 天卖出(价格 6),利润 6-1=5。

  • 示例 2:输入 [7,6,4,3,1],输出 0。解释:股价一直下跌,任何时候买入卖出都亏,所以返回 0。

二、解题思路:贪心策略 + 一次遍历

拿到这道题,首先想到的暴力解法是:遍历所有可能的买入-卖出组合,计算利润并取最大值。但暴力解法的时间复杂度是 O(n²),对于 n 较大的情况(比如 LeetCode 测试用例中 n 可能达到 10⁵),会超时。

那有没有更高效的方法?答案是肯定的——用贪心策略,只需一次遍历(O(n) 时间复杂度)就能解决。

核心思路很简单:在遍历过程中,始终记住当前遇到的最低股价(最佳买入点),同时计算当前股价与最低股价的差值(当前可获得的最大利润),不断更新最大利润值

为什么这个思路可行?因为股票交易要求“先买后卖”,对于每一天来说,能获得的最大利润必然是用当前股价减去之前所有天数中的最低股价(如果当前股价比最低股价高的话)。如果当前股价比最低股价低,说明当前天更适合作为买入点,就更新最低股价。这样遍历一遍,就能找到全局的最大利润。

三、代码实现与逐行解析

下面是题目中给出的 TypeScript 代码,我们逐行拆解,搞懂每一步的作用:


function maxProfit(prices: number[]): number {
  // 1. 获取价格数组长度,避免循环中重复计算长度(小优化)
  const pL = prices.length;
  // 2. 初始化最小价格为无穷大(Infinity)
  let minP = Infinity;
  // 3. 初始化最大利润为 0(无利润时直接返回 0)
  let res = 0;
  // 4. 遍历每一天的股票价格
  for (let i = 0; i < pL; i++) {
    // 5. 如果当前股价比记录的最低价格还低,更新最低价格
    if (prices[i] < minP) {
      minP = prices[i];
    } else {
      // 6. 否则,计算当前股价与最低价格的差值(利润),更新最大利润
      res = Math.max(res, prices[i] - minP);
    }
  }
  // 7. 返回最大利润
  return res;
};

关键代码解析(重点看这几句)

  1. let minP = Infinity;:初始化最小价格为无穷大,这是一个小技巧。因为数组中的股价都是正数,所以第一个股价必然会小于无穷大,从而成功更新 minP,保证后续计算的有效性。

  2. if (prices[i] < minP) minP = prices[i];:遇到更低的股价,就更新买入点。这是贪心的核心——始终选择当前最优的买入时机。

  3. res = Math.max(res, prices[i] - minP);:如果当前股价不低于最低股价,就计算当前利润,并和之前记录的最大利润比较,取较大值更新 res。这样就能保证 res始终是遍历到当前为止的最大利润。

四、复杂度分析

  • 时间复杂度:O(n)。只对价格数组进行了一次遍历,n 是数组的长度。

  • 空间复杂度:O(1)。只使用了 3 个变量(pL、minP、res),额外空间开销与数组长度无关。

这个复杂度是最优的,因为要计算最大利润,至少需要遍历一次数组(否则无法获取所有股价信息)。

五、边界案例测试

好的算法需要能处理各种边界情况,我们来测试几个特殊场景:

  1. 边界情况 1:数组长度为 1。输入 [5],此时无法完成“买入+卖出”的交易,返回 0。代码中循环不执行,res 初始为 0,正确。

  2. 边界情况 2:股价一直下跌。输入 [7,6,4,3,1],每次遍历都会更新 minP(因为当前股价始终比之前的 minP 小),else 分支从未执行,res 保持 0,正确。

  3. 边界情况 3:股价一直上涨。输入 [1,2,3,4,5],minP 始终是 1,每次遍历都会计算当前利润(2-1=1、3-1=2、4-1=3、5-1=4),res 不断更新为更大值,最终返回 4,正确。

  4. 边界情况 4:股价先跌后涨再跌。输入 [3,2,6,5,0,3],过程如下: 最终返回 4,正确(最佳交易是第 4 天买入,第 5 天卖出?不,等一下——第 4 天价格 0,第 5 天 3,利润 3;而第 1 天 2 买入,第 2 天 6 卖出利润 4,确实是最大的)。

    • i=0(3):3 < 无穷大 → minP=3,res=0;

    • i=1(2):2 < 3 → minP=2,res=0;

    • i=2(6):6 > 2 → 利润 4,res=4;

    • i=3(5):5 > 2 → 利润 3,res 保持 4;

    • i=4(0):0 < 2 → minP=0,res=4;

    • i=5(3):3 > 0 → 利润 3,res 保持 4;

六、总结与拓展

这道题的核心是“贪心思想”——抓住当前的最优解(最低买入价),从而推导全局的最优解(最大利润)。一次遍历的解法不仅高效,而且逻辑清晰,容易理解和实现。

拓展思考:如果题目允许进行多次交易(比如 LeetCode 122 题),这个思路还能用吗?答案是可以,但需要调整——多次交易的核心是“低买高卖,见好就收”,本质上还是贪心策略的延伸。大家可以尝试思考一下 122 题的解法,巩固这个思路。

最后,建议大家亲自把这段代码敲一遍,再测试几个不同的用例,加深对思路的理解。如果有疑问,欢迎在评论区交流~