Files
leetcode-go/贪心算法/77-买卖股票的最佳时机.md
T

5.0 KiB
Raw Blame History

tags, create time
tags create time
贪心算法
数组
动态规划
简单
LeetCode
2026-05-16 10:00

77 - 买卖股票的最佳时机

[!question] 一句话描述 给定每天的价格,只能完成一笔交易(买一次 + 卖一次),求最大利润。

题面

要素 描述
输入 prices int 数组,第 i 个元素表示第 i 天的价格
约束 先买入、后卖出,不能在同一天;若无利润返回 0
输出 最大利润(int)
数据范围 1 <= prices.length <= 10^5,0 <= prices[i] <= 10^4

示例 1

输入: [7,1,5,3,6,4]
输出: 5
解释: 第 2 天以 1 买入,第 5 天以 6 卖出 → 6 - 1 = 5

示例 2

输入: [7,6,4,3,1]
输出: 0
解释: 价格持续下跌,无法获利

思路

[!tip] 核心洞察:不要「预测」最高点,而是「跟踪」最低点

暴力做法是枚举每一对 (买入日, 卖出日),复杂度 O(n²),在 n = 10^5 时必然超时。我们需要一个 遍历一次就能得出答案 的方法。

关键问题:对于每个「今天」,最优的买入价是什么?

答案是:从第 0 天到今天为止的最小价格。

换句话说,我们不用提前知道未来的最低价——随着遍历推进,维护两个变量就够了:

  • minPrice —— 到目前为止见过的最低价格
  • maxProfit —— 用这个最低价格买入、在今天卖出的最大利润

每一步做一次比较:如果 prices[i] - minPrice > maxProfit,就更新利润;否则继续前进。

整个过程可以理解为一条从左到右的扫描线:

flowchart LR
    A["📍 扫描线"] -->|"遍历每一天"| B["看到当前价格"]
    B --> C{"比历史最低价还低?"}
    C -->|"是"| D["更新 minPrice"]
    C -->|"否"| E["检查利润"]
    D --> E
    E --> F{"当前利润更高?"}
    F -->|"是"| G["更新 maxProfit"]
    F -->|"否"| H["继续往后走"]
    G --> H
    H -->|"还没到最后"| A
    H -->|"到达末尾"| I["返回 maxProfit"]

为什么这是正确的? 因为最优解一定形如「在某一天以最低买入价购入,在之后以最高价卖出」。当我们的扫描线走到真正的"卖出日"时,minPrice 已经记录了它之前的最低买入价,此时计算出的利润恰好就是答案。

一步步执行演示

以 [7,1,5,3,6,4] 为例,追踪两个变量的变化:

flowchart LR
    subgraph Step0 ["初始状态"]
        S0["minPrice = ∞\nmaxProfit = 0"]
    end
    subgraph Step1 ["Day 0: price = 7"]
        S1["minPrice = 7\nmaxProfit = 0"]
    end
    subgraph Step2 ["Day 1: price = 1"]
        S2["minPrice = 1 ← 更新低价\nmaxProfit = 0"]
    end
    subgraph Step3 ["Day 2: price = 5"]
        S3["minPrice = 1\nmaxProfit = 4 ← 更新利润"]
    end
    subgraph Step4 ["Day 3: price = 3"]
        S4["minPrice = 1\nmaxProfit = 4"]
    end
    subgraph Step5 ["Day 4: price = 6"]
        S5["minPrice = 1\nmaxProfit = 5 ← 更新利润 ← 最终答案"]
    end
    subgraph Step6 ["Day 5: price = 4"]
        S6["minPrice = 1\nmaxProfit = 5"]
    end
    S0 --> S1 --> S2 --> S3 --> S4 --> S5 --> S6

代码提示

// 初始化
minPrice = 最大值(或第一天的价格)
maxProfit = 0

// 对每一天 i 从 1 开始遍历
if prices[i] < minPrice:
    minPrice = prices[i]
else if prices[i] - minPrice > maxProfit:
    maxProfit = prices[i] - minPrice

return maxProfit

[!danger] 常见陷阱:空数组 题目保证 prices.length >= 1,但实战中应先判断边界条件再访问 prices[0]。

技巧

技巧 说明
「今天卖」思维 把问题拆成 N 个子问题:假设必须在第 i 天卖出,最佳利润是多少?最后取 max 即可
只卖不买 实际上只需要维护「卖出」的视角——每次考虑「今天卖 vs 之前卖」的最大值,买入隐含在 minPrice 中
DP 视角 这道题也可以写成最简动态规划(状态转移只有两行),后续「买卖股票 II/III」会在此基础上扩展

代码

package main

import "fmt"

// maxProfit 计算只能进行一次交易时的最大利润
// 使用一次扫描法:遍历时记录历史最低价和最大利润
func maxProfit(prices []int) int {
	// 初始化:第 0 天作为参考基准
	minPrice := prices[0]
	maxProfit := 0

	// 从第 1 天起,逐天判断
	for i := 1; i < len(prices); i++ {
		if prices[i] < minPrice {
			// 发现了更低的价位:刷新买入参考价
			minPrice = prices[i]
		} else if prices[i]-minPrice > maxProfit {
			// 今天的售价能带来更高的利润:记录之
			maxProfit = prices[i] - minPrice
		}
	}

	return maxProfit
}

func main() {
	// 示例 1
	fmt.Println(maxProfit([]int{7, 1, 5, 3, 6, 4})) // 5
	// 示例 2
	fmt.Println(maxProfit([]int{7, 6, 4, 3, 1})) // 0
}

复杂度分析

指标 结果
时间 O(n) — 仅一次线性扫描
空间 O(1) — 仅两个整型变量