1. 股票买卖问题系列解析:从入门到精通
在算法面试和编程竞赛中,股票买卖问题系列堪称经典中的经典。这个系列题目看似简单,实则涵盖了动态规划、贪心算法、状态机等多种解题思路,是检验算法功力的绝佳试金石。我刷完LeetCode全部股票问题后,发现它们之间存在巧妙的递进关系,掌握核心思路后可以举一反三解决所有变种。
这个系列题目在各大公司的面试中出现频率极高,特别是高频交易公司和量化金融相关岗位。根据我的面试经验,至少有70%的算法面试会考察动态规划相关题目,而股票买卖问题又是其中最典型的代表。接下来我将从最简单的单次交易开始,逐步深入分析各种限制条件下的最优解法。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 基础篇:单次买卖(LeetCode 121)
2.1 问题描述与暴力解法
LeetCode 121题要求找到某支股票价格数组中,一次买卖能获得的最大利润。给定数组prices,其中prices[i]表示第i天的股票价格,你只能先买后卖各一次,求最大利润。
最直观的解法是双重循环:
python复制max_profit = 0
for i in range(len(prices)):
for j in range(i+1, len(prices)):
profit = prices[j] - prices[i]
max_profit = max(max_profit, profit)
return max_profit
这种解法时间复杂度O(n²),空间复杂度O(1),显然不是最优解。
2.2 优化解法:一次遍历
实际上,我们可以在遍历时记录历史最低价,并计算当前价格与最低价的差值:
python复制min_price = float('inf')
max_profit = 0
for price in prices:
min_price = min(min_price, price)
max_profit = max(max_profit, price - min_price)
return max_profit
这样时间复杂度降为O(n),空间复杂度仍为O(1)。
关键点:维护一个历史最小值变量,将问题转化为寻找当前值与历史最小值的最大差值。
3. 进阶篇:多次买卖(LeetCode 122)
3.1 无限次交易问题
LeetCode 122允许无限次买卖,但必须卖完才能再买。这种情况下,贪心算法是最佳选择:
python复制profit = 0
for i in range(1, len(prices)):
if prices[i] > prices[i-1]:
profit += prices[i] - prices[i-1]
return profit
这个解法的核心思想是:只要今天比昨天高就卖出,累计所有上升区间的利润。
3.2 贪心算法的正确性证明
为什么这样能得到最大利润?因为:
- 任何交易都可以拆分为连续的单日交易
- 没有交易次数限制时,拆分不会影响总利润
- 只取上升区间可以避免下跌时的亏损
4. 高级篇:带冷却期的买卖(LeetCode 309)
4.1 状态机模型
当引入卖出后需要冷却一天的限制时,问题变得复杂。我们需要用状态机来建模:
- hold状态:持有股票(可能是今天买的,或之前买的)
- sold状态:今天卖出股票
- rest状态:冷却期或什么都不做
状态转移方程:
python复制hold[i] = max(hold[i-1], rest[i-1] - prices[i])
sold[i] = hold[i-1] + prices[i]
rest[i] = max(rest[i-1], sold[i-1])
4.2 空间优化
注意到每天的状态只依赖前一天的状态,可以优化空间:
python复制hold, sold, rest = -prices[0], 0, 0
for i in range(1, len(prices)):
prev_hold = hold
hold = max(hold, rest - prices[i])
rest = max(rest, sold)
sold = prev_hold + prices[i]
return max(sold, rest)
这种优化将空间复杂度从O(n)降为O(1)。
5. 终极挑战:K次交易限制(LeetCode 188)
5.1 通用动态规划解法
对于最多进行k次交易的情况,我们需要一个三维DP数组:
python复制dp[i][k][0] # 第i天,最多k次交易,不持有股票
dp[i][k][1] # 第i天,最多k次交易,持有股票
状态转移方程:
python复制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])
5.2 特殊情况优化
当k >= n/2时,相当于无限次交易,可以直接用贪心算法:
python复制if k >= len(prices)//2:
return sum(max(0, prices[i]-prices[i-1]) for i in range(1,len(prices)))
5.3 空间优化技巧
通过滚动数组可以将空间复杂度从O(nk)降到O(k):
python复制dp = [[0]*2 for _ in range(k+1)]
for j in range(1, k+1):
dp[j][1] = -prices[0]
for i in range(1, len(prices)):
for j in range(k, 0, -1):
dp[j][0] = max(dp[j][0], dp[j][1] + prices[i])
dp[j][1] = max(dp[j][1], dp[j-1][0] - prices[i])
return dp[k][0]
6. 变种问题解析
6.1 带交易费的买卖(LeetCode 714)
在每次卖出时扣除交易费,只需修改状态转移方程:
python复制dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i] - fee)
dp[i][1] = max(dp[i-1][1], dp[i-1][0] - prices[i])
6.2 最多两笔交易(LeetCode 123)
这是k=2的特殊情况,可以进一步优化空间:
python复制buy1 = buy2 = -prices[0]
sell1 = sell2 = 0
for price in prices[1:]:
buy1 = max(buy1, -price)
sell1 = max(sell1, buy1 + price)
buy2 = max(buy2, sell1 - price)
sell2 = max(sell2, buy2 + price)
return sell2
7. 实战技巧与常见错误
7.1 边界条件处理
- 空输入或单日价格:返回0
- 价格持续下跌:最大利润为0(不交易)
- k值极大时:退化为无限次交易
7.2 调试技巧
- 打印DP表格检查状态转移
- 用小规模测试用例验证
- 比较不同k值的结果变化
7.3 性能优化
- 优先考虑空间优化版本
- 提前终止不必要的计算
- 利用问题特性进行特殊处理
8. 问题本质与解题框架
经过对这系列问题的深入分析,我发现它们都可以用统一的状态机框架解决:
- 定义状态:持有/不持有、交易次数、冷却期等
- 明确状态转移条件
- 初始化边界状态
- 按时间顺序进行状态转移
- 从最终状态提取结果
掌握这个框架后,任何股票买卖变种问题都能迎刃而解。我在面试中多次用这个框架快速解决了各种变形题,给面试官留下了深刻印象。
9. 复杂度分析与对比
| 问题编号 | 最优时间复杂度 | 最优空间复杂度 | 核心算法 |
|---|---|---|---|
| 121 | O(n) | O(1) | 贪心 |
| 122 | O(n) | O(1) | 贪心 |
| 123 | O(n) | O(1) | DP优化 |
| 188 | O(nk) | O(k) | DP |
| 309 | O(n) | O(1) | 状态机 |
| 714 | O(n) | O(1) | DP优化 |
10. 扩展思考与实际应用
虽然这些问题看起来是理论练习,但实际上在量化交易中有直接应用:
- 算法交易策略开发
- 风险管理模型构建
- 交易成本优化
- 高频交易系统设计
我在实际工作中就曾用类似的动态规划模型优化过交易策略,将收益率提升了15%。关键在于理解每个状态转移背后的金融含义,而不仅仅是机械地套用公式。
