题目描述
给你一个整数数组 prices
,其中 prices[i]
表示某支股票第 i
天的价格。
在每一天,你可以决定是否购买和/或出售股票。你在任何时候 最多 只能持有 一股 股票。你也可以先购买,然后在 同一天 出售。
返回 你能获得的 最大 利润 。
示例 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
问题分析
与"买卖股票的最佳时机 I"不同,这道题允许我们进行多次交易,但有以下限制:
- 在任何时候最多只能持有一股股票
- 必须先买入才能卖出(不能卖空)
- 可以在同一天买入并卖出
关键问题是:如何安排买入和卖出的时机,使得总利润最大?
解题思路
贪心算法
这道题有一个非常直观的贪心解法。由于可以进行无限次交易,我们可以采取"只要有利可图就交易"的策略。具体来说:
- 当股票价格上涨时(明天价格 > 今天价格),我们应该在今天买入,明天卖出,赚取差价
- 当股票价格下跌时,我们不进行任何操作
这个策略可以进一步简化:只需要将所有相邻两天的价格上涨都累加起来,就得到了最大利润。
例如,对于数组 [1,2,3,4,5]:
- 第1天买入,第5天卖出,利润是 5-1=4
- 等价于:(2-1)+(3-2)+(4-3)+(5-4)=4
即将所有上涨的部分都累加起来,就是最大利润。
为什么贪心算法有效?
假设我们有三天的价格:a, b, c,其中 a < b > c(即价格先上涨后下跌)。
如果我们在价格为a时买入,在价格为b时卖出,然后在价格为c时再买入,总利润为(b-a)。
如果我们选择持有(即在价格为a时买入,一直持有到价格为c),利润为(c-a),由于c<b,所以(c-a)<(b-a),不如前一种策略获利多。
代码实现
Java 实现
class Solution {
public int maxProfit(int[] prices) {
// 边界条件检查
if (prices == null || prices.length <= 1) {
return 0;
}
int totalProfit = 0;
// 遍历价格数组
for (int i = 1; i < prices.length; i++) {
// 如果当前价格高于前一天的价格,就进行交易
if (prices[i] > prices[i - 1]) {
// 累加这一天的利润
totalProfit += prices[i] - prices[i - 1];
}
}
return totalProfit;
}
}
我们还可以添加一些调试信息,以便更清楚地理解算法的执行过程:
class Solution {
public int maxProfit(int[] prices) {
if (prices == null || prices.length <= 1) {
return 0;
}
int totalProfit = 0;
System.out.println("初始总利润: " + totalProfit);
for (int i = 1; i < prices.length; i++) {
int todayPrice = prices[i];
int yesterdayPrice = prices[i - 1];
if (todayPrice > yesterdayPrice) {
int profit = todayPrice - yesterdayPrice;
totalProfit += profit;
System.out.println("第" + i + "天: 价格从 " + yesterdayPrice +
" 上涨到 " + todayPrice + ", 获利 " + profit +
", 总利润增加到 " + totalProfit);
} else {
System.out.println("第" + i + "天: 价格从 " + yesterdayPrice +
" 下跌到 " + todayPrice + ", 不操作, 总利润仍为 " +
totalProfit);
}
}
return totalProfit;
}
}
输入:prices = [7,1,5,3,6,4]
初始总利润: 0
第1天: 价格从 7 下跌到 1, 不操作, 总利润仍为 0
第2天: 价格从 1 上涨到 5, 获利 4, 总利润增加到 4
第3天: 价格从 5 下跌到 3, 不操作, 总利润仍为 4
第4天: 价格从 3 上涨到 6, 获利 3, 总利润增加到 7
第5天: 价格从 6 下跌到 4, 不操作, 总利润仍为 7
C# 实现
public class Solution {
public int MaxProfit(int[] prices) {
// 边界条件检查
if (prices == null || prices.Length <= 1) {
return 0;
}
int totalProfit = 0;
// 遍历价格数组
for (int i = 1; i < prices.Length; i++) {
// 如果当前价格高于前一天的价格,就进行交易
if (prices[i] > prices[i - 1]) {
// 累加这一天的利润
totalProfit += prices[i] - prices[i - 1];
}
}
return totalProfit;
}
}
详细执行过程图解
以示例 [7,1,5,3,6,4] 为例,让我们详细跟踪算法的执行过程:
初始总利润:0
第1天 → 第2天:价格从 7 下跌到 1,不操作,总利润仍为 0
第2天 → 第3天:价格从 1 上涨到 5,进行交易:
- 第2天买入(价格=1)
- 第3天卖出(价格=5)
- 获利:5-1=4
- 总利润:0+4=4
第3天 → 第4天:价格从 5 下跌到 3,不操作,总利润仍为 4
第4天 → 第5天:价格从 3 上涨到 6,进行交易:
- 第4天买入(价格=3)
- 第5天卖出(价格=6)
- 获利:6-3=3
- 总利润:4+3=7
第5天 → 第6天:价格从 6 下跌到 4,不操作,总利润仍为 7
最终总利润:7
价格-时间图解
股票价格变化图:
价格
^
7 | *
6 | *
5 | *
4 | *
3 | *
2 |
1 | *
0 +-------------------> 时间
1 2 3 4 5 6
交易策略图解:
价格
^
7 |*
6 | *卖出
5 | *卖出
4 | *
3 | *买入
2 |
1 | *买入
0 +-----------------> 时间
1 2 3 4 5 6
算法分析
贪心算法的正确性
为什么这个贪心策略是正确的?我们可以证明,对于任何一段连续上涨的区间[i, j],在i买入,在j卖出的利润等于区间内所有相邻两天价格差的总和:
prices[j] - prices[i] = (prices[i+1] - prices[i]) + (prices[i+2] - prices[i+1]) + ... + (prices[j] - prices[j-1])
因此,无论是选择在整个上涨区间买卖一次,还是每天都进行买卖,最终的利润是相同的。而对于下跌区间,不进行操作是最优的。
复杂度分析
- 时间复杂度: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])(今天不持有 = max(昨天不持有,昨天持有今天卖出))
- dp[i][1] = max(dp[i-1][1], dp[i-1][0] - prices[i])(今天持有 = max(昨天持有,昨天不持有今天买入))
初始状态:
- dp[0][0] = 0(第0天不持有股票,利润为0)
- dp[0][1] = -prices[0](第0天持有股票,利润为-prices[0])
最终答案是 dp[n-1][0],即最后一天不持有股票的最大利润。
Java实现:
class Solution {
public int maxProfit(int[] prices) {
int n = prices.length;
if (n <= 1) return 0;
int[][] dp = new int[n][2];
// 初始状态
dp[0][0] = 0; // 第0天不持有股票
dp[0][1] = -prices[0]; // 第0天持有股票
// 状态转移
for (int i = 1; i < n; i++) {
// 今天不持有 = max(昨天不持有,昨天持有今天卖出)
dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1] + prices[i]);
// 今天持有 = max(昨天持有,昨天不持有今天买入)
dp[i][1] = Math.max(dp[i-1][1], dp[i-1][0] - prices[i]);
}
return dp[n-1][0]; // 最后一天不持有股票的最大利润
}
}
空间优化的动态规划(因为只依赖前一天的状态):
class Solution {
public int maxProfit(int[] prices) {
int n = prices.length;
if (n <= 1) return 0;
int noStock = 0; // 不持有股票的最大利润
int hasStock = -prices[0]; // 持有股票的最大利润
for (int i = 1; i < n; i++) {
int prevNoStock = noStock;
// 今天不持有 = max(昨天不持有,昨天持有今天卖出)
noStock = Math.max(noStock, hasStock + prices[i]);
// 今天持有 = max(昨天持有,昨天不持有今天买入)
hasStock = Math.max(hasStock, prevNoStock - prices[i]);
}
return noStock;
}
}
总结
买卖股票的最佳时机 II 是一个经典的贪心算法问题。通过简单的策略——只要价格上涨就进行交易,我们就能获得最大的利润。
这个问题的关键洞察是:
- 由于可以进行无限次交易,我们可以将任何一段上涨区间的总利润分解为每日差价之和
- 对于下跌区间,不进行操作是最优的
与第一题(只能交易一次)相比,这个问题实际上更简单,因为我们不再需要寻找全局最优的买入卖出点,而是可以采取局部贪心的策略。
理解这个问题对于解决更复杂的股票交易问题(如限制交易次数、有冷却期、有交易费用等)也很有帮助。