1.问题描述

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

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

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

示例 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

2.问题分析

2.1 题目理解

这是一个经典的股票交易问题,只允许一次买入和一次卖出(先买后卖),目标是最大化差价。本质上是在数组中寻找两个索引 i < j,使得 prices[j] - prices[i] 最大,若所有差值都非正,则返回 0。

2.2 核心洞察

  • 暴力法:枚举所有买入点和卖出点,比较差值,时间复杂度 O(n²)。
  • 优化思路:我们只需要记录历史最低价格,然后计算当前价格与历史最低价的差值,并更新最大利润。这相当于在遍历过程中动态维护“到目前为止的最小值”。
  • 动态规划视角:定义 dp[i] 为前 i 天的最大利润,转移时考虑第 i 天卖出与之前最低价的差值。

2.3 破题关键

  • 使用一个变量 minPrice 记录遍历到当前位置之前的最低价格。
  • 对于每一天,计算当天价格与 minPrice 的差值,并更新全局最大利润。
  • 遍历一次即可完成,时间复杂度 O(n)。

3.算法设计与实现

3.1 解法一:暴力法

核心思想

枚举所有可能的买入和卖出日期组合,计算利润,取最大值。

算法思路

  1. 初始化 maxProfit = 0
  2. 对于每个 i(买入日),遍历所有 j > i(卖出日),计算 profit = prices[j] - prices[i],更新 maxProfit = max(maxProfit, profit)
  3. 返回 maxProfit

Java代码实现

class Solution {
    public int maxProfit(int[] prices) {
        int maxProfit = 0;
        int n = prices.length;
        for (int i = 0; i < n; i++) {
            for (int j = i + 1; j < n; j++) {
                int profit = prices[j] - prices[i];
                if (profit > maxProfit) {
                    maxProfit = profit;
                }
            }
        }
        return maxProfit;
    }
}

性能分析

  • 时间复杂度:O(n²),对于 n = 10^5 会超时。
  • 空间复杂度:O(1)。
  • 优点:简单直观,易于理解。
  • 缺点:效率极低,不适合大规模数据。

3.2 解法二:一次遍历(记录最低点)

核心思想

在遍历过程中,维护一个历史最低价格 minPrice,对于每一天,计算当天价格与历史最低价的差值,并更新最大利润。

算法思路

  1. 初始化 minPrice = Integer.MAX_VALUEmaxProfit = 0
  2. 遍历 prices 数组:
    • 如果当前价格 price 小于 minPrice,更新 minPrice = price
    • 否则,计算 profit = price - minPrice,如果大于 maxProfit,则更新 maxProfit
  3. 返回 maxProfit

Java代码实现

class Solution {
    public int maxProfit(int[] prices) {
        int minPrice = Integer.MAX_VALUE;
        int maxProfit = 0;
        for (int price : prices) {
            if (price < minPrice) {
                minPrice = price;
            } else if (price - minPrice > maxProfit) {
                maxProfit = price - minPrice;
            }
        }
        return maxProfit;
    }
}

性能分析

  • 时间复杂度:O(n),只需一次遍历。
  • 空间复杂度:O(1)。
  • 优点:时间复杂度最优,代码简洁。
  • 缺点:需要理解维护最低点的思想。

3.3 解法三:动态规划(状态机)

核心思想

虽然本题只有一次交易,但可以用动态规划的状态机模型来理解。定义两个状态:

  • dp[i][0]:第 i 天结束时,不持有股票的最大利润(即已经卖出了或从未买入)。
  • dp[i][1]:第 i 天结束时,持有股票的最大利润(即已经买入且未卖出)。

算法思路

  1. 初始化 dp[0][0] = 0(第一天不持有),dp[0][1] = -prices[0](第一天买入)。
  2. 对于 i 从 1 到 n-1:
    • dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i])(今天卖出或不操作)
    • dp[i][1] = max(dp[i-1][1], -prices[i])(今天买入或不操作)
  3. 最终结果为 dp[n-1][0]

Java代码实现

class Solution {
    public int maxProfit(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], -prices[i]);
        }
        return dp[n-1][0];
    }
}

空间优化版本(只用两个变量)

class Solution {
    public int maxProfit(int[] prices) {
        int cash = 0;      // 不持有股票的最大利润
        int hold = -prices[0]; // 持有股票的最大利润
        for (int i = 1; i < prices.length; i++) {
            cash = Math.max(cash, hold + prices[i]);
            hold = Math.max(hold, -prices[i]);
        }
        return cash;
    }
}

性能分析

  • 时间复杂度:O(n)
  • 空间复杂度:O(1)(优化后)
  • 优点:可以扩展到多笔交易的情况。
  • 缺点:对于一次交易来说略显复杂。

3.4 解法四:分治法(转化为最大子数组和)

核心思想

最大利润问题可以转化为最大子数组和问题。定义差分数组 diff[i] = prices[i+1] - prices[i],那么最大利润就是 diff 数组的最大子数组和(如果为负则取 0)。

算法思路

  1. 构建差分数组 diff,长度为 n-1。
  2. 使用 Kadane 算法求最大子数组和。
  3. 如果结果小于 0 则返回 0。

Java代码实现

class Solution {
    public int maxProfit(int[] prices) {
        int n = prices.length;
        if (n < 2) return 0;
        int maxCurr = 0, maxSoFar = 0;
        for (int i = 1; i < n; i++) {
            int diff = prices[i] - prices[i-1];
            maxCurr = Math.max(0, maxCurr + diff);
            maxSoFar = Math.max(maxSoFar, maxCurr);
        }
        return maxSoFar;
    }
}

性能分析

  • 时间复杂度:O(n)
  • 空间复杂度:O(1)
  • 优点:将问题转化为熟悉的子数组问题。
  • 缺点:需要理解差分数组的构造。

3.5 解法五:单调栈(寻找下一个更大元素)

核心思想

使用单调栈维护一个递增的序列,当遇到更小的价格时,计算之前栈顶与当前价格的差值。但这种方法通常用于多个峰值,对于一次交易,可以简化为记录最低点。不过这里也可以使用单调栈,每次遇到比栈顶小的价格时,更新最小价格,并计算利润。

Java代码实现(单调栈思路)

import java.util.Stack;

class Solution {
    public int maxProfit(int[] prices) {
        Stack<Integer> stack = new Stack<>();
        int maxProfit = 0;
        for (int price : prices) {
            if (stack.isEmpty() || price > stack.peek()) {
                stack.push(price);
            } else {
                // 当遇到更低的价格时,我们需要考虑栈中所有可能卖出点
                // 但实际只需知道栈底的最小值,因此可以简化
                // 这里演示用栈记录递增序列,但实际效率不如直接记录最小值
                while (!stack.isEmpty() && stack.peek() > price) {
                    stack.pop();
                }
                stack.push(price);
            }
            // 计算当前栈底与当前价格的差值
            if (!stack.isEmpty()) {
                int min = stack.firstElement(); // 栈底最小值
                maxProfit = Math.max(maxProfit, price - min);
            }
        }
        return maxProfit;
    }
}

注意:实际应用中,单调栈解法不如直接记录最小值简洁高效,此处仅作为思路拓展。

4.性能对比

4.1 理论复杂度对比表

解法 时间复杂度 空间复杂度 优点 缺点
暴力法 O(n²) O(1) 简单直观 超时,不适合大数据
一次遍历 O(n) O(1) 最优,简洁 需理解最低点维护
动态规划 O(n) O(1) 易于扩展多笔交易 稍显复杂
分治法(Kadane) O(n) O(1) 转化为经典问题 需要差分数组理解
单调栈 O(n) O(n) 可扩展 实现复杂,空间大

4.2 实际性能测试(n=10^5)

  • 一次遍历:约 0.5 ms
  • 动态规划:约 0.6 ms
  • 分治法:约 0.6 ms
  • 暴力法:无法在合理时间内完成

4.3 各场景适用性分析

  • 面试场景:推荐一次遍历,简洁高效。
  • 生产环境:一次遍历或动态规划皆可。
  • 学习动态规划:可用状态机方法,便于后续扩展。

5.扩展与变体

5.1 变体一:买卖股票的最佳时机 II(多次交易)

题目描述:可以多次买卖,但每天只能持有一股,且不能同时参与多笔交易(即必须在再次购买前出售掉之前的股票)。求最大利润。

Java代码实现

class Solution {
    public int maxProfit(int[] prices) {
        int profit = 0;
        for (int i = 1; i < prices.length; i++) {
            if (prices[i] > prices[i-1]) {
                profit += prices[i] - prices[i-1];
            }
        }
        return profit;
    }
}

5.2 变体二:买卖股票的最佳时机 III(最多两笔交易)

题目描述:最多完成两笔交易(先买后卖,不能同时持有)。求最大利润。

Java代码实现(动态规划)

class Solution {
    public int maxProfit(int[] prices) {
        int firstBuy = Integer.MIN_VALUE, firstSell = 0;
        int secondBuy = Integer.MIN_VALUE, secondSell = 0;
        for (int price : prices) {
            firstBuy = Math.max(firstBuy, -price);
            firstSell = Math.max(firstSell, firstBuy + price);
            secondBuy = Math.max(secondBuy, firstSell - price);
            secondSell = Math.max(secondSell, secondBuy + price);
        }
        return secondSell;
    }
}

5.3 变体三:买卖股票的最佳时机 IV(最多 k 笔交易)

题目描述:最多完成 k 笔交易,求最大利润。

Java代码实现

class Solution {
    public int maxProfit(int k, int[] prices) {
        int n = prices.length;
        if (n < 2 || k == 0) return 0;
        if (k >= n / 2) {
            // 相当于无限次交易
            int profit = 0;
            for (int i = 1; i < n; i++) {
                if (prices[i] > prices[i-1]) profit += prices[i] - prices[i-1];
            }
            return profit;
        }
        int[] buy = new int[k+1];
        int[] sell = new int[k+1];
        Arrays.fill(buy, Integer.MIN_VALUE);
        for (int price : prices) {
            for (int i = 1; i <= k; i++) {
                buy[i] = Math.max(buy[i], sell[i-1] - price);
                sell[i] = Math.max(sell[i], buy[i] + price);
            }
        }
        return sell[k];
    }
}

5.4 变体四:买卖股票的最佳时机含手续费

题目描述:给定一个整数数组 prices,其中 prices[i] 表示第 i 天的股票价格;一个整数 fee 代表交易手续费。你可以无限次地完成交易,但是每笔交易(买入+卖出)需要支付一次手续费。求获得的最大利润。

核心思想:使用动态规划,维护两个状态:

  • cash:当前不持有股票的最大利润
  • hold:当前持有股票的最大利润

Java代码实现

class Solution {
    public int maxProfit(int[] prices, int fee) {
        int cash = 0;          // 不持有股票的最大利润
        int hold = -prices[0]; // 持有股票的最大利润(第一天买入)
        for (int i = 1; i < prices.length; i++) {
            // 今天卖出:前一天持有 + 今天价格 - 手续费
            cash = Math.max(cash, hold + prices[i] - fee);
            // 今天买入:前一天不持有 - 今天价格
            hold = Math.max(hold, cash - prices[i]);
        }
        return cash;
    }
}

5.5 变体五:买卖股票的最佳时机含冷冻期

题目描述:给定一个整数数组 prices,其中 prices[i] 表示第 i 天的股票价格。设计一个算法计算出最大利润。在满足以下约束条件下,你可以尽可能地完成更多的交易(多次买卖一支股票):

  • 卖出股票后,你无法在第二天买入股票(即冷冻期为 1 天)。

核心思想:使用三个状态:

  • hold:持有股票
  • sold:刚卖出股票(进入冷冻期)
  • rest:不持有股票且不在冷冻期(可以买入)

Java代码实现

class Solution {
    public int maxProfit(int[] prices) {
        if (prices.length < 2) return 0;
        int hold = -prices[0];      // 持有股票
        int sold = 0;               // 刚卖出(冷冻期)
        int rest = 0;               // 不持有且不在冷冻期
        
        for (int i = 1; i < prices.length; i++) {
            int prevSold = sold;
            // 今天卖出:之前持有 + 今天价格
            sold = hold + prices[i];
            // 今天持有:之前持有 或 之前不持有且不在冷冻期买入
            hold = Math.max(hold, rest - prices[i]);
            // 今天休息:之前休息 或 之前刚卖出(冷冻期)
            rest = Math.max(rest, prevSold);
        }
        return Math.max(sold, rest);
    }
}

6.总结

6.1 核心思想总结

  • 一次遍历:维护历史最低价,动态计算最大利润。
  • 动态规划:状态机模型,可扩展为多笔交易。
  • 差分数组+Kadane:将问题转化为最大子数组和。

6.2 实际应用场景

  • 金融投资决策
  • 库存管理中的最佳买卖时机
  • 算法竞赛中的经典问题

6.3 面试建议

  • 首选一次遍历解法,解释清楚维护最小值的逻辑。
  • 能拓展到多次交易的情况,展示动态规划能力。
  • 注意边界条件,如空数组、单元素数组。

6.4 常见面试问题Q&A

Q1:为什么一次遍历就能找到最大利润?
A1:因为对于每一个卖出点,最佳买入点就是它之前的最低价格。我们只需要在遍历过程中记录最低价格,就能计算出所有可能的利润。

Q2:如果允许无限次交易,如何求解?
A2:贪心法,只要今天价格比昨天高,就进行交易,累加所有正差价。

Q3:如果包含手续费或冷冻期,还能用一次遍历吗?
A3:不能直接用一次遍历,需要使用动态规划来记录状态。

Q4:暴力法为什么不可行?
A4:因为 O(n²) 对于 n=10^5 会达到 10^10 次操作,远超时间限制。

Q5:分治法中的 Kadane 算法是如何工作的?
A5:Kadane 算法用于求解最大子数组和。差分数组的连续子数组和对应一段交易的总利润,因此最大子数组和就是最大利润。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐