【华为机试】122. 买卖股票的最佳时机 II
文章目录
- 122. 买卖股票的最佳时机 II
- 描述
- 示例 1
- 示例 2
- 示例 3
- 提示
- 解题思路
- 核心观察
- 关键洞察
- 算法实现
- 方法1:贪心算法(推荐)
- 方法2:动态规划
- 方法3:动态规划(空间优化)
- 方法4:波峰波谷法
- 算法分析
- 复杂度对比
- 算法流程图
- 贪心算法证明
- 示例分析
- 动态规划详解
- 状态转移方程
- 状态转移图
- DP状态表(示例)
- 实际应用
- 1. 投资策略
- 2. 类似问题
- 3. 实际约束
- 代码实现要点
- 边界条件处理
- 数组越界防护
- 整数溢出考虑
- 练习建议
- 相关题目
- 完整题解代码
122. 买卖股票的最佳时机 II
描述
给你一个整数数组 prices ,其中 prices[i] 表示某支股票第 i 天的价格。
在每一天,你可以决定是否购买和/或出售股票。你在任何时候 最多 只能持有 一股 股票。你也可以先购买,然后在 同一天 出售。
返回 你能获得的 最大 利润 。
示例 1
输入:prices = [7,1,5,3,6,4]
输出:7
解释:在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5 - 1 = 4。
随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6 - 3 = 3。
最大总利润为 4 + 3 = 7 。
示例 2
输入:prices = [1,2,3,4,5]
输出:4
解释:在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5 - 1 = 4。
最大总利润为 4 。
示例 3
输入:prices = [7,6,4,3,1]
输出:0
解释:在这种情况下, 交易无法获得正利润,所以不参与交易可以获得最大利润,最大利润为 0。
提示
- 1 <= prices.length <= 3 * 10^4
- 0 <= prices[i] <= 10^4
解题思路
这道题的核心思想是:由于可以进行多次买卖,我们需要抓住每一次股价上涨的机会来获得最大利润。
核心观察
- 无限制交易:可以进行任意次数的买卖
- 同一天可以买卖:先买再卖,或者先卖再买
- 最多持有一股:不能同时持有多股
- 目标:获得最大总利润
关键洞察
贪心策略:只要明天的价格比今天高,就今天买入明天卖出。这样可以捕获所有的价格上涨段。
算法实现
方法1:贪心算法(推荐)
func maxProfitGreedy(prices []int) int {maxProfit := 0for i := 1; i < len(prices); i++ {if prices[i] > prices[i-1] {maxProfit += prices[i] - prices[i-1]}}return maxProfit
}
原理:累加所有相邻的正收益。
时间复杂度:O(n)
空间复杂度:O(1)
方法2:动态规划
func maxProfitDP(prices []int) int {n := len(prices)dp := make([][2]int, n)dp[0][0] = 0 // 不持有股票dp[0][1] = -prices[0] // 持有股票for i := 1; i < n; i++ {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]) // 买入}return dp[n-1][0]
}
状态定义:
dp[i][0]
:第i天结束后不持有股票的最大利润dp[i][1]
:第i天结束后持有股票的最大利润
时间复杂度:O(n)
空间复杂度:O(n)
方法3:动态规划(空间优化)
func maxProfitDPOptimized(prices []int) int {hold := -prices[0] // 持有股票的最大利润sold := 0 // 不持有股票的最大利润for i := 1; i < len(prices); i++ {newSold := max(sold, hold+prices[i])newHold := max(hold, sold-prices[i])sold, hold = newSold, newHold}return sold
}
时间复杂度:O(n)
空间复杂度:O(1)
方法4:波峰波谷法
找到所有的波谷和波峰,在波谷买入,在波峰卖出。
func maxProfitPeakValley(prices []int) int {i, maxProfit := 0, 0for i < len(prices)-1 {// 找波谷for i < len(prices)-1 && prices[i+1] <= prices[i] {i++}valley := prices[i]// 找波峰for i < len(prices)-1 && prices[i+1] >= prices[i] {i++}peak := prices[i]maxProfit += peak - valley}return maxProfit
}
算法分析
复杂度对比
方法 | 时间复杂度 | 空间复杂度 | 优点 | 缺点 |
---|---|---|---|---|
贪心算法 | O(n) | O(1) | 简洁高效,易理解 | 需要理解贪心思想 |
动态规划 | O(n) | O(n) | 状态转移清晰 | 空间开销大 |
动态规划优化 | O(n) | O(1) | 空间效率高 | 状态维护复杂 |
波峰波谷 | O(n) | O(1) | 直观易懂 | 代码稍复杂 |
状态机 | O(n) | O(1) | 逻辑清晰 | 理解成本高 |
算法流程图
贪心算法证明
命题:对于任意价格序列,贪心策略(累加所有正差值)能够获得最大利润。
证明:
- 可行性:贪心策略产生的交易序列是合法的(每次买入后必须卖出才能再次买入)
- 最优性:任何其他交易策略的利润都不会超过贪心策略
设最优解包含交易 (buy_i, sell_i)
,其总利润为:
profit = Σ(sell_i - buy_i)
贪心策略的利润为:
greedy_profit = Σ(prices[i] - prices[i-1]) for all i where prices[i] > prices[i-1]
可以证明:greedy_profit >= profit
示例分析
示例1:prices = [7,1,5,3,6,4]
贪心分析:
第1天: 7 -> 第2天: 1,下跌,不交易
第2天: 1 -> 第3天: 5,上涨,利润 = 5-1 = 4
第3天: 5 -> 第4天: 3,下跌,不交易
第4天: 3 -> 第5天: 6,上涨,利润 = 6-3 = 3
第5天: 6 -> 第6天: 4,下跌,不交易总利润 = 4 + 3 = 7
可视化:
价格走势图:7 |●| \5 | ●| \3 | ●---●| \1 | ● ●+--+--+--+--+--+1 2 3 4 5 天数交易策略:第2天买入(1) -> 第3天卖出(5):利润4第4天买入(3) -> 第5天卖出(6):利润3
动态规划详解
状态转移方程
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])
状态转移图
DP状态表(示例)
对于 prices = [7,1,5,3,6,4]
:
天数 | 价格 | 不持有股票 | 持有股票 | 决策 |
---|---|---|---|---|
0 | 7 | 0 | -7 | 初始状态 |
1 | 1 | 0 | -1 | 在第1天买入更优 |
2 | 5 | 4 | -1 | 第2天卖出获利4 |
3 | 3 | 4 | 1 | 第3天买入 |
4 | 6 | 7 | 1 | 第4天卖出获利3 |
5 | 4 | 7 | 3 | 保持不持有状态 |
实际应用
1. 投资策略
- 短线交易:频繁买卖获取价差
- 波段操作:在价格波动中获利
- 算法交易:程序化交易系统
2. 类似问题
- 买卖股票的最佳时机系列题目
- 背包问题的变种
- 区间调度问题
3. 实际约束
在实际投资中还需考虑:
- 交易手续费
- 税收影响
- 市场流动性
- 价格滑点
代码实现要点
边界条件处理
if len(prices) <= 1 {return 0 // 少于2个价格无法交易
}
数组越界防护
for i := 1; i < len(prices); i++ { // 从第2个元素开始// 安全访问 prices[i] 和 prices[i-1]
}
整数溢出考虑
对于极大数据,考虑使用 int64
或添加溢出检查。
练习建议
- 理解贪心思想:为什么累加所有正差值是最优的?
- 掌握状态转换:DP中两个状态如何相互转换?
- 空间优化技巧:如何从O(n)优化到O(1)?
- 扩展思考:如果有交易手续费怎么办?
- 变种练习:限制交易次数的情况如何处理?
相关题目
- 121. 买卖股票的最佳时机(只能交易一次)
- 123. 买卖股票的最佳时机 III(最多交易两次)
- 188. 买卖股票的最佳时机 IV(最多交易k次)
- 309. 最佳买卖股票时机含冷冻期(含冷冻期)
- 714. 买卖股票的最佳时机含手续费(含手续费)
完整题解代码
package mainimport ("fmt""time"
)// 方法1:贪心算法 - 抓住每一次上涨机会
// 时间复杂度:O(n),空间复杂度:O(1)
func maxProfitGreedy(prices []int) int {if len(prices) <= 1 {return 0}maxProfit := 0// 只要第二天比第一天价格高,就在第一天买入,第二天卖出for i := 1; i < len(prices); i++ {if prices[i] > prices[i-1] {maxProfit += prices[i] - prices[i-1]}}return maxProfit
}// 方法2:动态规划
// 时间复杂度:O(n),空间复杂度:O(n)
func maxProfitDP(prices []int) int {if len(prices) <= 1 {return 0}n := len(prices)// dp[i][0] 表示第i天不持有股票的最大利润// dp[i][1] 表示第i天持有股票的最大利润dp := make([][2]int, n)// 初始状态dp[0][0] = 0 // 第0天不持有股票,利润为0dp[0][1] = -prices[0] // 第0天买入股票,利润为-prices[0]for i := 1; i < n; i++ {// 第i天不持有股票:可能是前一天就不持有,或者前一天持有今天卖出dp[i][0] = max(dp[i-1][0], dp[i-1][1]+prices[i])// 第i天持有股票:可能是前一天就持有,或者前一天不持有今天买入dp[i][1] = max(dp[i-1][1], dp[i-1][0]-prices[i])}// 最后一天不持有股票肯定比持有股票利润更大return dp[n-1][0]
}// 方法3:动态规划(空间优化)
// 时间复杂度:O(n),空间复杂度:O(1)
func maxProfitDPOptimized(prices []int) int {if len(prices) <= 1 {return 0}// 只需要记录当前的状态,不需要整个数组hold := -prices[0] // 持有股票的最大利润sold := 0 // 不持有股票的最大利润for i := 1; i < len(prices); i++ {newSold := max(sold, hold+prices[i]) // 今天卖出newHold := max(hold, sold-prices[i]) // 今天买入sold = newSoldhold = newHold}return sold
}func max(a, b int) int {if a > b {return a}return b
}func main() {testCases := [][]int{{7, 1, 5, 3, 6, 4}, // 预期输出: 7{1, 2, 3, 4, 5}, // 预期输出: 4{7, 6, 4, 3, 1}, // 预期输出: 0{1, 2, 1, 2, 1}, // 预期输出: 2{5}, // 预期输出: 0{}, // 预期输出: 0}fmt.Println("=== 买卖股票的最佳时机 II - 多种解法测试 ===")fmt.Println()methods := []struct {name stringfn func([]int) int}{{"贪心算法", maxProfitGreedy},{"动态规划", maxProfitDP},{"动态规划(优化)", maxProfitDPOptimized},}for i, testCase := range testCases {fmt.Printf("测试用例 %d: %v\n", i+1, testCase)for _, method := range methods {start := time.Now()result := method.fn(testCase)duration := time.Since(start)fmt.Printf(" %s: %d (用时: %v)\n", method.name, result, duration)}fmt.Println()}// 算法思路演示fmt.Println("=== 算法思路演示 ===")demonstrateGreedyApproach([]int{7, 1, 5, 3, 6, 4})
}func demonstrateGreedyApproach(prices []int) {fmt.Printf("\n贪心算法思路演示:\n")fmt.Printf("价格数组: %v\n", prices)maxProfit := 0transactions := []string{}for i := 1; i < len(prices); i++ {if prices[i] > prices[i-1] {profit := prices[i] - prices[i-1]maxProfit += profittransaction := fmt.Sprintf("第%d天买入(%d) -> 第%d天卖出(%d), 利润: %d", i, prices[i-1], i+1, prices[i], profit)transactions = append(transactions, transaction)}}fmt.Println("交易记录:")for _, transaction := range transactions {fmt.Printf(" %s\n", transaction)}fmt.Printf("总利润: %d\n", maxProfit)
}