买卖股票的最佳时机2是力扣平台上经典的动态规划练习题(题号122)。这道题要求我们在给定的股票价格序列中,通过多次买卖操作获取最大利润。与只能买卖一次的基础版本不同,这道题的难点在于如何识别所有可能的盈利机会。
举个生活中的例子:假设你是个水果摊主,每天苹果的价格都在波动。你可以在低价时买入,高价时卖出,如此反复操作。这道题就是要计算出在这种反复买卖的情况下,最多能赚多少钱。
题目给出的典型输入是一个数组,比如[7,1,5,3,6,4],表示连续几天的股票价格。我们需要设计算法找出在这些天里通过多次买卖能获得的最大利润。
最直观的想法是尝试所有可能的买卖组合。对于n天的价格序列,有O(2^n)种可能的交易序列。这种方法虽然理论上可行,但在实际中完全不可行,因为时间复杂度太高。
我曾经尝试用递归实现暴力搜索,当n=20时,在我的笔记本上运行就超过了10分钟。这提醒我们:在算法设计中,时间复杂度的考量至关重要。
经过分析发现,这道题有一个关键特性:只要今天的价格比昨天高,就可以获得利润。因此可以采用贪心算法:
python复制def maxProfit(prices):
profit = 0
for i in range(1, len(prices)):
if prices[i] > prices[i-1]:
profit += prices[i] - prices[i-1]
return profit
这个算法的时间复杂度是O(n),空间复杂度是O(1),效率非常高。它背后的逻辑是:不放过任何可能的盈利机会,把所有上涨的差价都收入囊中。
注意:贪心算法在这里有效的前提是交易次数不受限。如果限制了交易次数,就需要更复杂的动态规划解法。
虽然贪心算法已经足够好,但为了理解更通用的股票买卖问题解法,我们可以用动态规划来解决:
定义状态:
状态转移方程:
python复制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]) # 保持持仓或买入
初始化:
python复制dp[0][0] = 0 # 第一天不持有
dp[0][1] = -prices[0] # 第一天持有
这种解法虽然代码稍长,但它为更复杂的股票问题(如带手续费、冷冻期等)提供了框架思路。
贪心算法的Python实现有几个优化点:
python复制return sum(max(prices[i]-prices[i-1],0) for i in range(1,len(prices)))
动态规划版本可以优化空间复杂度到O(1):
python复制def maxProfit(prices):
cash, hold = 0, -prices[0]
for price in prices[1:]:
cash, hold = max(cash, hold + price), max(hold, cash - price)
return cash
实际编码时要特别注意边界条件:
完整的防御性编程示例:
python复制def maxProfit(prices):
if not prices or len(prices) < 2:
return 0
# 主逻辑...
为什么贪心算法在这里有效?可以通过数学归纳法证明:
假设对于前k天,贪心算法能获得最大利润。对于第k+1天:
因此贪心策略不会错过任何盈利机会,同时避免了亏损交易。
| 方法 | 时间复杂度 | 空间复杂度 | 适用场景 |
|---|---|---|---|
| 暴力搜索 | O(2^n) | O(n) | 仅理论分析 |
| 贪心算法 | O(n) | O(1) | 无交易限制 |
| 动态规划 | O(n) | O(n)或O(1) | 通用,可扩展 |
错误理解题意:以为必须先买后卖,实际上可以当天卖出后立即买入
索引越界:直接比较prices[i]和prices[i+1]导致数组越界
初始条件错误:动态规划版本忘记初始化dp[0][1]
使用小测试案例手动验证:
python复制assert maxProfit([1,2,3]) == 2
assert maxProfit([3,2,1]) == 0
打印中间变量:
python复制print(f"Day {i}: price={price}, cash={cash}, hold={hold}")
可视化价格走势:
python复制import matplotlib.pyplot as plt
plt.plot(prices)
plt.show()
如果每次交易有固定手续费fee,动态规划方程需要调整:
python复制cash = max(cash, hold + price - fee) # 卖出时扣除手续费
hold = max(hold, cash - price) # 买入不受影响
卖出后需要等待一天才能再买入,状态转移需要调整:
python复制cash = max(cash, hold + price)
hold = max(hold, prev_cash - price) # 使用前天的cash
prev_cash = cash # 保存前一天的cash
这是更通用的版本,需要三维DP数组:
python复制dp[k][i][0] = max(dp[k][i-1][0], dp[k][i-1][1] + prices[i])
dp[k][i][1] = max(dp[k][i-1][1], dp[k-1][i-1][0] - prices[i])
这类算法不仅用于股票交易,还可以应用于:
比如电商平台的自动定价系统,就需要实时分析价格走势,决定何时补货、何时促销。我在参与一个电商项目时,就借鉴了这种思路来优化库存周转。
当价格序列很长时(如高频交易数据),可以考虑以下优化:
合并连续上涨/下跌:将连续上涨的天数合并为单次交易
python复制i, n, profit = 0, len(prices), 0
while i < n-1:
while i < n-1 and prices[i] >= prices[i+1]: i += 1
valley = prices[i]
while i < n-1 and prices[i] <= prices[i+1]: i += 1
peak = prices[i]
profit += peak - valley
并行计算:将价格序列分块,用多线程处理
增量处理:对于实时数据流,维护当前状态而非重新计算
全面的测试应该包括:
| 测试类型 | 示例输入 | 预期输出 |
|---|---|---|
| 常规情况 | [7,1,5,3,6,4] | 7 |
| 单调递增 | [1,2,3,4,5] | 4 |
| 单调递减 | [7,6,4,3,1] | 0 |
| 平顶/平底 | [1,1,1,1,1] | 0 |
| 大波动 | [1,10,1,10,1] | 18 |
| 空数组 | [] | 0 |
| 单元素 | [5] | 0 |
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;
}
cpp复制int maxProfit(vector<int>& prices) {
int profit = 0;
for (size_t i = 1; i < prices.size(); ++i) {
profit += max(prices[i] - prices[i-1], 0);
}
return profit;
}
javascript复制function maxProfit(prices) {
return prices.slice(1).reduce((sum, price, i) =>
sum + Math.max(price - prices[i], 0), 0);
}
不同语言的实现虽然语法不同,但核心逻辑一致。Java版本更严谨,C++版本性能最优,JavaScript版本最简洁。
根据不同的应用场景,我建议:
在实际项目中,我通常会先写贪心算法版本作为基准,然后用动态规划版本处理更复杂的约束条件。这种分层实现策略既保证了开发效率,又保留了扩展性。