LeetCode经典算法面试题 #121:买卖股票的最佳时机(贪心法、动态规划、分治法等多种实现方案详解)
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^50 <= 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 解法一:暴力法
核心思想:
枚举所有可能的买入和卖出日期组合,计算利润,取最大值。
算法思路:
- 初始化
maxProfit = 0。 - 对于每个
i(买入日),遍历所有j > i(卖出日),计算profit = prices[j] - prices[i],更新maxProfit = max(maxProfit, profit)。 - 返回
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,对于每一天,计算当天价格与历史最低价的差值,并更新最大利润。
算法思路:
- 初始化
minPrice = Integer.MAX_VALUE,maxProfit = 0。 - 遍历
prices数组:- 如果当前价格
price小于minPrice,更新minPrice = price。 - 否则,计算
profit = price - minPrice,如果大于maxProfit,则更新maxProfit。
- 如果当前价格
- 返回
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 天结束时,持有股票的最大利润(即已经买入且未卖出)。
算法思路:
- 初始化
dp[0][0] = 0(第一天不持有),dp[0][1] = -prices[0](第一天买入)。 - 对于 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])(今天买入或不操作)
- 最终结果为
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)。
算法思路:
- 构建差分数组
diff,长度为 n-1。 - 使用 Kadane 算法求最大子数组和。
- 如果结果小于 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 算法用于求解最大子数组和。差分数组的连续子数组和对应一段交易的总利润,因此最大子数组和就是最大利润。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)