LeetCode100天Day15-买卖股票II与跳跃游戏

LeetCode100天Day15-买卖股票II与跳跃游戏:贪心算法与逆向思维

摘要:本文详细解析了LeetCode中两道经典题目------"买卖股票的最佳时机II"和"跳跃游戏"。通过贪心算法实现多次买卖股票,以及使用逆向思维判断能否到达终点,帮助读者掌握贪心策略和逆向推导的技巧。

目录

文章目录

  • LeetCode100天Day15-买卖股票II与跳跃游戏:贪心算法与逆向思维
    • 目录
    • [1. 买卖股票的最佳时机II(Best Time to Buy and Sell Stock II)](#1. 买卖股票的最佳时机II(Best Time to Buy and Sell Stock II))
      • [1.1 题目描述](#1.1 题目描述)
      • [1.2 解题思路](#1.2 解题思路)
      • [1.3 代码实现](#1.3 代码实现)
      • [1.4 代码逐行解释](#1.4 代码逐行解释)
      • [1.5 执行流程详解](#1.5 执行流程详解)
      • [1.6 算法图解](#1.6 算法图解)
      • [1.7 复杂度分析](#1.7 复杂度分析)
      • [1.8 边界情况](#1.8 边界情况)
    • [2. 跳跃游戏(Jump Game)](#2. 跳跃游戏(Jump Game))
      • [2.1 题目描述](#2.1 题目描述)
      • [2.2 解题思路](#2.2 解题思路)
      • [2.3 代码实现](#2.3 代码实现)
      • [2.4 代码逐行解释](#2.4 代码逐行解释)
      • [2.5 执行流程详解](#2.5 执行流程详解)
      • [2.6 算法图解](#2.6 算法图解)
      • [2.7 复杂度分析](#2.7 复杂度分析)
      • [2.8 边界情况](#2.8 边界情况)
    • [3. 两题对比与总结](#3. 两题对比与总结)
      • [3.1 算法对比](#3.1 算法对比)
      • [3.2 贪心算法模板](#3.2 贪心算法模板)
      • [3.3 逆向思维的应用](#3.3 逆向思维的应用)
      • [3.4 贪心 vs 正向遍历](#3.4 贪心 vs 正向遍历)
    • [4. 总结](#4. 总结)
    • 参考资源
    • 文章标签

1. 买卖股票的最佳时机II(Best Time to Buy and Sell Stock II)

1.1 题目描述

给你一个整数数组 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.2 解题思路

这道题使用贪心算法:

  1. 只要今天的价格低于明天的价格,就在今天买入明天卖出
  2. 累加所有正利润
  3. 返回总利润

解题步骤

  1. 初始化hand为第一天的价格(当前持有成本)
  2. 初始化earn为0(总利润)
  3. 遍历数组,如果当前价格低于持有成本,更新持有成本
  4. 如果当前价格高于持有成本,计算利润并累加
  5. 返回总利润

1.3 代码实现

java 复制代码
class Solution {
    public int maxProfit(int[] prices) {
        int hand = prices[0];
        int earn = 0;
        for(int i = 1;i < prices.length;i++){
            if(prices[i] < hand){
                hand = prices[i];
            }
            if(prices[i] - hand > 0){
                earn += prices[i] - hand;
                hand = prices[i];
            }
        }
        return earn;
    }
}

1.4 代码逐行解释

第一部分:初始化
java 复制代码
int hand = prices[0];
int earn = 0;

变量说明

变量 初始值 含义
hand prices[0] 当前持有的买入成本
earn 0 累计利润

为什么hand初始为prices[0]

复制代码
假设第一天就买入:
prices = [7, 1, 5, 3, 6, 4]

hand = 7 (第一天价格)
earn = 0

如果第二天价格更低,可以"更换"买入时机
第二部分:遍历计算利润
java 复制代码
for(int i = 1;i < prices.length;i++){
    if(prices[i] < hand){
        hand = prices[i];
    }
    if(prices[i] - hand > 0){
        earn += prices[i] - hand;
        hand = prices[i];
    }
}

逻辑详解

java 复制代码
if(prices[i] < hand){
    hand = prices[i];
}
条件 操作 说明
prices[i] < hand 更新hand 找到更低的买入成本
java 复制代码
if(prices[i] - hand > 0){
    earn += prices[i] - hand;
    hand = prices[i];
}
条件 操作 说明
prices[i] - hand > 0 有利润 卖出并计算利润
earn += ... 累加利润 增加总收益
hand = prices[i] 更新hand 重新买入(同一天)

1.5 执行流程详解

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

复制代码
初始状态:
prices = [7, 1, 5, 3, 6, 4]
hand = 7
earn = 0

i=1: prices[1]=1
  1 < hand(7)? 是
  hand = 1
  1 - 1 > 0? 否

i=2: prices[2]=5
  5 < hand(1)? 否
  5 - 1 = 4 > 0? 是
  earn = 0 + 4 = 4
  hand = 5

i=3: prices[3]=3
  3 < hand(5)? 是
  hand = 3
  3 - 3 > 0? 否

i=4: prices[4]=6
  6 < hand(3)? 否
  6 - 3 = 3 > 0? 是
  earn = 4 + 3 = 7
  hand = 6

i=5: prices[5]=4
  4 < hand(6)? 是
  hand = 4
  4 - 4 > 0? 否

循环结束,返回 earn = 7

输出: 7

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

复制代码
初始状态:
prices = [1, 2, 3, 4, 5]
hand = 1
earn = 0

i=1: prices[1]=2
  2 < 1? 否
  2 - 1 = 1 > 0? 是
  earn = 1, hand = 2

i=2: prices[2]=3
  3 < 2? 否
  3 - 2 = 1 > 0? 是
  earn = 2, hand = 3

i=3: prices[3]=4
  4 < 3? 否
  4 - 3 = 1 > 0? 是
  earn = 3, hand = 4

i=4: prices[4]=5
  5 < 4? 否
  5 - 4 = 1 > 0? 是
  earn = 4, hand = 5

循环结束,返回 earn = 4

输出: 4

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

复制代码
初始状态:
prices = [7, 6, 4, 3, 1]
hand = 7
earn = 0

i=1: prices[1]=6
  6 < 7? 是
  hand = 6
  6 - 6 > 0? 否

i=2: prices[2]=4
  4 < 6? 是
  hand = 4
  4 - 4 > 0? 否

i=3: prices[3]=3
  3 < 4? 是
  hand = 3
  3 - 3 > 0? 否

i=4: prices[4]=1
  1 < 3? 是
  hand = 1
  1 - 1 > 0? 否

循环结束,返回 earn = 0

输出: 0

1.6 算法图解

复制代码
prices = [7, 1, 5, 3, 6, 4]
天数:     0  1  2  3  4  5

贪心策略:只要明天涨,今天就买入明天卖出

i=0→1: 7 → 1 (跌,不买)
i=1→2: 1 → 5 (涨)
       买入价1,卖出价5,利润4
       earn = 4

i=2→3: 5 → 3 (跌,不买)
i=3→4: 3 → 6 (涨)
       买入价3,卖出价6,利润3
       earn = 4 + 3 = 7

i=4→5: 6 → 4 (跌,不买)

总利润: 7

交易记录:
第2天买入(价格1),第3天卖出(价格5),利润4
第4天买入(价格3),第5天卖出(价格6),利润3

prices = [1, 2, 3, 4, 5]

i=0→1: 1 → 2 (涨),利润1
i=1→2: 2 → 3 (涨),利润1
i=2→3: 3 → 4 (涨),利润1
i=3→4: 4 → 5 (涨),利润1

总利润: 1+1+1+1 = 4

等价于:
第1天买入(价格1),第5天卖出(价格5),利润4

1.7 复杂度分析

分析维度 复杂度 说明
时间复杂度 O(n) 遍历数组一次
空间复杂度 O(1) 只使用常数空间

简化版贪心

java 复制代码
// 简化版:只收集所有正利润
class Solution {
    public int maxProfit(int[] prices) {
        int earn = 0;
        for (int i = 1; i < prices.length; i++) {
            if (prices[i] > prices[i - 1]) {
                earn += prices[i] - prices[i - 1];
            }
        }
        return earn;
    }
}

1.8 边界情况

prices 说明 输出
[7,6,4,3,1] 持续下跌 0
[1,2,3,4,5] 持续上涨 4
[1] 单个价格 0
[2,4,1,5] 波动 6

2. 跳跃游戏(Jump Game)

2.1 题目描述

给你一个非负整数数组 nums,你最初位于数组的 第一个下标

数组中的每个元素代表你在该位置可以跳跃的最大长度。

判断你是否能够到达最后一个下标,如果可以,返回 true;否则,返回 false

示例 1

复制代码
输入:nums = [2,3,1,1,4]
输出:true
解释:可以先跳1步,从下标0到达下标1,然后再从下标1跳3步到达最后一个下标。

示例 2

复制代码
输入:nums = [3,2,1,0,4]
输出:false
解释:无论怎样,总会到达下标为3的位置。但该下标的最大跳跃长度是0,所以永远不可能到达最后一个下标。

2.2 解题思路

这道题使用逆向思维:

  1. 从最后一个位置开始,向前寻找能跳到当前位置的位置
  2. 如果能找到,将目标位置更新为该位置
  3. 重复这个过程,直到到达起点或无法继续
  4. 最后判断是否能到达起点(位置0)

解题步骤

  1. 初始化end为最后一个位置
  2. 从倒数第二个位置向前遍历
  3. 如果当前位置+跳跃长度 >= end,说明可以跳到end,更新end
  4. 最后判断end是否等于0

2.3 代码实现

java 复制代码
class Solution {
    public boolean canJump(int[] nums) {
        int end = nums.length-1;
        for(int i = nums.length - 2;i >= 0;i--){
            if(nums[i] + i >= end){
                end = i;
            }
        }
        return (end == 0);
    }
}

2.4 代码逐行解释

第一部分:初始化
java 复制代码
int end = nums.length-1;

功能:设置目标位置为最后一个位置

nums length end
[2,3,1,1,4] 5 4
[3,2,1,0,4] 5 4
第二部分:逆向遍历
java 复制代码
for(int i = nums.length - 2;i >= 0;i--){
    if(nums[i] + i >= end){
        end = i;
    }
}

变量说明

变量 含义
end 当前需要到达的目标位置
i 当前检查的位置(从后向前)
nums[i] 位置i的最大跳跃长度
nums[i] + i 从位置i能到达的最远位置

判断逻辑

java 复制代码
if(nums[i] + i >= end)
条件 说明
nums[i] + i >= end 位置i可以跳到或超过end

更新逻辑

java 复制代码
end = i;
操作 说明
end = i 将目标位置更新为i
第三部分:返回结果
java 复制代码
return (end == 0);
结果 说明
end == 0 可以到达起点,返回true
end != 0 无法到达起点,返回false

2.5 执行流程详解

示例1nums = [2,3,1,1,4]

复制代码
初始状态:
nums = [2, 3, 1, 1, 4]
索引:   0  1  2  3  4
end = 4

i=3: nums[3]=1
  nums[3] + 3 = 1 + 3 = 4
  4 >= end(4)? 是
  end = 3

i=2: nums[2]=1
  nums[2] + 2 = 1 + 2 = 3
  3 >= end(3)? 是
  end = 2

i=1: nums[1]=3
  nums[1] + 1 = 3 + 1 = 4
  4 >= end(2)? 是
  end = 1

i=0: nums[0]=2
  nums[0] + 0 = 2 + 0 = 2
  2 >= end(1)? 是
  end = 0

循环结束,end == 0? 是

输出: true

示例2nums = [3,2,1,0,4]

复制代码
初始状态:
nums = [3, 2, 1, 0, 4]
索引:   0  1  2  3  4
end = 4

i=3: nums[3]=0
  nums[3] + 3 = 0 + 3 = 3
  3 >= end(4)? 否
  end = 4

i=2: nums[2]=1
  nums[2] + 2 = 1 + 2 = 3
  3 >= end(4)? 否
  end = 4

i=1: nums[1]=2
  nums[1] + 1 = 2 + 1 = 3
  3 >= end(4)? 否
  end = 4

i=0: nums[0]=3
  nums[0] + 0 = 3 + 0 = 3
  3 >= end(4)? 否
  end = 4

循环结束,end == 0? 否

输出: false

2.6 算法图解

复制代码
nums = [2, 3, 1, 1, 4]
索引:   0  1  2  3  4

逆向思考:
目标:从某个位置跳到位置4

步骤1: 检查位置3
数组: [2,  3,  1,  1,  4]
索引:   0   1   2   3   4
                        ↑
                       end=4

位置3: nums[3]=1
能跳到: 3 + 1 = 4
4 >= 4? 是
end = 3

步骤2: 检查位置2
数组: [2,  3,  1,  1,  4]
索引:   0   1   2   3   4
                    ↑
                   end=3

位置2: nums[2]=1
能跳到: 2 + 1 = 3
3 >= 3? 是
end = 2

步骤3: 检查位置1
数组: [2,  3,  1,  1,  4]
索引:   0   1   2   3   4
                ↑
               end=2

位置1: nums[1]=3
能跳到: 1 + 3 = 4
4 >= 2? 是
end = 1

步骤4: 检查位置0
数组: [2,  3,  1,  1,  4]
索引:   0   1   2   3   4
            ↑
           end=1

位置0: nums[0]=2
能跳到: 0 + 2 = 2
2 >= 1? 是
end = 0

结果: end == 0,可以到达

跳跃路径:
0 → 1 → 4
(跳1步) (跳3步)

nums = [3, 2, 1, 0, 4]
索引:   0  1  2  3  4

逆向思考:

初始: end = 4

i=3: nums[3]=0, 能跳到 3+0=3
     3 >= 4? 否,无法到达4

i=2: nums[2]=1, 能跳到 2+1=3
     3 >= 4? 否,无法到达4

i=1: nums[1]=2, 能跳到 1+2=3
     3 >= 4? 否,无法到达4

i=0: nums[0]=3, 能跳到 0+3=3
     3 >= 4? 否,无法到达4

结果: end = 4 ≠ 0,无法到达

问题:位置3的跳跃长度为0,是"陷阱"
     任何位置都无法跳过这个位置到达4

2.7 复杂度分析

分析维度 复杂度 说明
时间复杂度 O(n) 遍历数组一次
空间复杂度 O(1) 只使用常数空间

正向遍历版本

java 复制代码
// 正向版本:记录能到达的最远位置
class Solution {
    public boolean canJump(int[] nums) {
        int maxReach = 0;
        for (int i = 0; i < nums.length; i++) {
            if (i > maxReach) {
                return false;  // 无法到达位置i
            }
            maxReach = Math.max(maxReach, i + nums[i]);
            if (maxReach >= nums.length - 1) {
                return true;
            }
        }
        return true;
    }
}

2.8 边界情况

nums 说明 输出
[0] 单个元素 true
[1,0] 两元素 true
[0,1] 无法开始 false
[2,0,0] 可以到达 true

3. 两题对比与总结

3.1 算法对比

对比项 买卖股票II 跳跃游戏
核心算法 贪心算法 逆向思维
数据结构 数组 数组
时间复杂度 O(n) O(n)
空间复杂度 O(1) O(1)
应用场景 最优化问题 可达性判断

3.2 贪心算法模板

java 复制代码
// 贪心算法模板:收集所有正收益
int result = 0;
for (int i = 1; i < n; i++) {
    if (当前决策有利) {
        result += 收益;
    }
}
return result;

3.3 逆向思维的应用

跳跃游戏的逆向推导

java 复制代码
// 从终点向起点推导
int end = n - 1;
for (int i = n - 2; i >= 0; i--) {
    if (能从i到达end) {
        end = i;  // 更新目标
    }
}
return end == 0;

适用场景

  • 需要判断是否能到达某个目标
  • 从目标逆向推导更容易判断
  • 避免正向遍历的复杂判断

3.4 贪心 vs 正向遍历

对比项 贪心算法 正向遍历
思维方式 局部最优 全局考虑
实现难度 简单 中等
适用场景 明确的最优子结构 需要维护状态
股票问题 适合(涨就买) 适合(记录最低价)
跳跃游戏 不适合 适合

4. 总结

今天我们学习了两道贪心算法题目:

  1. 买卖股票的最佳时机II:掌握贪心算法收集所有正利润,理解多次买卖的策略
  2. 跳跃游戏:掌握逆向思维判断可达性,理解从终点向起点推导的方法

核心收获

  • 贪心算法通过局部最优得到全局最优
  • 只要明天涨,今天就买入明天卖出
  • 逆向思维可以将问题简化
  • 从终点向起点推导,更新可达目标
  • 贪心算法不一定适用于所有问题

练习建议

  1. 尝试正向遍历解决跳跃游戏
  2. 思考如果限制交易次数,应该如何解决
  3. 学习其他贪心算法题目,如区间调度

参考资源

文章标签

#LeetCode #算法 #Java #贪心算法 #数组

喜欢这篇文章吗?别忘了点赞、收藏和分享!你的支持是我创作的最大动力!

相关推荐
微光守望者2 小时前
游戏创意的构思技巧
人工智能·游戏
套码汉子3 小时前
SLG游戏多赛季配置管理架构:从简单到复杂的设计演进
游戏·架构
科技块儿15 小时前
IP定位技术:游戏反外挂体系中的精准识别引擎
数据库·tcp/ip·游戏
卓怡学长15 小时前
m115乐购游戏商城系统
java·前端·数据库·spring boot·spring·游戏
2501_9445264215 小时前
Flutter for OpenHarmony 万能游戏库App实战 - 蜘蛛纸牌游戏实现
android·java·python·flutter·游戏
云边散步20 小时前
godot2D游戏教程系列一(5)
游戏·游戏开发
sunfove20 小时前
Python小游戏:在 2048 游戏中实现基于线性插值(Lerp)的平滑动画
开发语言·python·游戏
2501_9445264220 小时前
Flutter for OpenHarmony 万能游戏库App实战 - 抽牌游戏实现
android·开发语言·python·flutter·游戏
夜雨声烦丿21 小时前
Flutter 框架跨平台鸿蒙开发 - 游戏存档管理器应用开发教程
flutter·游戏·华为·harmonyos