Article / 文章

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

给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。 你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。 返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0 。

题目描述

给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。

你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。

返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0

难度

简单

题目链接

点击在LeetCode中查看题目

示例

示例 1:

输入:[7,1,5,3,6,4]
输出:5
解释:在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。
     注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格;同时,你不能在买入前卖出股票。

示例 2:

输入:prices = [7,6,4,3,1]
输出:0
解释:在这种情况下, 没有交易完成, 所以最大利润为 0。

提示

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

解题思路

方法一:暴力法

最直观的方法是使用两层循环,遍历所有可能的买入卖出组合,找出最大利润。

关键点:

  • 外层循环遍历所有可能的买入日期
  • 内层循环遍历所有可能的卖出日期(必须在买入日期之后)
  • 计算每种组合的利润,并更新最大利润

具体步骤:

  1. 初始化最大利润为0
  2. 使用两层循环,外层循环i从0到n-2,内层循环j从i+1到n-1
  3. 对于每一对(i,j),计算利润profit = prices[j] - prices[i]
  4. 如果profit大于当前最大利润,则更新最大利润
  5. 返回最大利润

时间复杂度:O(n²),其中n是数组的长度,需要两层循环遍历所有可能的买入卖出组合 空间复杂度:O(1),只需要常数级别的额外空间

方法二:一次遍历(贪心算法)

我们可以在遍历数组的同时,记录当前遇到的最低价格,并计算以当前价格卖出时的最大利润。

关键点:

  • 记录遍历过程中遇到的最低价格
  • 对于每个价格,计算如果在当前价格卖出的利润
  • 更新最大利润

具体步骤:

  1. 初始化最大利润maxProfit为0,最低价格minPrice为一个很大的值(如int.MaxValue)
  2. 遍历数组prices,对于每个价格prices[i]: a. 如果prices[i]小于minPrice,则更新minPrice = prices[i] b. 计算当前利润profit = prices[i] - minPrice c. 如果profit大于maxProfit,则更新maxProfit = profit
  3. 返回maxProfit

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

方法三:动态规划

这个问题也可以用动态规划来解决。我们可以定义状态dp[i]表示前i天的最大利润。

关键点:

  • 状态定义:dp[i]表示前i天的最大利润
  • 状态转移方程:dp[i] = max(dp[i-1], prices[i] - minPrice)
  • 其中minPrice是前i-1天的最低价格

具体步骤:

  1. 初始化dp数组,dp[0] = 0
  2. 初始化最低价格minPrice = prices[0]
  3. 遍历数组prices,从索引1开始: a. 更新dp[i] = max(dp[i-1], prices[i] - minPrice) b. 更新minPrice = min(minPrice, prices[i])
  4. 返回dp[n-1]

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

图解思路

方法二:一次遍历过程

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

索引 价格 最低价格 当前利润 最大利润 说明
0 7 7 0 0 初始状态,最低价格为7,最大利润为0
1 1 1 0 0 更新最低价格为1,当前利润为0
2 5 1 4 4 最低价格仍为1,当前利润为5-1=4,更新最大利润为4
3 3 1 2 4 最低价格仍为1,当前利润为3-1=2,最大利润仍为4
4 6 1 5 5 最低价格仍为1,当前利润为6-1=5,更新最大利润为5
5 4 1 3 5 最低价格仍为1,当前利润为4-1=3,最大利润仍为5

最终结果:最大利润为5

方法三:动态规划过程

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

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

最终结果:dp[5] = 5

代码实现

C# 实现

public class Solution {
    // 方法一:暴力法(超时)
    public int MaxProfitBruteForce(int[] prices) {
        int maxProfit = 0;
        int n = prices.Length;
        
        for (int i = 0; i < n - 1; i++) {
            for (int j = i + 1; j < n; j++) {
                int profit = prices[j] - prices[i];
                maxProfit = Math.Max(maxProfit, profit);
            }
        }
        
        return maxProfit;
    }
    
    // 方法二:一次遍历(贪心算法)
    public int MaxProfit(int[] prices) {
        int maxProfit = 0;
        int minPrice = int.MaxValue;
        
        for (int i = 0; i < prices.Length; i++) {
            if (prices[i] < minPrice) {
                minPrice = prices[i];
            } else {
                int profit = prices[i] - minPrice;
                maxProfit = Math.Max(maxProfit, profit);
            }
        }
        
        return maxProfit;
    }
    
    // 方法三:动态规划
    public int MaxProfitDP(int[] prices) {
        if (prices.Length <= 1) {
            return 0;
        }
        
        int n = prices.Length;
        int[] dp = new int[n];
        int minPrice = prices[0];
        
        for (int i = 1; i < n; i++) {
            dp[i] = Math.Max(dp[i - 1], prices[i] - minPrice);
            minPrice = Math.Min(minPrice, prices[i]);
        }
        
        return dp[n - 1];
    }
}

Python 实现

class Solution:
    # 方法一:暴力法(超时)
    def maxProfitBruteForce(self, prices: List[int]) -> int:
        max_profit = 0
        n = len(prices)
        
        for i in range(n - 1):
            for j in range(i + 1, n):
                profit = prices[j] - prices[i]
                max_profit = max(max_profit, profit)
        
        return max_profit
    
    # 方法二:一次遍历(贪心算法)
    def maxProfit(self, prices: List[int]) -> int:
        max_profit = 0
        min_price = float('inf')
        
        for price in prices:
            if price < min_price:
                min_price = price
            else:
                profit = price - min_price
                max_profit = max(max_profit, profit)
        
        return max_profit
    
    # 方法三:动态规划
    def maxProfitDP(self, prices: List[int]) -> int:
        if len(prices) <= 1:
            return 0
        
        n = len(prices)
        dp = [0] * n
        min_price = prices[0]
        
        for i in range(1, n):
            dp[i] = max(dp[i - 1], prices[i] - min_price)
            min_price = min(min_price, prices[i])
        
        return dp[n - 1]

C++ 实现

class Solution {
public:
    // 方法一:暴力法(超时)
    int maxProfitBruteForce(vector<int>& prices) {
        int maxProfit = 0;
        int n = prices.size();
        
        for (int i = 0; i < n - 1; i++) {
            for (int j = i + 1; j < n; j++) {
                int profit = prices[j] - prices[i];
                maxProfit = max(maxProfit, profit);
            }
        }
        
        return maxProfit;
    }
    
    // 方法二:一次遍历(贪心算法)
    int maxProfit(vector<int>& prices) {
        int maxProfit = 0;
        int minPrice = INT_MAX;
        
        for (int i = 0; i < prices.size(); i++) {
            if (prices[i] < minPrice) {
                minPrice = prices[i];
            } else {
                int profit = prices[i] - minPrice;
                maxProfit = max(maxProfit, profit);
            }
        }
        
        return maxProfit;
    }
    
    // 方法三:动态规划
    int maxProfitDP(vector<int>& prices) {
        if (prices.size() <= 1) {
            return 0;
        }
        
        int n = prices.size();
        vector<int> dp(n, 0);
        int minPrice = prices[0];
        
        for (int i = 1; i < n; i++) {
            dp[i] = max(dp[i - 1], prices[i] - minPrice);
            minPrice = min(minPrice, prices[i]);
        }
        
        return dp[n - 1];
    }
};

执行结果

C# 实现

  • 执行用时:320 ms
  • 内存消耗:45.8 MB

Python 实现

  • 执行用时:240 ms
  • 内存消耗:25.1 MB

C++ 实现

  • 执行用时:96 ms
  • 内存消耗:93.3 MB

性能对比

语言 执行用时 内存消耗 特点
C# 320 ms 45.8 MB 代码结构清晰,但性能较慢
Python 240 ms 25.1 MB 代码简洁,性能适中
C++ 96 ms 93.3 MB 执行速度最快,但内存占用较大

代码亮点

  1. 🎯 方法二通过一次遍历实现了O(n)的时间复杂度,大幅提升了效率
  2. 💡 贪心算法的思想:记录历史最低价格,并尝试在每个价格点卖出
  3. 🔍 动态规划方法清晰地定义了状态和转移方程,易于理解和扩展
  4. 🎨 代码结构清晰,变量命名规范,易于理解和维护

常见错误分析

  1. 🚫 没有考虑到股票价格可能一直下跌的情况,此时最大利润应为0
  2. 🚫 错误地认为可以在同一天买入和卖出股票
  3. 🚫 在动态规划方法中,没有正确初始化最低价格和dp数组
  4. 🚫 在暴力法中,没有正确处理边界情况,如数组长度小于2的情况

解法对比

解法 时间复杂度 空间复杂度 优点 缺点
暴力法 O(n²) O(1) 思路简单直观 效率低下,大规模数据会超时
一次遍历(贪心) O(n) O(1) 效率高,空间占用小 不易扩展到更复杂的股票问题
动态规划 O(n) O(n) 思路清晰,易于扩展 空间占用较大

相关题目