LeetCode 122. 买卖股票的最佳时机 II:贪心进阶,多次交易的最优解

47 阅读8分钟

大家好!在上一篇博客中,我们搞定了股票系列的入门题 121. 买卖股票的最佳时机(一次交易限制)。今天我们趁热打铁,拆解它的进阶版——122. 买卖股票的最佳时机 II。这道题取消了“一次交易”的限制,允许多次买卖,核心思路依然是贪心,但策略需要稍作调整。下面我们还是从题目理解、思路分析、代码拆解到边界测试,一步步把这道题讲透。

一、题目回顾:核心差异要分清

先明确 122 题的要求,重点对比 121 题找差异:

  • 给定数组 pricesprices[i] 表示第 i 天的股票价格。

  • 交易规则升级:可多次买卖,但任何时候最多持有 1 股(必须先卖出再买入,不能持仓多股);甚至可在同一天多次买卖(比如当天买当天卖,虽利润为 0 但不违规)。

  • 目标:获取最大利润(无利润时仍返回 0)。

用两个示例直观理解:

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

  • 示例 2:输入 [1,2,3,4,5],输出 4。解释:第 1 天买→第 2 天卖(1)、第 2 天买→第 3 天卖(1)、第 3 天买→第 4 天卖(1)、第 4 天买→第 5 天卖(1),总利润 4;或直接第 1 天买→第 5 天卖(利润 4),结果一致。

  • 示例 3:输入 [7,6,4,3,1],输出 0。解释:股价持续下跌,任何买卖都亏,不交易最赚。

核心差异总结:121 题是“单次交易找最大差价”,122 题是“多次交易累加所有正向差价”——这是贪心策略调整的关键。

二、解题思路:贪心升级,赚尽每一分正向差价

先思考暴力解法:遍历所有可能的交易组合(多次买入卖出的排列),计算总利润取最大值。但暴力解法时间复杂度是 O(2ⁿ)(每两天都有“买/卖”选择),n 稍大就会超时,显然不可行。

回到贪心思路——121 题是“找全局最大差价”,122 题该怎么调整?其实核心更简单:只要后一天的股价比前一天高,就进行一次“当天买、次日卖”的交易,把这部分正向差价累加起来,总利润就是最大的

为什么这个思路成立?我们可以把股价走势想象成“上坡和下坡”:

  • 上坡段(后一天 > 前一天):每一段上坡的差价都是能赚到的利润,累加所有上坡差价就是最大利润。比如 [1,2,3,4],上坡差价是 1(2-1)+1(3-2)+1(4-3)=3,和直接 4-1=3 结果一样。

  • 下坡段(后一天 ≤ 前一天):不交易,避免亏损。

再对应到题目给出的代码思路:代码中用 minP 记录“当前持仓的买入价”,如果当天股价 > minP,就把差价加入利润(相当于卖出),然后立即更新 minP 为当天股价(相当于当天又买入,为后续差价做准备);如果当天股价 ≤ minP,就更新 minP 为当天股价(换更低的买入价)。本质上,这和“累加所有正向差价”是等价的,只是实现方式更简洁。

三、代码实现与逐行解析

下面是题目给出的 TypeScript 代码,我们逐行拆解,同时对比 121 题的代码找不同:


function maxProfit(prices: number[]): number {
  // 1. 获取数组长度,避免循环中重复计算(和 121 题一致)
  const pL = prices.length;
  // 2. 初始化最小买入价为无穷大(和 121 题一致)
  let minP = Infinity;
  // 3. 初始化最大利润为 0(和 121 题一致)
  let res = 0;
  // 4. 遍历每一天的股价
  for (let i = 0; i < pL; i++) {
    // 5. 核心差异:只要当前股价 > 最小买入价,就累加差价(相当于卖出)
    if (prices[i] > minP) {
      res += prices[i] - minP;
    }
    // 6. 核心差异:无论是否卖出,都更新最小买入价为当前股价(相当于重新买入/换更低买入价)
    minP = prices[i];
  }
  // 7. 返回最大利润
  return res;
};

关键代码解析(重点看和 121 题的差异)

  1. 初始化部分(pLminPres):和 121 题完全一致,minP = Infinity 确保第一天股价能正常更新买入价。

  2. 核心判断 if (prices[i] > minP) res += prices[i] - minP;

    • 和 121 题的“取最大值更新 res”不同,这里是“累加差价”——因为允许多次交易,每一次正向差价都要赚到手。

    • 比如输入 [1,2,3,4]:i=1 时 2>1,res +=1(res=1);i=2 时 3>2,res +=1(res=2);i=3 时 4>3,res +=1(res=3),最终得到总利润 3。

  3. 强制更新 minP = prices[i]

    • 这是 121 题没有的关键步骤!无论当天是否卖出,都把买入价更新为当前股价。

    • 如果当天卖出了(prices[i] > minP):更新 minP 相当于“当天卖出后立即以当前价买入”,为后续计算下一段差价做准备。

    • 如果当天没卖出(prices[i] ≤ minP):更新 minP 相当于“换更低的买入价”,保证后续能赚到更多差价。

和 121 题代码核心差异总结

对比维度121 题(单次交易)122 题(多次交易)
利润更新方式res = Math.max(res, 差价)(取全局最大)res += 差价(累加所有正向差价)
minP 更新时机仅当当前股价 < minP 时更新(保留历史最低买入价)每次循环都更新(卖出后重新买入/换更低买入价)
核心逻辑找“一次买入-卖出”的最大差价赚尽每一段“后一天>前一天”的差价

四、复杂度分析

和 121 题一样,这是最优复杂度:

  • 时间复杂度:O(n)。仅遍历数组一次,n 为数组长度,无额外嵌套循环。

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

为什么这是最优?因为要统计所有正向差价,必须遍历一次数组获取所有股价的先后关系,无法做到比 O(n) 更优。

五、边界案例测试

用极端场景验证代码的鲁棒性:

  1. 边界情况 1:数组长度为 1。输入 [5],无法完成任何交易(买入后无卖出时机),循环中 minP 被更新为 5,但无差价累加,res 保持 0,正确。

  2. 边界情况 2:股价持续下跌。输入 [7,6,4,3,1],每次循环中 prices[i] ≤ minP,无差价累加,res 始终为 0,正确。

  3. 边界情况 3:股价持续上涨。输入 [1,2,3,4,5],每次循环都累加差价(1+1+1+1=4),最终返回 4,正确。

  4. 边界情况 4:股价波动剧烈(有涨有跌)。输入 [3,2,6,5,0,3],过程拆解: 最终返回 7,正确(最佳交易:2→6 赚 4,0→3 赚 3,总 7)。

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

    • i=1(2):2 < 3 → 不累加,minP=2;

    • i=2(6):6 > 2 → res +=4(res=4),minP=6;

    • i=3(5):5 < 6 → 不累加,minP=5;

    • i=4(0):0 < 5 → 不累加,minP=0;

    • i=5(3):3 > 0 → res +=3(res=7),minP=3;

  5. 边界情况 5:同一天买卖。输入 [2,2,2],每次循环中 prices[i] = minP,无差价累加,res=0,正确(多次买卖无利润,和不交易一致)。

六、总结与拓展

122 题的核心是“贪心策略的进阶应用”——从 121 题的“找全局最大”升级为“赚尽局部所有正向收益”。解题的关键是想通:多次交易的最大利润,本质就是累加所有“后一天股价 > 前一天股价”的差价,而题目给出的代码用“动态更新买入价”的方式,简洁高效地实现了这个逻辑。

拓展思考:

  • 如果题目再加限制(比如最多交易 2 次、有冷冻期、有手续费),贪心还能用吗?大部分情况需要用动态规划(DP)来解决,因为限制条件会让“局部最优”无法推导“全局最优”。

  • 122 题的解法还有一个形象的名字——“峰谷法”:把股价的每个低点(谷)作为买入点,每个高点(峰)作为卖出点,累加峰谷差价就是最大利润。题目代码的逻辑和峰谷法完全等价,只是实现更简洁。

最后建议:把 121 题和 122 题的代码放在一起对比,仔细体会“单次交易”和“多次交易”在贪心策略上的差异,这能帮你更好地理解贪心算法的“灵活适配性”。如果有疑问,欢迎在评论区交流~