📖 第65课:买卖股票的最佳时机

4 阅读20分钟

想系统提升编程能力、查看更完整的学习路线,欢迎访问 AI Compass:github.com/tingaicompa… 仓库持续更新刷题题解、Python 基础和 AI 实战内容,适合想高效进阶的你。

📖 第65课:买卖股票的最佳时机

模块:贪心算法 | 难度:Easy ⭐⭐⭐ LeetCode 链接:leetcode.cn/problems/be… 前置知识:数组遍历、变量更新 预计学习时间:15分钟


🎯 题目描述

给定一个数组 prices,其中 prices[i] 表示股票在第 i 天的价格。你只能选择某一天买入这只股票,并在未来的某一天卖出。计算你所能获取的最大利润。如果不能获得任何利润,返回 0。

注意:你只能完成一笔交易(即买入和卖出各一次)。

示例:

输入:prices = [7,1,5,3,6,4]
输出:5
解释:在第 2 天(价格 = 1)买入,在第 5 天(价格 = 6)卖出,最大利润 = 6 - 1 = 5

约束条件:

  • 1 <= prices.length <= 10^5
  • 0 <= prices[i] <= 10^4
  • 必须先买入后卖出(不能在买入前卖出)

🧪 边界用例(面试必考)

用例类型输入期望输出考察点
最小输入prices=[1]0只有一天无法交易
单调递减prices=[7,6,4,3,1]0无利可图
单调递增prices=[1,2,3,4,5]4首尾最优
大规模n=10^5-必须O(n)算法
相同价格prices=[5,5,5,5]0无差价

💡 思路引导

生活化比喻

想象你是一个商人,拿到了未来一周的水果价格表。你只能带一车水果,决定什么时候买入、什么时候卖出来赚取最大利润。

🐌 笨办法:穷举所有可能的买入-卖出组合,对比每一天买入、后续每一天卖出的利润。这就像你把价格表上所有可能的配对都算了一遍,复杂度 O(n²),当价格表有 10 万天时,就要计算 100 亿次!

🚀 聪明办法:边看价格表边思考——"如果今天卖出,我应该在之前哪天买入最划算?"答案显而易见:在今天之前的最低价买入!所以只需要一遍扫描,一边维护"历史最低价",一边计算"今天卖出的最大利润"。

关键洞察

核心思想:贪心地维护历史最低价,实时更新最大利润

这是贪心算法的入门经典题——每一步都做局部最优决策(维护最小值),最终得到全局最优解(最大利润)。


🧠 解题思维链

这一节模拟你在面试中"从零开始思考"的过程。

Step 1:理解题目 → 锁定输入输出

  • 输入:整数数组 prices,长度 1 到 10^5
  • 输出:一个整数,表示最大利润(可以为 0)
  • 限制:必须先买入后卖出,只能交易一次

Step 2:先想笨办法(暴力法)

最直接的想法:枚举所有可能的 (买入日, 卖出日) 组合,计算每个组合的利润,取最大值。

  • 时间复杂度:O(n²) — 双层循环
  • 瓶颈在哪:对于每个卖出日,都要向前扫描所有可能的买入日,重复计算了很多次"找最小值"的操作

Step 3:瓶颈分析 → 优化方向

分析暴力法中"重复计算"的环节:

  • 核心问题:每次考虑"今天卖出"时,都要重新扫描之前所有天找最低价
  • 优化思路:能不能一边遍历一边维护"截止到今天的历史最低价"?这样就不需要每次回头查找了

Step 4:选择武器

  • 选用:贪心算法 + 一次遍历
  • 理由:每次只需要知道"历史最低价"即可判断当前的最大利润,不需要回溯,符合贪心的无后效性

🔑 模式识别提示:当题目要求"一次遍历中维护某个历史最值",优先考虑"贪心 + 单变量维护"模式


🔑 解法一:暴力双循环(直觉法)

思路

枚举所有 i < j 的组合,计算 prices[j] - prices[i],取最大值。

图解过程

prices = [7, 1, 5, 3, 6, 4]

暴力法枚举所有组合:
买入日 i=0 (价格7): 卖出日j=1(1)利润-6, j=2(5)利润-2, j=3(3)利润-4, j=4(6)利润-1, j=5(4)利润-3
买入日 i=1 (价格1): 卖出日j=2(5)利润4, j=3(3)利润2, j=4(6)利润5 ← 最大, j=5(4)利润3
买入日 i=2 (价格5): 卖出日j=3(3)利润-2, j=4(6)利润1, j=5(4)利润-1
买入日 i=3 (价格3): 卖出日j=4(6)利润3, j=5(4)利润1
买入日 i=4 (价格6): 卖出日j=5(4)利润-2

最大利润 = 5

Python代码

from typing import List


def maxProfit_brute(prices: List[int]) -> int:
    """
    解法一:暴力双循环
    思路:枚举所有买入-卖出组合,找最大利润
    """
    max_profit = 0
    n = len(prices)

    # 外层循环枚举买入日
    for i in range(n):
        # 内层循环枚举卖出日(必须在买入日之后)
        for j in range(i + 1, n):
            profit = prices[j] - prices[i]
            max_profit = max(max_profit, profit)

    return max_profit


# ✅ 测试
print(maxProfit_brute([7,1,5,3,6,4]))  # 期望输出:5
print(maxProfit_brute([7,6,4,3,1]))    # 期望输出:0
print(maxProfit_brute([1,2]))          # 期望输出:1

复杂度分析

  • 时间复杂度:O(n²) — 双层循环,每个元素都要和后面所有元素配对
    • 具体地说:如果输入规模 n=1000,大约需要 1000×999/2 ≈ 50万次操作
  • 空间复杂度:O(1) — 只用了几个变量

优缺点

  • ✅ 思路直观,容易理解
  • ✅ 代码简洁
  • ❌ 时间复杂度O(n²)无法通过大数据量测试(n=10^5会超时)
  • ❌ 存在大量重复计算

🏆 解法二:一次遍历维护最小值(贪心算法 - 最优解)

优化思路

从解法一的痛点出发:每次考虑"今天卖出"时,都要重新找之前的最低价。能否一边遍历一边维护历史最低价呢?

💡 关键想法:贪心策略——对于每一天,只需要知道"之前的最低价"即可计算"今天卖出的最大利润"。维护两个变量:min_price(历史最低价)和 max_profit(最大利润),一次遍历即可。

为什么这是贪心算法?

  • 局部最优:每次都选择"历史最低价"买入
  • 全局最优:由于每天都考虑了最优买入价,最终得到的就是全局最大利润
  • 无后效性:当前的决策(今天卖出的利润)只依赖历史最低价,不影响未来

图解过程

prices = [7, 1, 5, 3, 6, 4]
初始化: min_price = 7, max_profit = 0

第1天(价格7):
  min_price = 7 (历史最低)
  今天卖出利润 = 7 - 7 = 0
  max_profit = max(0, 0) = 0
  状态: min_price=7, max_profit=0

第2天(价格1):
  min_price = min(7, 1) = 1 ← 更新历史最低价
  今天卖出利润 = 1 - 1 = 0
  max_profit = max(0, 0) = 0
  状态: min_price=1, max_profit=0

第3天(价格5):
  min_price = min(1, 5) = 1 (保持)
  今天卖出利润 = 5 - 1 = 4 ← 如果在第2天买入,今天卖出
  max_profit = max(0, 4) = 4
  状态: min_price=1, max_profit=4

第4天(价格3):
  min_price = min(1, 3) = 1 (保持)
  今天卖出利润 = 3 - 1 = 2
  max_profit = max(4, 2) = 4 (保持)
  状态: min_price=1, max_profit=4

第5天(价格6):
  min_price = min(1, 6) = 1 (保持)
  今天卖出利润 = 6 - 1 = 5 ← 最优方案:第2天买,第5天卖
  max_profit = max(4, 5) = 5 ← 更新最大利润
  状态: min_price=1, max_profit=5

第6天(价格4):
  min_price = min(1, 4) = 1 (保持)
  今天卖出利润 = 4 - 1 = 3
  max_profit = max(5, 3) = 5 (保持)
  最终答案: 5

可视化时间线:
  第1天    第2天    第3天    第4天    第5天    第6天
   7        1        5        3        6        4
   |        |        |                 |
   |        |________|_________________|  利润 = 6-1 = 5
   |        ↑                          ↑
   |   历史最低价                   最佳卖出点
   |
   初始价格(不是最优买入点)

再用边界案例演示一次(单调递减):

prices = [7, 6, 4, 3, 1]

第1天(7): min_price=7, max_profit=0
第2天(6): min_price=6, 今天卖利润=6-6=0, max_profit=0
第3天(4): min_price=4, 今天卖利润=4-4=0, max_profit=0
第4天(3): min_price=3, 今天卖利润=3-3=0, max_profit=0
第5天(1): min_price=1, 今天卖利润=1-1=0, max_profit=0

最终答案: 0 (无利可图)

Python代码

from typing import List


def maxProfit(prices: List[int]) -> int:
    """
    解法二:一次遍历维护最小值(贪心算法)
    思路:维护历史最低价,实时计算今天卖出的最大利润
    """
    if not prices:
        return 0

    # 初始化历史最低价为第一天的价格
    min_price = prices[0]
    # 初始化最大利润为0
    max_profit = 0

    # 从第二天开始遍历
    for price in prices[1:]:
        # 计算今天卖出的利润(前提是在历史最低价买入)
        profit = price - min_price
        # 更新最大利润
        max_profit = max(max_profit, profit)
        # 更新历史最低价
        min_price = min(min_price, price)

    return max_profit


# ✅ 测试
print(maxProfit([7,1,5,3,6,4]))  # 期望输出:5
print(maxProfit([7,6,4,3,1]))    # 期望输出:0
print(maxProfit([1,2,3,4,5]))    # 期望输出:4
print(maxProfit([2,4,1]))        # 期望输出:2

复杂度分析

  • 时间复杂度:O(n) — 只遍历一次数组,每个元素访问一次
    • 具体地说:如果输入规模 n=100,000,只需要 100,000 次操作,比暴力法快了 50,000 倍!
  • 空间复杂度:O(1) — 只用了 min_price 和 max_profit 两个变量

为什么O(n)是最优的?

  • 理论下界:至少要遍历一次数组才能知道所有价格,所以 O(n) 是理论最优
  • 这个解法已经达到了理论下界,无法再优化

⚡ 解法三:动态规划思想(可选理解)

优化思路

虽然这道题用贪心算法最简洁,但也可以从动态规划角度理解:定义状态 dp[i] 表示"第 i 天卖出能获得的最大利润"。

💡 状态转移:dp[i] = max(0, prices[i] - min(prices[0:i]))

实际上这和贪心算法是等价的,只是换了一种表述方式。

Python代码

from typing import List


def maxProfit_dp(prices: List[int]) -> int:
    """
    解法三:动态规划思想
    思路:dp[i] 表示第 i 天卖出的最大利润
    """
    if not prices:
        return 0

    n = len(prices)
    # dp[i] 表示第 i 天卖出能获得的最大利润
    dp = [0] * n
    min_price = prices[0]

    for i in range(1, n):
        # 第 i 天卖出的利润 = 今天价格 - 历史最低价
        dp[i] = max(0, prices[i] - min_price)
        # 更新历史最低价
        min_price = min(min_price, prices[i])

    # 返回所有天中的最大利润
    return max(dp)


# ✅ 测试
print(maxProfit_dp([7,1,5,3,6,4]))  # 期望输出:5
print(maxProfit_dp([7,6,4,3,1]))    # 期望输出:0

复杂度分析

  • 时间复杂度:O(n) — 一次遍历 + 一次 max()
  • 空间复杂度:O(n) — 需要 dp 数组

优缺点

  • ✅ 从 DP 角度理解问题,为股票系列进阶题打基础
  • ❌ 空间复杂度比贪心法高,但可以优化为 O(1)

🐍 Pythonic 写法

利用 Python 的语法糖,可以写得更简洁:

def maxProfit_pythonic(prices: List[int]) -> int:
    """Pythonic 一行流写法"""
    min_price = float('inf')
    max_profit = 0
    for price in prices:
        min_price = min(min_price, price)
        max_profit = max(max_profit, price - min_price)
    return max_profit

更极致的函数式写法(不推荐面试用):

from itertools import accumulate

def maxProfit_functional(prices: List[int]) -> int:
    """函数式编程风格(仅供学习)"""
    if not prices:
        return 0
    # accumulate 实现滚动最小值
    min_prices = accumulate(prices, min)
    profits = [p - mp for p, mp in zip(prices, min_prices)]
    return max(profits)

⚠️ 面试建议:优先写清晰版本(解法二)展示思路,再提 Pythonic 写法展示语言功底。面试官更看重你的思考过程和贪心思想的理解,而非代码行数。


📊 解法对比

维度解法一:暴力双循环🏆 解法二:贪心维护最小值(最优)解法三:动态规划
时间复杂度O(n²)O(n) ← 时间最优O(n)
空间复杂度O(1)O(1) ← 空间最优O(n) 可优化为 O(1)
代码难度简单简单中等
面试推荐⭐⭐⭐ ← 首选⭐⭐
适用场景只适合小数据面试首选,通用性强为股票系列进阶题铺垫

为什么解法二是最优解?

  1. 时间复杂度O(n)已经是理论最优:必须至少看一遍所有价格,无法更快
  2. 空间复杂度O(1)已经是最优:只用了两个变量,无法更省
  3. 代码简洁易懂:逻辑清晰,不易出错
  4. 贪心思想典型:完美体现"局部最优→全局最优"的贪心精髓

面试建议:

  1. 先用30秒口述暴力法思路(O(n²)),表明你能想到基本解法
  2. 立即优化到🏆最优解(O(n)贪心法),展示优化能力
  3. 重点讲解贪心策略:"一边遍历一边维护历史最低价,实时计算最大利润"
  4. 强调为什么这是最优:时间空间都已达理论下限,无法再优化
  5. 手动测试边界用例(单调递减、单调递增),展示对解法的深入理解

🎤 面试现场

模拟面试中的完整对话流程,帮你练习"边想边说"。

面试官:请你解决一下这道股票买卖的问题。

:(审题30秒)好的,这道题要求在数组中找一对 (买入日, 卖出日),使得利润最大,且买入必须在卖出之前。让我先想一下...

我的第一个想法是暴力枚举所有 i < j 的组合,计算 prices[j] - prices[i],时间复杂度是 O(n²)。但这显然不够优,因为存在大量重复计算——每次考虑"今天卖出"时,都要重新找之前的最低价。

更好的方法是用贪心算法:一边遍历一边维护"历史最低价",实时计算"今天卖出的最大利润"。这样只需要一次遍历,时间复杂度优化到 O(n),空间复杂度 O(1)。

面试官:很好,为什么这个贪心策略是正确的?

:因为对于任意一天,如果我们决定在这天卖出,那么最优的买入日一定是"这天之前的最低价那天"。所以维护历史最低价就是局部最优决策,而遍历所有天数就能找到全局最优解。这符合贪心算法的"局部最优→全局最优"特性。

面试官:请写一下代码。

:(边写边说)我用两个变量,min_price 记录历史最低价,max_profit 记录最大利润。初始化 min_price 为第一天价格,max_profit 为 0。然后从第二天开始遍历,每天做两件事:一是计算今天卖出的利润并更新 max_profit,二是更新 min_price。

def maxProfit(prices):
    min_price = prices[0]
    max_profit = 0
    for price in prices[1:]:
        profit = price - min_price
        max_profit = max(max_profit, profit)
        min_price = min(min_price, price)
    return max_profit

面试官:测试一下?

:用示例 [7,1,5,3,6,4] 走一遍:

  • 第1天价格7,min_price=7,max_profit=0
  • 第2天价格1,min_price更新为1,profit=1-1=0,max_profit=0
  • 第3天价格5,profit=5-1=4,max_profit更新为4
  • 第4天价格3,profit=3-1=2,max_profit保持4
  • 第5天价格6,profit=6-1=5,max_profit更新为5 ← 最优方案
  • 第6天价格4,profit=4-1=3,max_profit保持5
  • 返回 5 ✓

再测一个边界情况 7,6,4,3,1:每天的 profit 都是 0 或负数,max_profit 始终为 0,返回 0 ✓

高频追问

追问应答策略
"还有更优解吗?"时间O(n)已经是理论最优(至少要遍历一遍),空间O(1)也已最优。无法再优化。
"如果可以交易多次呢?"那就变成 LeetCode 122,用贪心策略:只要后一天价格高于前一天就交易,累加所有上涨差价。
"如果最多交易k次呢?"那就是 LeetCode 188,需要用动态规划,状态定义为 dp[i][j][0/1] 表示第i天、已交易j次、当前是否持有股票的最大利润。
"数据量特别大怎么办?"O(n)已经是线性时间,可以流式处理。如果内存不够,可以分块读取,每块维护局部最小值和最大利润,最后合并结果。
"如果有手续费呢?"计算利润时减去手续费:profit = price - min_price - fee,其余逻辑不变。

🎓 知识点总结

Python技巧卡片 🐍

# 技巧1:无穷大初始化 — 用于找最小值
min_price = float('inf')
# 这样第一次比较时任何实际价格都会更新 min_price

# 技巧2:同时更新两个变量
max_profit = max(max_profit, price - min_price)
min_price = min(min_price, price)
# 注意顺序:先用旧的min_price计算profit,再更新min_price

# 技巧3:三元表达式简化
max_profit = profit if profit > max_profit else max_profit
# 等价于: max_profit = max(max_profit, profit)

# 技巧4:enumerate遍历时可以同时获取索引和值
for i, price in enumerate(prices):
    print(f"第{i}天价格为{price}")

💡 底层原理(选读)

为什么贪心算法在这道题有效?

贪心算法成立的两个关键条件:

  1. 最优子结构:问题的最优解包含子问题的最优解。对于股票问题,如果在第 i 天卖出获得最大利润,那么买入日一定是第 0 到 i-1 天中价格最低的那天。
  2. 贪心选择性质:每一步都选择当前看来最优的选择(维护历史最低价),不会影响后续选择,最终能得到全局最优解。

与动态规划的区别:

  • 贪心算法:每步只做局部最优决策,不回溯,不需要存储子问题结果。时间O(n),空间O(1)。
  • 动态规划:需要存储子问题结果(dp数组),通过状态转移方程求解。时间O(n),空间O(n)(可优化为O(1))。

这道题用贪心更简洁,但理解 DP 思路有助于解决股票系列的进阶题(如 LeetCode 123, 188, 309, 714)。

算法模式卡片 📐

  • 模式名称:贪心算法 — 维护历史最值
  • 适用条件:需要在遍历过程中实时维护某个历史最值(最大/最小/最远等),并基于该最值做决策
  • 识别关键词:"历史最低/最高"、"截止到当前"、"一次遍历"、"实时更新"
  • 模板代码:
def greedy_maintain_extreme(arr):
    """贪心维护历史最值模板"""
    if not arr:
        return 0

    extreme_value = arr[0]  # 历史最值(最小/最大)
    result = 0              # 要求的结果

    for val in arr[1:]:
        # 基于历史最值计算当前结果
        current_result = calculate(val, extreme_value)
        result = update(result, current_result)

        # 更新历史最值
        extreme_value = update_extreme(extreme_value, val)

    return result

同类型题目:

  • LeetCode 122. 买卖股票的最佳时机 II(多次交易)
  • LeetCode 55. 跳跃游戏(维护最远可达位置)
  • LeetCode 45. 跳跃游戏 II(贪心跳跃)
  • LeetCode 53. 最大子数组和(Kadane算法)

易错点 ⚠️

  1. 错误:先更新 min_price 再计算 profit

    # ❌ 错误写法
    min_price = min(min_price, price)
    profit = price - min_price  # 这样会导致 profit 为 0(自己和自己比)
    

    正确做法:先用旧的 min_price 计算 profit,再更新 min_price。

  2. 错误:忘记处理单调递减的情况

    # ❌ 错误写法:初始化 max_profit = -1
    max_profit = -1  # 如果所有价格递减,会返回 -1 而非 0
    

    正确做法:初始化 max_profit = 0,因为不交易利润为 0。

  3. 错误:没有考虑只有一天的情况

    # ❌ 错误写法
    for price in prices[1:]:  # 如果 prices = [5],会跳过循环
    

    正确做法:要么在开头检查长度,要么确保初始化能处理边界情况。

  4. 错误:认为必须找到具体的买入日和卖出日

    • 题目只要求返回最大利润,不需要返回具体日期
    • 如果要返回日期,需要额外记录 buy_day 和 sell_day

🏗️ 工程实战(选读)

这个算法思想在真实项目中的应用,让你知道"学了有什么用"。

  • 场景1:股票交易系统

    • 高频交易系统需要实时计算最优买入卖出时机
    • 本题的贪心思想可以扩展为"滑动窗口内的最优交易策略"
  • 场景2:电商价格监控

    • 电商平台的"价格历史曲线"功能,告诉用户"什么时候买最划算"
    • 维护历史最低价并计算当前价格的"性价比"
  • 场景3:资源调度优化

    • 云计算平台的资源购买策略:在价格低谷期购买计算资源,在高峰期使用
    • 贪心地选择最低价时段采购,最高价时段释放
  • 场景4:数据流处理

    • 流式数据中实时维护"历史最值"是常见需求
    • 例如:监控系统中维护"过去1小时内的最低延迟"用于对比当前性能

🏋️ 举一反三

完成本课后,试试这些同类题目来巩固知识:

题目难度相关知识点提示
LeetCode 122. 买卖股票的最佳时机 IIMedium贪心(多次交易)只要后一天价格高就交易,累加所有上涨差价
LeetCode 55. 跳跃游戏Medium贪心(维护最远可达)维护当前能到达的最远位置
LeetCode 45. 跳跃游戏 IIMedium贪心(最少跳跃次数)在当前能到达的范围内选择下一跳最远的
LeetCode 53. 最大子数组和Medium贪心/DP(Kadane)维护"以当前元素结尾的最大和"
LeetCode 123. 买卖股票的最佳时机 IIIHard动态规划(多次交易限制)状态机DP,限制交易次数

📝 课后小测

试试这道变体题,不要看答案,自己先想5分钟!

题目:给定股票价格数组 prices 和一个整数 fee 表示交易手续费(每次交易都要支付)。你可以进行多次交易,但每次卖出时需要支付手续费。计算能获得的最大利润。

示例:prices = [1,3,2,8,4,9], fee = 2,输出:8(买入1卖出8支付2手续费获利5,买入4卖出9支付2手续费获利3,总利润8)

💡 提示(实在想不出来再点开)

这道题是"买卖股票的最佳时机 II"(可多次交易) + 手续费。可以用贪心或动态规划。贪心思路:维护"有效买入价"(考虑手续费后的成本),只有当"卖出价 - 有效买入价 > fee"时才交易。

✅ 参考答案
def maxProfit_with_fee(prices: List[int], fee: int) -> int:
    """
    股票交易含手续费(贪心算法)
    思路:维护有效买入价,只有利润 > 手续费时才卖出
    """
    if not prices:
        return 0

    max_profit = 0
    min_price = prices[0]  # 当前的有效买入价

    for price in prices[1:]:
        if price < min_price:
            # 发现更低价格,更新买入价
            min_price = price
        elif price > min_price + fee:
            # 利润 > 手续费,执行卖出
            max_profit += price - min_price - fee
            # 更新买入价为"卖出价 - 手续费"(允许连续交易优化)
            min_price = price - fee

    return max_profit

# 测试
print(maxProfit_with_fee([1,3,2,8,4,9], 2))  # 输出:8

核心思路:每次卖出后,将"有效买入价"更新为"卖出价 - fee",这样如果后续价格继续上涨,可以无缝连续交易而不重复支付手续费。这是贪心策略的巧妙应用。


如果这篇内容对你有帮助,推荐收藏 AI Compass:github.com/tingaicompa… 更多系统化题解、编程基础和 AI 学习资料都在这里,后续复习和拓展会更省时间。