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))
- [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 解题思路
这道题使用贪心算法:
- 只要今天的价格低于明天的价格,就在今天买入明天卖出
- 累加所有正利润
- 返回总利润
解题步骤:
- 初始化hand为第一天的价格(当前持有成本)
- 初始化earn为0(总利润)
- 遍历数组,如果当前价格低于持有成本,更新持有成本
- 如果当前价格高于持有成本,计算利润并累加
- 返回总利润
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 执行流程详解
示例1 :prices = [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
示例2 :prices = [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
示例3 :prices = [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 解题思路
这道题使用逆向思维:
- 从最后一个位置开始,向前寻找能跳到当前位置的位置
- 如果能找到,将目标位置更新为该位置
- 重复这个过程,直到到达起点或无法继续
- 最后判断是否能到达起点(位置0)
解题步骤:
- 初始化end为最后一个位置
- 从倒数第二个位置向前遍历
- 如果当前位置+跳跃长度 >= end,说明可以跳到end,更新end
- 最后判断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 执行流程详解
示例1 :nums = [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
示例2 :nums = [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. 总结
今天我们学习了两道贪心算法题目:
- 买卖股票的最佳时机II:掌握贪心算法收集所有正利润,理解多次买卖的策略
- 跳跃游戏:掌握逆向思维判断可达性,理解从终点向起点推导的方法
核心收获:
- 贪心算法通过局部最优得到全局最优
- 只要明天涨,今天就买入明天卖出
- 逆向思维可以将问题简化
- 从终点向起点推导,更新可达目标
- 贪心算法不一定适用于所有问题
练习建议:
- 尝试正向遍历解决跳跃游戏
- 思考如果限制交易次数,应该如何解决
- 学习其他贪心算法题目,如区间调度
参考资源
文章标签
#LeetCode #算法 #Java #贪心算法 #数组
喜欢这篇文章吗?别忘了点赞、收藏和分享!你的支持是我创作的最大动力!