1. 问题背景与核心挑战
股票交易中的手续费是影响最终收益的关键因素之一。在LeetCode 714题"买卖股票的最佳时机含手续费"中,我们需要设计一个算法,在考虑交易成本的情况下,计算出能够获得的最大利润。这与常规的股票买卖问题最大的区别在于:每次卖出操作都需要支付固定金额的手续费。
这个问题的现实意义非常强。在实际的股票交易中,无论是通过券商平台还是其他渠道,每笔交易都会产生一定的费用。这些费用虽然单笔看起来不大,但在高频交易或大额交易中,会显著影响最终的收益。我曾经在一次量化交易策略的回测中就因为没有充分考虑手续费因素,导致模拟收益和实际收益出现了15%的偏差。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 问题分析与建模思路
2.1 问题重述
给定一个整数数组prices,其中prices[i]表示第i天股票的价格;以及一个整数fee表示交易股票的手续费用。你可以无限次地完成交易,但每次交易都需要付手续费。注意:你不能同时参与多笔交易(必须在再次购买前出售掉之前的股票)。
2.2 动态规划解法框架
这类股票买卖问题通常可以采用动态规划(DP)来解决。我们需要定义状态转移方程来描述不同状态下的最优解。
定义两个状态:
- dp[i][0]:第i天结束时,手中不持有股票时的最大利润
- dp[i][1]:第i天结束时,手中持有股票时的最大利润
初始状态:
- dp[0][0] = 0
- dp[0][1] = -prices[0] (第一天就买入,利润为负的股价)
2.3 状态转移方程推导
对于第i天:
-
如果当天结束时手中不持有股票(dp[i][0]),可能有两种情况:
- 前一天也不持有:dp[i-1][0]
- 前一天持有,今天卖出:dp[i-1][1] + prices[i] - fee
取两者较大值:dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i] - fee)
-
如果当天结束时手中持有股票(dp[i][1]),可能有两种情况:
- 前一天也持有:dp[i-1][1]
- 前一天不持有,今天买入:dp[i-1][0] - prices[i]
取两者较大值:dp[i][1] = max(dp[i-1][1], dp[i-1][0] - prices[i])
最终的最大利润就是dp[n-1][0](最后一天不持有股票)
3. 代码实现与优化
3.1 基础DP实现
python复制def maxProfit(prices, fee):
n = len(prices)
if n < 2:
return 0
dp = [[0]*2 for _ in range(n)]
dp[0][0] = 0
dp[0][1] = -prices[0]
for i in range(1, n):
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])
return dp[n-1][0]
3.2 空间优化版本
观察状态转移方程可以发现,dp[i]只依赖于dp[i-1],因此可以优化空间复杂度到O(1):
python复制def maxProfit(prices, fee):
n = len(prices)
if n < 2:
return 0
cash = 0 # 不持有股票时的最大利润
hold = -prices[0] # 持有股票时的最大利润
for i in range(1, n):
cash = max(cash, hold + prices[i] - fee)
hold = max(hold, cash - prices[i])
return cash
3.3 贪心算法解法
除了DP,这个问题还可以用贪心算法解决。基本思路是:在价格上涨时持有股票,在价格足够高时卖出(考虑手续费后的净收益)。
python复制def maxProfit(prices, fee):
n = len(prices)
if n < 2:
return 0
profit = 0
min_price = prices[0]
for i in range(1, n):
if prices[i] < min_price:
min_price = prices[i]
elif prices[i] > min_price + fee:
profit += prices[i] - min_price - fee
min_price = prices[i] - fee # 关键点:避免重复扣除手续费
return profit
这里的关键点在于卖出后,我们将min_price更新为prices[i] - fee。这样可以避免在后续连续上涨时重复扣除手续费。
4. 算法选择与性能对比
4.1 时间复杂度分析
两种方法的时间复杂度都是O(n),只需要一次遍历数组。
4.2 空间复杂度分析
- 基础DP:O(n)
- 优化DP:O(1)
- 贪心算法:O(1)
4.3 适用场景选择
- DP方法更加通用,可以处理更复杂的交易规则
- 贪心算法效率更高,但需要更深入的问题理解和特定的条件
在实际应用中,如果只是解决这个特定问题,贪心算法是更好的选择。但如果是作为更复杂交易策略的一部分,DP框架更容易扩展。
5. 边界条件与测试用例
5.1 常见测试用例
python复制# 示例1
prices = [1,3,2,8,4,9]
fee = 2
# 输出:8
# 解释:买入 prices[0] = 1,卖出 prices[3] = 8,买入 prices[4] = 4,卖出 prices[5] = 9
# 利润:(8-1-2) + (9-4-2) = 8
# 示例2
prices = [1,3,7,5,10,3]
fee = 3
# 输出:6
5.2 特殊边界情况
-
空数组或单元素数组:
python复制prices = [] fee = 1 # 输出:0 prices = [5] fee = 2 # 输出:0 -
价格持续下跌:
python复制prices = [7,6,5,4,3,2] fee = 1 # 输出:0 -
手续费为0:
python复制prices = [1,3,2,8,4,9] fee = 0 # 输出:8 (可以频繁交易) -
手续费极高:
python复制prices = [1,3,2,8,4,9] fee = 10 # 输出:0 (任何交易都不划算)
6. 实际应用中的注意事项
6.1 手续费计算方式的差异
在实际交易中,手续费的计算可能有多种方式:
- 固定费用(如本题)
- 按交易金额的百分比
- 两者结合(如固定费用+百分比)
本题的解法适用于固定费用场景。如果是百分比费用,状态转移方程需要相应调整。
6.2 高频交易的影响
虽然算法允许无限次交易,但在现实中:
- 高频交易可能导致手续费累积
- 需要考虑交易的最小单位(如A股是100股)
- 平台可能有交易频率限制
6.3 滑点问题
实际交易中还存在"滑点"问题:
- 下单价格和成交价格可能有差异
- 在大额交易时尤为明显
- 需要考虑在算法中加入缓冲空间
7. 算法扩展与变种
7.1 多支股票的情况
如果可以选择多支股票进行交易,但每次只能持有一支股票:
- 可以维护每个股票的持有/不持有状态
- 状态转移需要考虑不同股票间的切换
7.2 交易次数限制
如果加上交易次数限制(如最多k次交易):
- 需要增加一个维度来记录交易次数
- 状态变为dp[i][k][0/1]
7.3 冷却期限制
如果卖出后需要等待一天才能再次买入:
- 状态转移方程需要调整
- 买入时只能从前两天的不持有状态转移
8. 个人实战经验分享
在实现这个算法时,有几个容易出错的地方值得注意:
-
手续费扣除时机:
- 应该在卖出时扣除手续费
- 有些初学者会在买入时扣除,这会导致逻辑错误
-
贪心算法中的min_price更新:
- 卖出后更新为prices[i] - fee是关键
- 我最初实现时忽略了这点,导致在连续上涨行情中多扣除了手续费
-
初始状态设置:
- dp[0][1]应该初始化为-prices[0]
- 如果初始化为0,会导致第一天无法买入
-
空间优化时的变量更新顺序:
- 在优化版DP中,需要先保存旧的cash值
- 因为计算hold时需要用到未更新的cash值
一个实用的调试技巧是打印出每天的dp状态或cash/hold值,这样可以直观地看到算法的决策过程,便于发现逻辑错误。
