Article / 文章

LeetCode 第122题:买卖股票的最佳时机 II

给你一个整数数组 prices ,其中 prices[i] 表示某支股票第 i 天的价格。 在每一天,你可以决定是否购买和/或出售股票。你在任何时候 最多 只能持有 一股 股票。你也可以先购买,然后在 同一天 出售。 返回你能获得的 最大 利润。

题目描述

给你一个整数数组 prices ,其中 prices[i] 表示某支股票第 i 天的价格。

在每一天,你可以决定是否购买和/或出售股票。你在任何时候 最多 只能持有 一股 股票。你也可以先购买,然后在 同一天 出售。

返回你能获得的 最大 利润。

难度

中等

题目链接

点击在LeetCode中查看题目

示例

示例 1:

输入:prices = [7,1,5,3,6,4]
输出:7
解释:在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出,这笔交易所能获得利润 = 5 - 1 = 4 。
     随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,这笔交易所能获得利润 = 6 - 3 = 3 。
     总利润为 4 + 3 = 7 。

示例 2:

输入:prices = [1,2,3,4,5]
输出:4
解释:在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5 - 1 = 4 。
     总利润为 4 。

示例 3:

输入:prices = [7,6,4,3,1]
输出:0
解释:在这种情况下, 交易无法获得正利润,所以不参与交易可以获得最大利润,最大利润为 0 。

提示

  • 1 <= prices.length <= 3 * 10^4
  • 0 <= prices[i] <= 10^4

解题思路

方法一:贪心算法

与第121题不同,这道题允许多次交易(买入卖出),但任何时候最多只能持有一股股票。贪心的思想是:只要今天的价格比昨天高,就在昨天买入,今天卖出,赚取差价。

关键点:

  • 只关注相邻两天的价格差
  • 只要价格上涨,就进行交易(买入卖出)
  • 累加所有正的价格差,即为最大利润

具体步骤:

  1. 初始化最大利润maxProfit为0
  2. 遍历数组prices,从索引1开始: a. 计算当天与前一天的价格差diff = prices[i] - prices[i-1] b. 如果diff > 0,则将diff加到maxProfit上
  3. 返回maxProfit

时间复杂度:O(n),其中n是数组的长度,只需要遍历一次数组 空间复杂度:O(1),只需要常数级别的额外空间

方法二:动态规划

这个问题也可以用动态规划来解决。我们可以定义两个状态:

关键点:

  • 状态定义:
    • dp[i][0]表示第i天结束时不持有股票的最大利润
    • dp[i][1]表示第i天结束时持有股票的最大利润
  • 状态转移方程:
    • dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i])
    • dp[i][1] = max(dp[i-1][1], dp[i-1][0] - prices[i])

具体步骤:

  1. 初始化dp数组,dp[0][0] = 0, dp[0][1] = -prices[0]
  2. 遍历数组prices,从索引1开始: a. 更新dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i]) b. 更新dp[i][1] = max(dp[i-1][1], dp[i-1][0] - prices[i])
  3. 返回dp[n-1][0](最后一天不持有股票的最大利润)

时间复杂度:O(n),其中n是数组的长度,只需要遍历一次数组 空间复杂度:O(n),需要一个二维dp数组

方法三:动态规划(空间优化)

我们可以发现,在方法二中,当前状态只与前一天的状态有关,因此可以使用两个变量来代替整个dp数组,从而优化空间复杂度。

关键点:

  • 使用两个变量代替dp数组:
    • cash表示当前不持有股票的最大利润
    • hold表示当前持有股票的最大利润
  • 状态转移方程保持不变

具体步骤:

  1. 初始化cash = 0, hold = -prices[0]
  2. 遍历数组prices,从索引1开始: a. 更新cash = max(cash, hold + prices[i]) b. 更新hold = max(hold, cash - prices[i])
  3. 返回cash

时间复杂度:O(n),其中n是数组的长度,只需要遍历一次数组 空间复杂度:O(1),只需要常数级别的额外空间

图解思路

方法一:贪心算法过程

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

索引 价格 价格差 是否交易 当前利润 累计利润 说明
0 7 - - 0 0 初始状态
1 1 -6 0 0 价格下跌,不交易
2 5 4 4 4 价格上涨,在第1天买入,第2天卖出
3 3 -2 0 4 价格下跌,不交易
4 6 3 3 7 价格上涨,在第3天买入,第4天卖出
5 4 -2 0 7 价格下跌,不交易

最终结果:最大利润为7

方法二:动态规划过程

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

索引 价格 dp[i][0] dp[i][1] 说明
0 7 0 -7 初始状态
1 1 0 -1 dp[1][0] = max(0, -7+1) = 0, dp[1][1] = max(-7, 0-1) = -1
2 5 4 -1 dp[2][0] = max(0, -1+5) = 4, dp[2][1] = max(-1, 0-5) = -1
3 3 4 1 dp[3][0] = max(4, -1+3) = 4, dp[3][1] = max(-1, 4-3) = 1
4 6 7 1 dp[4][0] = max(4, 1+6) = 7, dp[4][1] = max(1, 4-6) = 1
5 4 7 3 dp[5][0] = max(7, 1+4) = 7, dp[5][1] = max(1, 7-4) = 3

最终结果:dp[5][0] = 7

代码实现

C# 实现

public class Solution {
    // 方法一:贪心算法
    public int MaxProfit(int[] prices) {
        int maxProfit = 0;
        
        for (int i = 1; i < prices.Length; i++) {
            int diff = prices[i] - prices[i - 1];
            if (diff > 0) {
                maxProfit += diff;
            }
        }
        
        return maxProfit;
    }
    
    // 方法二:动态规划
    public int MaxProfitDP(int[] prices) {
        int n = prices.Length;
        int[,] dp = new int[n, 2];
        
        // 初始状态
        dp[0, 0] = 0;  // 不持有股票
        dp[0, 1] = -prices[0];  // 持有股票
        
        for (int i = 1; i < n; i++) {
            // 当前不持有股票的最大利润
            dp[i, 0] = Math.Max(dp[i - 1, 0], dp[i - 1, 1] + prices[i]);
            // 当前持有股票的最大利润
            dp[i, 1] = Math.Max(dp[i - 1, 1], dp[i - 1, 0] - prices[i]);
        }
        
        return dp[n - 1, 0];
    }
    
    // 方法三:动态规划(空间优化)
    public int MaxProfitDPOptimized(int[] prices) {
        int cash = 0;  // 不持有股票的最大利润
        int hold = -prices[0];  // 持有股票的最大利润
        
        for (int i = 1; i < prices.Length; i++) {
            int prevCash = cash;
            cash = Math.Max(cash, hold + prices[i]);
            hold = Math.Max(hold, prevCash - prices[i]);
        }
        
        return cash;
    }
}

Python 实现

class Solution:
    # 方法一:贪心算法
    def maxProfit(self, prices: List[int]) -> int:
        max_profit = 0
        
        for i in range(1, len(prices)):
            diff = prices[i] - prices[i - 1]
            if diff > 0:
                max_profit += diff
        
        return max_profit
    
    # 方法二:动态规划
    def maxProfitDP(self, prices: List[int]) -> int:
        n = len(prices)
        dp = [[0, 0] for _ in range(n)]
        
        # 初始状态
        dp[0][0] = 0  # 不持有股票
        dp[0][1] = -prices[0]  # 持有股票
        
        for i in range(1, n):
            # 当前不持有股票的最大利润
            dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] + prices[i])
            # 当前持有股票的最大利润
            dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] - prices[i])
        
        return dp[n - 1][0]
    
    # 方法三:动态规划(空间优化)
    def maxProfitDPOptimized(self, prices: List[int]) -> int:
        cash = 0  # 不持有股票的最大利润
        hold = -prices[0]  # 持有股票的最大利润
        
        for i in range(1, len(prices)):
            prev_cash = cash
            cash = max(cash, hold + prices[i])
            hold = max(hold, prev_cash - prices[i])
        
        return cash

C++ 实现

class Solution {
public:
    // 方法一:贪心算法
    int maxProfit(vector<int>& prices) {
        int maxProfit = 0;
        
        for (int i = 1; i < prices.size(); i++) {
            int diff = prices[i] - prices[i - 1];
            if (diff > 0) {
                maxProfit += diff;
            }
        }
        
        return maxProfit;
    }
    
    // 方法二:动态规划
    int maxProfitDP(vector<int>& prices) {
        int n = prices.size();
        vector<vector<int>> dp(n, vector<int>(2, 0));
        
        // 初始状态
        dp[0][0] = 0;  // 不持有股票
        dp[0][1] = -prices[0];  // 持有股票
        
        for (int i = 1; i < n; i++) {
            // 当前不持有股票的最大利润
            dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] + prices[i]);
            // 当前持有股票的最大利润
            dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] - prices[i]);
        }
        
        return dp[n - 1][0];
    }
    
    // 方法三:动态规划(空间优化)
    int maxProfitDPOptimized(vector<int>& prices) {
        int cash = 0;  // 不持有股票的最大利润
        int hold = -prices[0];  // 持有股票的最大利润
        
        for (int i = 1; i < prices.size(); i++) {
            int prevCash = cash;
            cash = max(cash, hold + prices[i]);
            hold = max(hold, prevCash - prices[i]);
        }
        
        return cash;
    }
};

执行结果

C# 实现

  • 执行用时:88 ms
  • 内存消耗:39.9 MB

Python 实现

  • 执行用时:44 ms
  • 内存消耗:17.8 MB

C++ 实现

  • 执行用时:4 ms
  • 内存消耗:12.9 MB

性能对比

语言 执行用时 内存消耗 特点
C# 88 ms 39.9 MB 代码结构清晰,但性能较慢
Python 44 ms 17.8 MB 代码简洁,性能适中
C++ 4 ms 12.9 MB 执行速度最快,内存占用最少

代码亮点

  1. 🎯 贪心算法简洁高效,直接累加所有正的价格差
  2. 💡 动态规划方法清晰地定义了状态和转移方程,易于理解和扩展
  3. 🔍 空间优化的动态规划方法将空间复杂度从O(n)降低到O(1)
  4. 🎨 三种方法各有优势,可以根据具体需求选择合适的实现

常见错误分析

  1. 🚫 没有理解题目允许多次交易的特性,错误地只进行一次交易
  2. 🚫 在动态规划方法中,没有正确定义状态或状态转移方程
  3. 🚫 在空间优化的动态规划方法中,没有使用临时变量保存上一个状态,导致状态更新错误
  4. 🚫 没有考虑到股票价格可能一直下跌的情况,此时最大利润应为0

解法对比

解法 时间复杂度 空间复杂度 优点 缺点
贪心算法 O(n) O(1) 简单直观,效率高 不易扩展到更复杂的股票问题
动态规划 O(n) O(n) 思路清晰,易于扩展 空间占用较大
动态规划(空间优化) O(n) O(1) 效率高,空间占用小 代码稍复杂

相关题目