文章目录
- 121.买卖股票的最佳时机
- 题目:
- 思路:
- 举例
- 代码实现(Go):
- 122.买卖股票的最佳时机II
- 题目:
- 思路:
- 举例
- 代码实现(Go):
- 进阶思路:动态规划
- 代码实现(Go):
- [空间优化(滚动数组,O (1) 空间)](#空间优化(滚动数组,O (1) 空间))
- 代码实现(Go):
121.买卖股票的最佳时机
题目:
给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。
你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。
返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0 。
示例 1:
输入:[7,1,5,3,6,4]
输出:5
解释:在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。
注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格 ;同时,你不能在买入前卖出股票。
示例 2:
输入:prices = [7,6,4,3,1]
输出:0
解释:在这种情况下, 没有交易完成, 所以最大利润为 0。
提示:
1 <= prices.length <= 10^5
0 <= prices[i] <= 10^4
思路:
想要一次交易的利润最大,本质就是在「遍历到当前天为止的最低点」买入,在该最低点之后的任意一天卖出 (取卖出的最大利润);
遍历过程中只需维护两个关键变量,不用额外空间,边遍历边更新,每个价格只看一次:
minPrice:遍历到当前天时,之前所有天的股票最低价格(也就是到目前为止最划算的买入价);maxProfit:遍历到当前天时,能获取的最大利润(初始为0,无利润时直接返回这个值)。
遍历的核心两步(对每个价格都执行)
- 先更新最小买入价 :如果当前价格比
minPrice更低,就把minPrice换成当前价格(找到更划算的买入点); - 再计算当前利润,更新最大利润 :用当前价格 -
minPrice得到「当天卖出的利润」,如果这个利润比maxProfit大,就更新maxProfit;
关键 :如果当前价格等于minPrice,第一步不会更新(等于不是更低),第二步计算的利润为0,不会超过maxProfit(初始0,后续只增不减),所以无需单独处理等于的情况,自然跳过即可。
举例
初始化
minPrice = 数组第一个元素(题目保证数组非空,无越界风险);maxProfit = 0(无利润时直接返回,利润为负也取0)。
场景1:正常上涨有盈利
输入价格数组:[7,1,5,3,6,4](共6天)
步骤
- 初始化:
minPrice=7,maxProfit=0; - 第2天(价格1):1 < 7 →
minPrice=1;利润1-1=0 → 不大于0,maxProfit=0; - 第3天(价格5):5 不小于1 →
minPrice不变;利润5-1=4 → 大于0 →maxProfit=4; - 第4天(价格3):3 不小于1 →
minPrice不变;利润3-1=2 → 小于4,maxProfit不变; - 第5天(价格6):6 不小于1 →
minPrice不变;利润6-1=5 → 大于4 →maxProfit=5; - 第6天(价格4):4 不小于1 →
minPrice不变;利润4-1=3 → 小于5,maxProfit不变。
结果
maxProfit=5,即第2天买入、第5天卖出,利润最大。
场景2:持续下跌无盈利
输入价格数组:[7,6,4,3,1](共5天)
步骤
- 初始化:
minPrice=7,maxProfit=0; - 第2天(价格6):6 < 7 →
minPrice=6;利润6-6=0 → 不大于0,maxProfit=0; - 第3天(价格4):4 < 6 →
minPrice=4;利润4-4=0 → 不大于0,maxProfit=0; - 第4天(价格3):3 < 4 →
minPrice=3;利润3-3=0 → 不大于0,maxProfit=0; - 第5天(价格1):1 < 3 →
minPrice=1;利润1-1=0 → 不大于0,maxProfit=0。
结果
maxProfit=0,全程股价下跌,无盈利可能,直接返回0。
场景3:含等于价格
输入价格数组:[7,1,1,5,3,6](共6天,第2、3天价格均为1,含等于)
步骤
- 初始化:
minPrice=7,maxProfit=0; - 第2天(价格1):1 < 7 →
minPrice=1;利润1-1=0 → 不大于0,maxProfit=0; - 第3天(价格1):1 不小于1 →
minPrice不变;利润1-1=0 → 不大于0,maxProfit不变; - 第4天(价格5):5 不小于1 →
minPrice不变;利润5-1=4 → 大于0 →maxProfit=4; - 第5天(价格3):3 不小于1 →
minPrice不变;利润3-1=2 → 小于4,maxProfit不变; - 第6天(价格6):6 不小于1 →
minPrice不变;利润6-1=5 → 大于4 →maxProfit=5。
结果
maxProfit=5,和去掉等于价格的数组[7,1,5,3,6]结果一致,等于价格无任何影响,只需要比较小于即可。
时间复杂度:O (n),仅遍历一次价格数组
空间复杂度:O (1),仅使用常数个变量
代码实现(Go):
go
package main
import "fmt"
func maxProfit(prices []int) int {
minPrice := prices[0] // 初始化为首个元素
maxProfit := 0 // 无利润返回0,利润为负也保持0
// 从第二个元素开始遍历(索引1),首个元素已作为初始minPrice
for i := 1; i < len(prices); i++ {
curPrice := prices[i]
// 第一步:更新到目前为止的最小买入价,仅当前价格更小时更新
if curPrice < minPrice {
minPrice = curPrice
}
// 第二步:计算当天卖出的利润,更新最大利润
curProfit := curPrice - minPrice
if curProfit > maxProfit {
maxProfit = curProfit
}
}
return maxProfit
}
func main() {
var n int
fmt.Scan(&n) // 读取价格数组的长度
prices := make([]int, n)
for i := 0; i < n; i++ {
fmt.Scan(&prices[i]) // 循环读取n个价格到数组
}
// 计算并打印最大利润
res := maxProfit(prices)
fmt.Println(res)
}
读取单个数字 / 字符串:fmt.Scan(&变量名)
go
var n int // 定义一个整数变量n
fmt.Scan(&n) // 终端输入一个数字,比如6,n就等于6
var s string
fmt.Scan(&s) // 终端输入一个字符串,s就等于该字符串
循环读取多个值:for循环 + fmt.Scan(&数组元素)
go
// 步骤1:定义变量接收数组长度,读取长度n 【必须加&】
var n int
fmt.Scan(&n)
// 步骤2:初始化一个长度为n的切片/数组 【固定写法】
nums := make([]int, n) // 切片,算法面试首选,比数组灵活
// 步骤3:循环n次,读取n个数字,存入切片 【必须加&nums[i]】
for i := 0; i < n; i++ {
fmt.Scan(&nums[i]) // 每次读取一个数字,赋值给nums[i]
}
读取两个数字
go
var a, b int
fmt.Scan(&a, &b) // 终端输入3 5,a=3,b=5
fmt.Println(a+b) // 输出8
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)数学等价性:拆分交易 = 长期持有
对于任意持续上涨区间 prices[i] < prices[i+1] < ... < prices[j]:
- 拆分交易利润:
(prices[i+1]-prices[i]) + (prices[i+2]-prices[i+1]) + ... + (prices[j]-prices[j-1]) - 单次长期交易利润:
prices[j] - prices[i]
两者展开后中间项完全抵消 ,最终结果相等(比如1→2→3→4→5,拆分赚1+1+1+1=4,长期持有赚5-1=4)。
这意味着:"每天买卖赚小差价"只是数学上的等价拆解,并非真的要高频操作,实际可简化为"低点买、高点卖",利润完全一致。
(2)覆盖所有盈利可能,无遗漏
相邻天数的判断能精准捕捉所有独立上涨区间 (比如 7,1,5,3,6,4 中的 1→5、3→6),不会放过任何可盈利的差价;而"跳天判断"要么不可行(中间下跌时持有会亏损),要么利润与相邻判断相等,无任何优势。
举个反例:如果股价是[1,3,2,4],有人觉得 "1 买 4 卖"(利润 3)比 "1 买 3 卖、2 买 4 卖"(利润 2+2=4)更优?
→ 错!"1 买 4 卖" 的实际操作中,3→2 下跌时持有股票,相当于 "赚了 1→3 的 2,亏了 3→2 的 1 ,赚了 2→4 的 2",总利润 2-1+2=3,反而比相邻贪心的 4 少 ------ 这就是 "跳天操作" 的问题:
中间下跌会吃掉部分利润,而相邻贪心会在下跌前卖出,避免亏损。
具体步骤
- 初始化 :定义
maxProfit = 0,用于累加总利润; - 遍历数组 :从第2天(索引1)开始,逐个对比当天与前一天的股价:
- 若
prices[i] > prices[i-1]:将prices[i] - prices[i-1]加到maxProfit; - 若
prices[i] ≤ prices[i-1]:不操作,利润保持不变;
- 若
- 返回结果 :遍历结束后,
maxProfit即为最大利润(全程下跌时利润为0,符合题目要求)。
举例
示例1:prices = [7,1,5,3,6,4]
- 1→5:差价4 →
maxProfit=4; - 3→6:差价3 →
maxProfit=7; - 其余天数下跌/持平,无操作;最终利润7,与示例一致。
示例2:prices = [1,2,3,4,5]
- 每天差价1,累加得4 → 与"1买5卖"利润一致。
示例3:prices = [7,6,4,3,1]
- 所有天数股价下跌,利润保持0。
时间复杂度:O (n),仅遍历一次价格数组
空间复杂度:O (1),仅使用 maxProfit 一个变量
代码实现(Go):
go
package main
import "fmt"
func maxProfit(prices []int) int {
maxProfit := 0
// 从第2天(索引1)开始遍历,对比当天与前一天的股价
// 索引0是第1天,无前置价格,无需处理
for i := 1; i < len(prices); i++ {
if prices[i] > prices[i-1] { // 若当天价格 > 前一天价格,赚取该段差价
maxProfit += prices[i] - prices[i-1]
} // 价格下跌/持平时不操作,利润保持不变,无需额外处理
}
return maxProfit
}
func main() {
var n int
fmt.Scan(&n)
prices := make([]int, n)
for i := 0; i < n; i++ {
fmt.Scan(&prices[i])
}
res := maxProfit(prices)
fmt.Println(res)
}
进阶思路:动态规划
1. 状态定义
dp[i][0]:第i天不持有股票的最大利润;dp[i][1]:第i天持有股票的最大利润。
2. 状态转移
- 不持有股票:要么前一天也不持有,要么前一天持有、今天卖出
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[i][0]:卖出操作会让利润增加(+prices[i]),取「不操作」和「卖出」中利润更大的情况;
dp[i][1]:买入操作会让利润减少(-prices[i]),取「不操作」和「买入」中利润更大的情况;
3. 初始状态
dp[0][0] = 0(第0天不持有,利润0);dp[0][1] = -prices[0](第0天买入,利润为负)。
4. 最终结果
dp[n-1][0](最后一天不持有股票的利润,必然高于持有)。
时间复杂度:O (n),仅遍历一次价格数组
空间复杂度:O (n),创建了长度为 n、每个元素为长度 2 的数组 dp
代码实现(Go):
go
package main
import "fmt"
func maxProfit(prices []int) int {
n := len(prices)
// 边界处理:如果价格数组为空,直接返回0
if n == 0 {
return 0
}
// 定义dp数组:dp[i][0]表示第i天不持有股票的最大利润,dp[i][1]表示持有
dp := make([][2]int, n)
// 初始状态:第0天(第一天)的状态
dp[0][0] = 0 // 第0天不持有股票,利润为0
dp[0][1] = -prices[0] // 第0天买入股票,利润为 -股价(成本)
// 遍历从第1天开始(索引1),逐天更新状态
for i := 1; i < n; i++ {
// 状态转移1:第i天不持有股票的两种情况
// 情况1:前一天也不持有,利润不变;情况2:前一天持有,今天卖出,利润=前一天持有利润+今天股价
dp[i][0] = max(dp[i-1][0], dp[i-1][1]+prices[i])
// 状态转移2:第i天持有股票的两种情况
// 情况1:前一天也持有,利润不变;情况2:前一天不持有,今天买入,利润=前一天不持有利润-今天股价
dp[i][1] = max(dp[i-1][1], dp[i-1][0]-prices[i])
}
// 最终结果:最后一天不持有股票的利润(持有股票的话未卖出,利润必然更低)
return dp[n-1][0]
}
func max(a, b int) int {
if a > b {
return a
}
return b
}
func main() {
var n int
fmt.Scan(&n)
prices := make([]int, n)
for i := 0; i < n; i++ {
fmt.Scan(&prices[i])
}
res := maxProfit(prices)
fmt.Println(res)
}
空间优化(滚动数组,O (1) 空间)
上述代码用了 O (n) 空间,实际可优化为 O (1)(因为每天的状态仅依赖前一天),适合大数据量场景:
代码实现(Go):
go
package main
import "fmt"
func maxProfit(prices []int) int {
n := len(prices)
// 边界处理:如果价格数组为空,直接返回0
if n == 0 {
return 0
}
// 用两个变量代替dp数组,滚动更新
hold := -prices[0] // 持有股票的最大利润(初始为第0天买入)
notHold := 0 // 不持有股票的最大利润(初始为第0天不操作)
for i := 1; i < n; i++ {
// 先保存更新前的状态,避免覆盖
preHold := hold
preNotHold := notHold
// 更新不持有状态:前一天不持有 或 前一天持有今天卖出
notHold = max(preNotHold, preHold+prices[i])
// 更新持有状态:前一天持有 或 前一天不持有今天买入
hold = max(preHold, preNotHold-prices[i])
}
return notHold
}
func main() {
var n int
fmt.Scan(&n)
prices := make([]int, n)
for i := 0; i < n; i++ {
fmt.Scan(&prices[i])
}
res := maxProfit(prices)
fmt.Println(res)
}