1. 买卖股票问题全解析:从入门到精通
在算法面试和编程竞赛中,买卖股票系列问题堪称经典中的经典。我第一次接触这个系列是在准备技术面试时,当时就被它多变的题型和精妙的解法深深吸引。这个系列问题不仅考察基础编程能力,更是检验动态规划思维和优化技巧的绝佳试金石。
LeetCode上的买卖股票问题共有8道变种(121、122、123、188、3573、714、309、3652),表面看都是关于股票交易,实则每道题都对交易规则做出了不同限制,需要采用完全不同的解题策略。有些题目可以用简单的贪心算法解决,有些则需要复杂的动态规划状态机模型,甚至需要三维DP数组才能准确描述所有可能状态。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 问题分类与核心思路
2.1 基础题型解析
121. 买卖股票的最佳时机是最简单的版本,它限制我们只能完成一笔交易(买一次+卖一次)。这个限制条件使得问题可以用O(n)时间复杂度的解法解决:
python复制def maxProfit(prices):
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
这个解法的核心思想是:在遍历过程中记录历史最低价,然后用当前价格减去历史最低价得到当前可能的最大利润。这种解法本质上是一种贪心算法,因为我们只需要维护两个变量就能得出最优解。
2.2 进阶题型分析
122. 买卖股票的最佳时机 II取消了交易次数限制,允许无限次买卖。这使得问题性质发生了根本变化:
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
这道题的解法更加简单:只要今天的价格比昨天高,就执行"昨天买今天卖"的操作。这种策略能够捕捉所有的上涨区间,实现利润最大化。
注意:这个解法成立的前提是交易没有手续费,且可以当天卖出后立即买入。如果有手续费(如714题),这种策略就不适用了。
3. 动态规划解法框架
3.1 状态定义与转移方程
当问题加入更多限制条件时(如交易次数限制、冷却期等),贪心算法就不再适用,必须采用动态规划。我们可以定义一个统一的DP框架:
code复制dp[i][k][0/1]
i: 第i天
k: 剩余交易次数
0/1: 不持有/持有股票
状态转移方程如下:
code复制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])
3.2 空间优化技巧
直接使用三维数组会消耗大量内存,我们可以根据题目特点进行优化。以123. 买卖股票的最佳时机 III为例(最多完成两笔交易):
python复制def maxProfit(prices):
buy1 = buy2 = float('-inf')
sell1 = sell2 = 0
for price in prices:
buy1 = max(buy1, -price)
sell1 = max(sell1, buy1 + price)
buy2 = max(buy2, sell1 - price)
sell2 = max(sell2, buy2 + price)
return sell2
这个解法将空间复杂度优化到了O(1),只用了四个变量就完整描述了所有可能的状态。关键在于理解每个变量代表的含义:
- buy1: 第一次买入后的最大利润(负值)
- sell1: 第一次卖出后的最大利润
- buy2: 第二次买入后的最大利润
- sell2: 第二次卖出后的最大利润
4. 复杂变种问题解析
4.1 含冷却期的股票问题(309题)
这道题在卖出股票后有一天的冷却期,不能立即买入。这需要我们对状态进行更细致的划分:
python复制def maxProfit(prices):
if not prices:
return 0
n = len(prices)
dp = [[0]*3 for _ in range(n)]
# dp[i][0]: 持有股票
# dp[i][1]: 不持有股票,处于冷却期
# dp[i][2]: 不持有股票,不处于冷却期
dp[0][0] = -prices[0]
for i in range(1, n):
dp[i][0] = max(dp[i-1][0], dp[i-1][2] - prices[i])
dp[i][1] = dp[i-1][0] + prices[i]
dp[i][2] = max(dp[i-1][1], dp[i-1][2])
return max(dp[-1][1], dp[-1][2])
4.2 含手续费的股票问题(714题)
每次交易需要支付固定手续费,我们需要在卖出时扣除手续费:
python复制def maxProfit(prices, fee):
cash, hold = 0, -prices[0]
for i in range(1, len(prices)):
cash = max(cash, hold + prices[i] - fee)
hold = max(hold, cash - prices[i])
return cash
这个解法使用了两个状态变量:
- cash: 不持有股票时的最大利润
- hold: 持有股票时的最大利润
5. 实战技巧与常见错误
5.1 边界条件处理
在实际编码中,边界条件常常是出错的重灾区。有几个关键点需要注意:
- 空输入处理:当prices为空时应该直接返回0
- 单日价格处理:当只有一天价格时无法完成交易
- 价格全降序列:这种情况下最大利润应该是0(不交易)
5.2 状态初始化技巧
DP问题的状态初始化往往决定了整个算法的正确性。对于股票问题:
- 初始持有状态应该设为-prices[0](第一天买入)
- 初始不持有状态应该设为0
- 对于多交易次数的问题,初始buy状态应该设为负无穷(表示不可能状态)
5.3 调试与验证方法
当你的解法出现问题时,可以采用以下调试策略:
- 打印DP表格,检查每个状态的变化是否符合预期
- 用小规模测试用例手动计算预期结果
- 检查状态转移方程是否所有情况都考虑到了
- 特别注意索引是否越界(特别是i-1的情况)
6. 性能优化进阶
6.1 时间复杂度分析
不同解法的时间复杂度差异很大:
- 贪心解法:O(n)
- 基本DP解法:O(n)
- 带交易次数限制的DP:O(kn)(k为交易次数限制)
- 暴力解法:O(n^2)
在实际面试中,应该优先考虑最优解法,但也可以从暴力解法开始,逐步优化。
6.2 空间复杂度优化
从三维DP到二维再到一维的空间优化过程:
- 原始三维DP:O(kn^2)
- 优化为二维:O(kn)
- 进一步优化:O(k)或O(1)
以188题为例,当k很大时(k>n/2),问题实际上退化为无限次交易的情况,可以直接用贪心算法解决:
python复制def maxProfit(k, prices):
if k >= len(prices)//2:
return sum(max(prices[i+1]-prices[i],0) for i in range(len(prices)-1))
dp = [[0]*(k+1) for _ in range(len(prices))]
for j in range(1, k+1):
min_price = prices[0]
for i in range(1, len(prices)):
min_price = min(min_price, prices[i] - dp[i-1][j-1])
dp[i][j] = max(dp[i-1][j], prices[i] - min_price)
return dp[-1][k]
7. 问题变种与扩展思考
7.1 其他限制条件下的变种
除了LeetCode上的标准题目,买卖股票问题还有很多有趣的变种:
- 交易时间限制(如每天最多交易一次)
- 持仓限制(如最多持有1股)
- 多支股票同时交易
- 加入做空机制
7.2 实际市场中的应用
虽然这些问题是简化模型,但核心思想在实际量化交易中有重要应用:
- 统计套利策略
- 均值回归策略
- 动量策略
- 高频交易策略
理解这些基础算法有助于开发更复杂的交易系统。
