【Day 27】121.买卖股票的最佳时机 122.买卖股票的最佳时机II

文章目录

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


思路:

想要一次交易的利润最大,本质就是在「遍历到当前天为止的最低点」买入,在该最低点之后的任意一天卖出 (取卖出的最大利润);

遍历过程中只需维护两个关键变量,不用额外空间,边遍历边更新,每个价格只看一次:

  1. minPrice:遍历到当前天时,之前所有天的股票最低价格(也就是到目前为止最划算的买入价);
  2. maxProfit:遍历到当前天时,能获取的最大利润(初始为0,无利润时直接返回这个值)。

遍历的核心两步(对每个价格都执行)

  1. 先更新最小买入价 :如果当前价格比minPrice更低,就把minPrice换成当前价格(找到更划算的买入点);
  2. 再计算当前利润,更新最大利润 :用当前价格 - minPrice得到「当天卖出的利润」,如果这个利润比maxProfit大,就更新maxProfit
    关键 :如果当前价格等于 minPrice,第一步不会更新(等于不是更低),第二步计算的利润为0,不会超过maxProfit(初始0,后续只增不减),所以无需单独处理等于的情况,自然跳过即可。

举例

初始化

  • minPrice = 数组第一个元素(题目保证数组非空,无越界风险);
  • maxProfit = 0(无利润时直接返回,利润为负也取0)。

场景1:正常上涨有盈利

输入价格数组:[7,1,5,3,6,4](共6天)

步骤

  1. 初始化:minPrice=7maxProfit=0
  2. 第2天(价格1):1 < 7 → minPrice=1;利润1-1=0 → 不大于0,maxProfit=0
  3. 第3天(价格5):5 不小于1 → minPrice不变;利润5-1=4 → 大于0 → maxProfit=4
  4. 第4天(价格3):3 不小于1 → minPrice不变;利润3-1=2 → 小于4,maxProfit不变;
  5. 第5天(价格6):6 不小于1 → minPrice不变;利润6-1=5 → 大于4 → maxProfit=5
  6. 第6天(价格4):4 不小于1 → minPrice不变;利润4-1=3 → 小于5,maxProfit不变。

结果
maxProfit=5,即第2天买入、第5天卖出,利润最大。


场景2:持续下跌无盈利

输入价格数组:[7,6,4,3,1](共5天)

步骤

  1. 初始化:minPrice=7maxProfit=0
  2. 第2天(价格6):6 < 7 → minPrice=6;利润6-6=0 → 不大于0,maxProfit=0
  3. 第3天(价格4):4 < 6 → minPrice=4;利润4-4=0 → 不大于0,maxProfit=0
  4. 第4天(价格3):3 < 4 → minPrice=3;利润3-3=0 → 不大于0,maxProfit=0
  5. 第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,含等于)

步骤

  1. 初始化:minPrice=7maxProfit=0
  2. 第2天(价格1):1 < 7 → minPrice=1;利润1-1=0 → 不大于0,maxProfit=0
  3. 第3天(价格1):1 不小于1 → minPrice不变;利润1-1=0 → 不大于0,maxProfit不变;
  4. 第4天(价格5):5 不小于1 → minPrice不变;利润5-1=4 → 大于0 → maxProfit=4
  5. 第5天(价格3):3 不小于1 → minPrice不变;利润3-1=2 → 小于4,maxProfit不变;
  6. 第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→53→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 少 ------ 这就是 "跳天操作" 的问题:中间下跌会吃掉部分利润,而相邻贪心会在下跌前卖出,避免亏损。

具体步骤

  1. 初始化 :定义 maxProfit = 0,用于累加总利润;
  2. 遍历数组 :从第2天(索引1)开始,逐个对比当天与前一天的股价:
    • prices[i] > prices[i-1]:将 prices[i] - prices[i-1] 加到 maxProfit
    • prices[i] ≤ prices[i-1]:不操作,利润保持不变;
  3. 返回结果 :遍历结束后,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)
}

相关推荐
牛奔5 小时前
如何理解 Go 的调度模型,以及 G / M / P 各自的职责
开发语言·后端·golang
铉铉这波能秀5 小时前
LeetCode Hot100数据结构背景知识之元组(Tuple)Python2026新版
数据结构·python·算法·leetcode·元组·tuple
铉铉这波能秀6 小时前
LeetCode Hot100数据结构背景知识之字典(Dictionary)Python2026新版
数据结构·python·算法·leetcode·字典·dictionary
我是咸鱼不闲呀6 小时前
力扣Hot100系列20(Java)——[动态规划]总结(下)( 单词拆分,最大递增子序列,乘积最大子数组 ,分割等和子集,最长有效括号)
java·leetcode·动态规划
唐梓航-求职中6 小时前
编程-技术-算法-leetcode-288. 单词的唯一缩写
算法·leetcode·c#
Ll13045252986 小时前
Leetcode二叉树part4
算法·leetcode·职场和发展
牛奔7 小时前
Go 是如何做抢占式调度的?
开发语言·后端·golang
@––––––7 小时前
力扣hot100—系列4-贪心算法
算法·leetcode·贪心算法
im_AMBER8 小时前
Leetcode 115 分割链表 | 随机链表的复制
数据结构·学习·算法·leetcode