1. 问题分析与理解
这道题目是经典的股票买卖问题变种,属于动态规划类问题。题目要求我们在给定的股票价格序列中,通过多次买卖获取最大利润。与只能买卖一次的基础版本不同,本题允许无限次交易,但必须遵守"先买后卖"的基本规则。
理解题意有几个关键点:
- 每天可以选择买入、卖出或持有
- 同一时间最多只能持有一股
- 可以完成任意数量的交易
- 不能参与多笔交易(必须在再次购买前出售掉之前的股票)
举个例子,给定价格序列[7,1,5,3,6,4],最佳策略是在第2天买入(1),第3天卖出(5),利润4;然后在第4天买入(3),第5天卖出(6),利润3。总利润为7。
2. 解题思路解析
2.1 贪心算法解法
最直观的解法是采用贪心算法。核心思想是:只要今天的价格比昨天高,就进行交易(昨天买入今天卖出)。
java复制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;
}
这个解法的时间复杂度是O(n),空间复杂度是O(1)。为什么这样能获得最大利润?因为题目允许无限次交易,我们实际上是在收集所有上升区间的利润。
注意:贪心解法只适用于本题的特殊条件(无限次交易)。对于交易次数有限制的情况,这种方法就不适用了。
2.2 动态规划解法
更通用的解法是采用动态规划。我们需要定义状态转移方程:
定义两个状态:
- dp[i][0]:第i天结束时未持有股票的最大利润
- dp[i][1]:第i天结束时持有股票的最大利润
状态转移方程:
code复制dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i])
dp[i][1] = max(dp[i-1][1], dp[i-1][0] - prices[i])
初始条件:
code复制dp[0][0] = 0
dp[0][1] = -prices[0]
Java实现:
java复制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], dp[i-1][0] - prices[i]);
}
return dp[n-1][0];
}
这个解法的时间复杂度是O(n),空间复杂度是O(n)。可以通过滚动数组优化空间复杂度到O(1)。
3. 代码优化与实现细节
3.1 空间优化版本
观察到每天的状态只依赖于前一天的状态,我们可以只用两个变量来存储状态:
java复制public int maxProfit(int[] prices) {
int n = prices.length;
int dp0 = 0, dp1 = -prices[0];
for (int i = 1; i < n; i++) {
int newDp0 = Math.max(dp0, dp1 + prices[i]);
int newDp1 = Math.max(dp1, dp0 - prices[i]);
dp0 = newDp0;
dp1 = newDp1;
}
return dp0;
}
3.2 边界条件处理
在实际编码中,需要特别注意边界条件:
- 空数组或单元素数组直接返回0
- 价格数组可能很长,要注意时间效率
- 价格可能为0或负数(虽然题目通常给出正数)
3.3 测试用例设计
好的测试用例应该包含:
- 单调递增序列
- 单调递减序列
- 波动序列
- 空数组或单元素数组
- 长随机序列
例如:
java复制@Test
public void testMaxProfit() {
Solution solution = new Solution();
assertEquals(7, solution.maxProfit(new int[]{7,1,5,3,6,4}));
assertEquals(4, solution.maxProfit(new int[]{1,2,3,4,5}));
assertEquals(0, solution.maxProfit(new int[]{7,6,4,3,1}));
assertEquals(0, solution.maxProfit(new int[]{}));
assertEquals(0, solution.maxProfit(new int[]{1}));
}
4. 算法比较与选择
4.1 贪心 vs 动态规划
贪心算法的优势:
- 代码简洁
- 空间效率高
- 直观易懂
动态规划的优势:
- 通用性强
- 可以扩展解决更复杂的问题(如交易次数限制)
- 思路清晰,状态定义明确
在实际面试中,如果明确是无限次交易,可以先给出贪心解法,然后讨论动态规划解法展示思考深度。
4.2 复杂度分析
两种方法的时间复杂度都是O(n),但动态规划的空间复杂度在未优化时为O(n),优化后为O(1)。贪心算法始终是O(1)空间。
5. 常见问题与调试技巧
5.1 为什么贪心算法有效?
贪心算法有效的核心原因是:在允许无限次交易的情况下,任何上升区间都可以被分解为连续的小上升区间。例如,从1涨到5可以看作1→2→3→4→5,每天买入卖出和一次性买入卖出的利润相同。
5.2 动态规划状态转移的理解
关键点:
- dp[i][0]:今天不持有股票,可能是昨天就不持有,或者昨天持有今天卖出
- dp[i][1]:今天持有股票,可能是昨天就持有,或者昨天不持有今天买入
5.3 调试技巧
- 打印每天的状态值,观察变化
- 对小的测试用例手动计算预期结果
- 特别注意初始条件的设置
- 检查数组越界问题
例如可以添加调试打印:
java复制System.out.println("Day "+i+": cash="+dp0+", hold="+dp1);
6. 扩展思考
6.1 交易费用的情况
如果每次交易有固定费用fee,如何修改算法?
动态规划解法只需稍作修改:
java复制dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1] + prices[i] - fee);
dp[i][1] = Math.max(dp[i-1][1], dp[i-1][0] - prices[i]);
6.2 冷却期的情况
如果卖出后需要冷却一天才能买入,如何解决?
状态转移需要调整:
java复制dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1] + prices[i]);
dp[i][1] = Math.max(dp[i-1][1], (i>=2?dp[i-2][0]:0) - prices[i]);
6.3 最多k次交易的情况
这是更通用的解法,需要三维动态规划:
java复制dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i])
dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i])
在实际编码中,当k很大时可以优化为无限次交易的情况。