☰
【Day 27】121.买卖股票的最佳时机 122.买卖股票的最佳时机II
2026/10/14 2:32:22 网站建设 项目流程

文章目录

  • 121.买卖股票的最佳时机
  • 题目:
  • 思路:
  • 举例
  • 代码实现(Go):
  • 122.买卖股票的最佳时机II
  • 题目:
  • 思路:
  • 举例
  • 代码实现(Go):
  • 进阶思路:动态规划
  • 代码实现(Go):
  • 空间优化(滚动数组,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


思路:

想要一次交易的利润最大,本质就是在「遍历到当前天为止的最低点」买入,在该最低点之后的任意一天卖出(取卖出的最大利润);
遍历过程中只需维护两个关键变量,不用额外空间,边遍历边更新,每个价格只看一次:

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

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

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

举例

初始化


场景1:正常上涨有盈利
输入价格数组:[7,1,5,3,6,4](共6天)

步骤

  1. 初始化:minPrice=7,maxProfit=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=7,maxProfit=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=7,maxProfit=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]结果一致,等于价格无任何影响,只需要比较小于即可。


代码实现(Go):

packagemainimport"fmt"funcmaxProfit(prices[]int)int{minPrice:=prices[0]// 初始化为首个元素maxProfit:=0// 无利润返回0,利润为负也保持0// 从第二个元素开始遍历(索引1),首个元素已作为初始minPricefori:=1;i<len(prices);i++{curPrice:=prices[i]// 第一步:更新到目前为止的最小买入价,仅当前价格更小时更新ifcurPrice<minPrice{minPrice=curPrice}// 第二步:计算当天卖出的利润,更新最大利润curProfit:=curPrice-minPriceifcurProfit>maxProfit{maxProfit=curProfit}}returnmaxProfit}funcmain(){// 直接构造题目给的输入数组 [7,1,5,3,6,4]prices:=[]int{7,1,5,3,6,4}res:=maxProfit(prices)fmt.Println(res)}// 或funcmain(){varnintfmt.Scan(&n)// 读取价格数组的长度prices:=make([]int,n)fori:=0;i<n;i++{fmt.Scan(&prices[i])// 循环读取n个价格到数组}// 计算并打印最大利润res:=maxProfit(prices)fmt.Println(res)}// 或funcmain(){varprices[]intvarnumint// 循环不断读取int,只要成功读到1个整数就继续,读到非数字/输入结束则退出循环forfmt.Scan(&num)==1{prices=append(prices,num)}res:=maxProfit(prices)fmt.Println(res)}/* ===== 测试用例1 ===== 【输入】:7 1 5 3 6 4 【预期输出】:5 解释:买入价格1,卖出价格6,最大利润 5

时间复杂度:O (n),仅遍历一次价格数组
空间复杂度:O (1),仅使用常数个变量

packagemainimport"fmt"// maxProfit 返回:最大利润,[买入价,卖出价]funcmaxProfit(prices[]int)(int,[2]int){n:=len(prices)ifn<2{return0,[2]int{0,0}}minPrice:=prices[0]// 历史最低价格buyPrice:=0sellPrice:=0maxProfitVal:=0fori:=1;i<n;i++{// 更新历史最低点(可以在这里买)ifprices[i]<minPrice{minPrice=prices[i]}else{// 在今天卖出,算利润profit:=prices[i]-minPriceifprofit>maxProfitVal{maxProfitVal=profit buyPrice=minPrice sellPrice=prices[i]}}}ifmaxProfitVal<=0{return0,[2]int{0,0}}returnmaxProfitVal,[2]int{buyPrice,sellPrice}}funcmain(){prices:=[]int{7,2,5,9,1,3,6,4}profit,pair:=maxProfit(prices)fmt.Printf("%d, %v\n",profit,pair)// 输出:7, [2 9] 和样例完全一致}

读取单个数字 / 字符串:fmt.Scan(&变量名)

varnint// 定义一个整数变量nfmt.Scan(&n)// 终端输入一个数字,比如6,n就等于6varsstringfmt.Scan(&s)// 终端输入一个字符串,s就等于该字符串

循环读取多个值:for循环 + fmt.Scan(&数组元素)

// 步骤1:定义变量接收数组长度,读取长度n 【必须加&】varnintfmt.Scan(&n)// 步骤2:初始化一个长度为n的切片/数组 【固定写法】nums:=make([]int,n)// 切片,算法面试首选,比数组灵活// 步骤3:循环n次,读取n个数字,存入切片 【必须加&nums[i]】fori:=0;i<n;i++{fmt.Scan(&nums[i])// 每次读取一个数字,赋值给nums[i]}

读取两个数字

vara,bintfmt.Scan(&a,&b)// 终端输入3 5,a=3,b=5fmt.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]:

这意味着:“每天买卖赚小差价”只是数学上的等价拆解,并非真的要高频操作,实际可简化为“低点买、高点卖”,利润完全一致。

(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 少 —— 这就是 “跳天操作” 的问题:中间下跌会吃掉部分利润,而相邻贪心会在下跌前卖出,避免亏损。

具体步骤

  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]

示例2:prices = [1,2,3,4,5]

示例3:prices = [7,6,4,3,1]


代码实现(Go):

packagemainimport"fmt"funcmaxProfit(prices[]int)int{maxProfit:=0// 从第2天(索引1)开始遍历,对比当天与前一天的股价// 索引0是第1天,无前置价格,无需处理fori:=1;i<len(prices);i++{ifprices[i]>prices[i-1]{// 若当天价格 > 前一天价格,赚取该段差价maxProfit+=prices[i]-prices[i-1]}// 价格下跌/持平时不操作,利润保持不变,无需额外处理}returnmaxProfit}funcmain(){varnintfmt.Scan(&n)prices:=make([]int,n)fori:=0;i<n;i++{fmt.Scan(&prices[i])}res:=maxProfit(prices)fmt.Println(res)}

时间复杂度:O (n),仅遍历一次价格数组
空间复杂度:O (1),仅使用 maxProfit 一个变量


进阶思路:动态规划

1. 状态定义

2. 状态转移

dp[i][0]:卖出操作会让利润增加(+prices[i]),取「不操作」和「卖出」中利润更大的情况;
dp[i][1]:买入操作会让利润减少(-prices[i]),取「不操作」和「买入」中利润更大的情况;

3. 初始状态

4. 最终结果
dp[n-1][0](最后一天不持有股票的利润,必然高于持有)。


代码实现(Go):

packagemainimport"fmt"funcmaxProfit(prices[]int)int{n:=len(prices)// 边界处理:如果价格数组为空,直接返回0ifn==0{return0}// 定义dp数组:dp[i][0]表示第i天不持有股票的最大利润,dp[i][1]表示持有dp:=make([][2]int,n)// 初始状态:第0天(第一天)的状态dp[0][0]=0// 第0天不持有股票,利润为0dp[0][1]=-prices[0]// 第0天买入股票,利润为 -股价(成本)// 遍历从第1天开始(索引1),逐天更新状态fori:=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])}// 最终结果:最后一天不持有股票的利润(持有股票的话未卖出,利润必然更低)returndp[n-1][0]}funcmax(a,bint)int{ifa>b{returna}returnb}funcmain(){varnintfmt.Scan(&n)prices:=make([]int,n)fori:=0;i<n;i++{fmt.Scan(&prices[i])}res:=maxProfit(prices)fmt.Println(res)}

时间复杂度:O (n),仅遍历一次价格数组
空间复杂度:O (n),创建了长度为 n、每个元素为长度 2 的数组 dp

空间优化(滚动数组,O (1) 空间)

上述代码用了 O (n) 空间,实际可优化为 O (1)(因为每天的状态仅依赖前一天),适合大数据量场景:

代码实现(Go):

packagemainimport"fmt"funcmaxProfit(prices[]int)int{n:=len(prices)// 边界处理:如果价格数组为空,直接返回0ifn==0{return0}// 用两个变量代替dp数组,滚动更新hold:=-prices[0]// 持有股票的最大利润(初始为第0天买入)notHold:=0// 不持有股票的最大利润(初始为第0天不操作)fori:=1;i<n;i++{// 先保存更新前的状态,避免覆盖preHold:=hold preNotHold:=notHold// 更新不持有状态:前一天不持有 或 前一天持有今天卖出notHold=max(preNotHold,preHold+prices[i])// 更新持有状态:前一天持有 或 前一天不持有今天买入hold=max(preHold,preNotHold-prices[i])}returnnotHold}funcmain(){varnintfmt.Scan(&n)prices:=make([]int,n)fori:=0;i<n;i++{fmt.Scan(&prices[i])}res:=maxProfit(prices)fmt.Println(res)}

时间复杂度:O (n),仅遍历一次价格数组
空间复杂度:O (1)


需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询