每日LeetCode : 买股票的最佳时机

100 阅读4分钟

题目描述

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

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

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

示例:

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

解法1:暴力破解法(双重循环)

核心思想:通过遍历所有可能的买入和卖出组合,找出最大利润差,这也是最简单常用的方法。

function maxProfit(prices) {
    let max = 0;
    for (let i = 0; i < prices.length; i++) {
        for (let j = i + 1; j < prices.length; j++) {
            const profit = prices[j] - prices[i];
            if (profit > max) {
                max = profit;
            }
        }
    }
    return max;
}

解题思路

  1. 首先初始化最大利润 max为0,通过外层循环遍历所有可能的买入日( i 从0 到 n-1)
  2. 通过内层循环遍历该买入日之后的所有卖出日( j 从 i+1到 n-1 )
  3. 计算每个买卖组合的利润 = prices[j] - prices[i],如果利润大于当前最大利润,则更新最大利润
  4. 返回最终的最大利润值

时间复杂度:O(n²)
空间复杂度:O(1)

注意点

1. 你必须要确保 j > i,即卖出日永远在买入日之后。

2. 题目要求不能获取利润时返回0,无需额外处理。

3.prices 为空时返回0。

4. 数据量大时效率极低(1000个元素需500万次计算)。

解法2:动态规划法(单次遍历)

核心思想:在遍历过程中记录历史最低价,并计算当前卖出可能的最大利润。

function maxProfit(prices) {
    if (prices.length === 0) return 0;
    
    let minPrice = prices[0];
    let maxProfit = 0;
    
    for (let i = 1; i < prices.length; i++) {
        // 更新历史最低价
        if (prices[i] < minPrice) {
            minPrice = prices[i];
        } 
        // 计算当前卖出利润并更新最大利润
        else if (prices[i] - minPrice > maxProfit) {
            maxProfit = prices[i] - minPrice;
        }
    }
    
    return maxProfit;
}

解题思路

  1. 初始化:设置最低价 minPrice 为第一天的价格,最大利润maxProfit 为0
  2. 遍历更新
  • 如果当前价格低于历史最低价: 更新历史最低价 minPrice 为当前价格。
  • 否则则计算 当前利润 = 当前价格 - 历史最低价,如果当前利润大于最大利润,更新最大利润maxProfit
  1. 返回结果:遍历结束后的 maxProfit 即为答案

时间复杂度:O(n)
空间复杂度:O(1)

注意点

1. minPrice 应初始化为 prices[0] 而非 Infinity

2. 应从第2天(也就是索引为1)的时候开始遍历

3. 不能在同一天同时更新 minPricemaxProfit

4. 遗漏 prices.length === 0 检查

解法3:动态规划法(状态机)

核心思想:明确定义持有未持有股票两种状态,使用状态转移方程计算最大利润。

function maxProfit(prices) {
  
 
    let dp0 = 0;  // dp0: 未持有股票时的最大利润
    let dp1 = -prices[0];   // dp1: 持有股票时的最大利润
    
    for (let i = 1; i < prices.length; i++) {
        // 保持未持有 或 卖出股票
        dp0 = Math.max(dp0, dp1 + prices[i]);
        // 保持持有 或 买入股票(只能买一次所以是 -prices[i])
        dp1 = Math.max(dp1, -prices[i]);
    }
    
    return dp0;
}

状态定义

  • dp0:第 i 天结束时未持有股票的最大利润
  • dp1:第 i 天结束时持有股票的最大利润

状态转移

  1. dp0 = max(保持未持有, 卖出股票) = max(dp0, dp1 + prices[i])
  2. dp1 = max(保持持有, 买入股票) = max(dp1, -prices[i])(因为只能交易一次)

时间复杂度:O(n)
空间复杂度:O(1)

优点:框架化思维,容易扩展到其他更复杂的股票问题

解法4:差值数组(最大子序和)

核心思想:将价格数组转换为每日价格变化,问题转化为求最大子序和。

function maxProfit(prices) {
    if (prices.length < 2) return 0;
    
    // 计算每日价格变化
    const diff = [];
    for (let i = 1; i < prices.length; i++) {
        diff.push(prices[i] - prices[i - 1]);
    }
    
    // 求最大子序和(Kadane算法)
    let maxCur = 0;
    let maxSoFar = 0;
    
    for (let i = 0; i < diff.length; i++) {
        maxCur = Math.max(0, maxCur + diff[i]);
        maxSoFar = Math.max(maxSoFar, maxCur);
    }
    
    return maxSoFar;
}

数学原理

i天买入,第j天卖出的利润:
prices[j] - prices[i] 
= (prices[i+1]-prices[i]) + ... + (prices[j]-prices[j-1])
= ∑每日差值

时间复杂度:O(n)
空间复杂度:O(n)(可优化为O(1))

上面的代码还能进一步优化(空间O(1)):

function maxProfit(prices) {
    let maxCur = 0;
    let maxSoFar = 0;
    
    for (let i = 1; i < prices.length; i++) {
        maxCur = Math.max(0, maxCur + prices[i] - prices[i-1]);
        maxSoFar = Math.max(maxSoFar, maxCur);
    }
    
    return maxSoFar;
}

总结

算法选择

  • 最优解法:单次遍历法(解法2),O(n)时间,O(1)空间
  • 扩展性:状态机法(解法3),适用于更复杂的股票问题
  • 学术价值:差值法(解法4),展示问题转化的技巧