题目描述

✅ 122. 买卖股票的最佳时机 II

image-20260928194624895

image-20260928194624896

题意分析

prices[i] 是第 i 天的股票价格,可以完成任意多次买入和卖出,但任何时刻最多持有一份股票,必须卖掉已有股票后才能继续买入。要求返回最终可以获得的最大利润,也可以选择完全不交易。

与只能交易一次的版本不同,本题可以利用多段上涨。题目没有手续费、冷冻期或交易次数上限,同一天买入再卖出也不会产生额外收益;这些条件决定了各段交易可以自由衔接。

解法:贪心累加所有上涨差价

核心思路

[!blue]

先看一笔在第 a 天买入、第 b 天卖出的交易。利润 prices[b] - prices[a] 等于持有期间所有相邻日差价之和,因为中间各天的价格会一加一减相互抵消。

每个相邻日差价为正时,持有股票可以得到这部分上涨收益;差价为负时,持有反而承担亏损。因此,任意合法交易方案的总利润,都不可能超过“所有正相邻差价之和”:一份股票只能让每段差价贡献一次,负差价也不可能提高收益。

这个上界可以实际达到。把每一段连续上涨看作一次交易,在上涨开始前买入、上涨结束时卖出,获得的就是这一段全部正差价之和;下跌时不持仓,持平时是否合并相邻交易都不影响利润。各段按时间先后完成,始终不会同时持有多份股票。

既然正差价之和既是所有方案的上界,又有合法方案达到,就可以直接累加每一天相对前一天的正差价,不必显式保存买入、卖出的日期。

解题步骤

  1. 初始化累计利润 ans = 0。
  2. 从第二天开始,比较当天价格与前一天价格。
  3. 若价格上涨,将 prices[i] - prices[i - 1] 加入答案;下跌或持平则不增加收益。
  4. 遍历结束后返回 ans。只有一天,或整个过程中都没有上涨时,答案自然为 0。

代码实现

class Solution {
    public int maxProfit(int[] prices) {
        int ans = 0;

        for (int i = 1; i < prices.length; i++) {
            // 连续上涨的整段利润等于这些正相邻差价之和。
            if (prices[i] > prices[i - 1]) {
                ans += prices[i] - prices[i - 1];
            }
        }

        return ans;
    }
}
func maxProfit(prices []int) int {
    ans := 0
    for i := 1; i < len(prices); i++ {
        // 连续上涨的整段利润等于这些正相邻差价之和。
        if prices[i] > prices[i-1] {
            ans += prices[i] - prices[i-1]
        }
    }
    return ans
}

复杂度分析

  • 时间复杂度:$O(n)$,只遍历价格数组一次。
  • 空间复杂度:$O(1)$。

关键点总结

[!green]

  • 一笔交易的首尾差价可以拆成持有期间的所有相邻差价。
  • 正差价之和构成利润上界,按连续上涨段买卖又能达到这个上界。
  • 计算时累加每日上涨,执行时可以合并为整段交易,两者收益完全相同。

解法:持有与空仓动态规划

核心思路

[!blue]

除了利用本题的贪心性质,也可以按每天结束时是否持有股票划分状态。cash 表示截至当天、手中没有股票时的最大现金余额;hold 表示截至当天、持有一份股票时的最大现金余额,其中已经扣除了买入成本。两个状态保存各自条件下的最优方案,不要求来自同一条交易过程。

今天空仓有两种来源:昨天就空仓,今天不操作;或者昨天持有,今天以 price 卖出。因此 cash = max(oldCash, oldHold + price)。

今天持有也有两种来源:昨天就持有,今天继续持有;或者昨天空仓,今天以 price 买入。因此 hold = max(oldHold, oldCash - price)。买入只能从空仓状态转移,保证不会同时持有多份股票;卖出之后仍可再次买入,所以没有交易次数限制。

第一天结束时,不买入的余额为 0,买入后的余额为 -prices[0]。以后每天按上述两式更新。当天反复买卖没有额外收益,因此这两种状态已经覆盖最优决策;最终返回 cash,代表股票已经卖出或从未买入的实际利润。

解题步骤

  1. 用第一天初始化 cash = 0、hold = -prices[0]。
  2. 从第二天开始,先保存前一天的 oldCash、oldHold。
  3. 比较继续空仓与今天卖出,得到新的 cash。
  4. 比较继续持有与今天买入,得到新的 hold。
  5. 返回最后一天的 cash。

代码实现

class Solution {
    public int maxProfit(int[] prices) {
        int cash = 0;
        int hold = -prices[0];

        for (int i = 1; i < prices.length; i++) {
            int oldCash = cash;
            int oldHold = hold;
            cash = Math.max(oldCash, oldHold + prices[i]);
            hold = Math.max(oldHold, oldCash - prices[i]);
        }

        return cash;
    }
}
func maxProfit(prices []int) int {
    cash := 0
    hold := -prices[0]

    for i := 1; i < len(prices); i++ {
        oldCash, oldHold := cash, hold
        cash = max(oldCash, oldHold+prices[i])
        hold = max(oldHold, oldCash-prices[i])
    }

    return cash
}

复杂度分析

  • 时间复杂度:$O(n)$,每天只更新两个状态。
  • 空间复杂度:$O(1)$,只保留前一天状态和更新所需的临时值。

关键点总结

[!green]

  • 用是否持仓区分后续能否买入或卖出,同一天的不同方案分别保留最优余额。
  • 卖出给余额加上当天价格,买入从余额扣除当天价格。
  • 贪心直接收集所有上涨,动态规划显式描述持仓决策,两种方法在本题得到同一个最优值。

易错点总结

[!yellow]

  • 套用只允许一次交易的逻辑,会遗漏卖出后重新买入获得的收益。
  • 对相邻差价取绝对值,会把下跌也算成利润;本题不能通过做空从下跌中获利。
  • 动态规划中的持有状态是扣除买入成本后的现金余额,不是股票价格,也不要求非负。
  • 两个新状态应由前一天状态转移。先保留旧值再更新,可以直接对应递推含义。
  • 最终返回空仓状态。若增加手续费、冷冻期或次数限制,应重新推导转移,不能继续无条件累加所有上涨。

相似题目

题目 难度 关联与区别
121. 买卖股票的最佳时机 简单 去掉只能交易一次的限制后,可以在多个上涨段分别获利。
714. 买卖股票的最佳时机含手续费 中等 加入每笔交易手续费后,不能简单把所有正差值相加,需要持有与空仓状态。
123. 买卖股票的最佳时机 III 困难 以持有状态和交易次数描述每天的最优收益;本题允许重复买卖,该题最多完成两笔交易。
188. 买卖股票的最佳时机 IV 困难 以持有状态和交易次数描述每天的最优收益;本题允许重复买卖,该题推广到 k 笔交易。
309. 买卖股票的最佳时机含冷冻期 中等 以持有状态和交易次数描述每天的最优收益;本题允许重复买卖,该题卖出后增加冷冻限制。
转载与许可
作者
链接 https://hgnulb.github.io/blog/2026/91892556
许可 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!