想系统提升编程能力、查看更完整的学习路线,欢迎访问 AI Compass:github.com/tingaicompa… 仓库持续更新刷题题解、Python 基础和 AI 实战内容,适合想高效进阶的你。
📖 第67课:跳跃游戏II
模块:贪心算法 | 难度:Medium ⭐⭐ LeetCode 链接:leetcode.cn/problems/ju… 前置知识:第66课《跳跃游戏》 预计学习时间:25分钟
🎯 题目描述
给定一个非负整数数组nums,你最初位于数组的第一个位置。数组中的每个元素代表你在该位置可以跳跃的最大长度。你的目标是使用最少的跳跃次数到达数组的最后一个位置。注意:假设你总是可以到达数组的最后一个位置。
示例:
输入:nums = [2,3,1,1,4]
输出:2
解释:跳到最后位置的最小跳跃次数是2。从下标0跳1步到下标1,然后跳3步到最后位置。
约束条件:
1 <= nums.length <= 10^40 <= nums[i] <= 1000- 题目保证总能到达最后位置(与第66题的关键区别)
🧪 边界用例(面试必考)
| 用例类型 | 输入 | 期望输出 | 考察点 |
|---|---|---|---|
| 最小输入 | nums=[0] | 0 | 已在终点无需跳 |
| 单步到达 | nums=[5,1,1,1,1,1] | 1 | 第一步直接跳到终点 |
| 每次跳1 | nums=[1,1,1,1,1] | 4 | 最坏情况,需n-1次 |
| 大跨度 | nums=[10,9,8,7,6,5,4,3,2,1,0] | 1 | 第一步就能到 |
| 混合策略 | nums=[2,3,1,1,4] | 2 | 核心测试用例 |
💡 思路引导
生活化比喻
想象你在玩一个跳格子游戏,每个格子上写着你能向前跳的最大步数。你的目标是用最少的跳跃次数到达终点。
🐌 笨办法:每次跳一小步,谨慎前进。这样虽然稳妥,但会浪费很多次跳跃机会。比如站在能跳5步的格子上却只跳1步,太保守了!
🚀 聪明办法:站在当前位置时,先"环顾四周",看看在当前这次跳跃范围内,哪个落脚点能让你在下一次跳得最远。这就像登山时选择能看到更远风景的中转站——贪心地选择最佳跳板。
关键洞察
每次跳跃都选择"下一跳能到达最远位置"的落脚点,局部最优即全局最优,这就是贪心策略的精髓。
与第66题的本质区别
- 第66题(跳跃游戏):判断能否到达终点 → 只需维护一个
max_reach最远可达位置 - 第67题(跳跃游戏II):求最少跳跃次数 → 需要分层跳跃,记录每次跳跃的边界
🧠 解题思维链
这一节模拟你在面试中"从零开始思考"的过程。
Step 1:理解题目 → 锁定输入输出
- 输入:
nums = [2,3,1,1,4],每个元素表示最大跳跃步数 - 输出:最少跳跃次数,如示例中是
2 - 限制:题目保证能到达终点(简化了问题,无需判断可达性)
Step 2:先想笨办法(暴力法)
可以用BFS层序遍历:把每个位置看作节点,能跳到的位置是邻接节点,求从起点到终点的最短路径。
# BFS思路伪代码
queue = [0] # 起点
steps = 0
while queue:
# 处理当前层所有节点
for pos in current_level:
if pos == n-1: # 到达终点
return steps
# 把所有能跳到的位置加入下一层
for next_pos in range(pos+1, pos+nums[pos]+1):
queue.append(next_pos)
steps += 1
- 时间复杂度:O(n²) — 最坏情况每个位置都要展开所有可达位置
- 瓶颈在哪:重复处理很多中间位置,有大量冗余计算
Step 3:瓶颈分析 → 优化方向
观察示例[2,3,1,1,4]:
第0步:站在index=0(值为2),能跳到index=1或2
第1步:从1或2起跳,哪个更好?
- 从index=1(值为3)能跳到index=2,3,4 → 最远到4(终点)✅
- 从index=2(值为1)只能跳到index=3 → 不如前者
核心问题:不需要逐个尝试所有可达位置,只需知道当前跳跃范围内的最远可达位置。
优化思路:
- 维护当前跳跃的边界
current_end - 在到达边界前,贪心地更新"下一跳能到达的最远位置"
farthest - 到达边界时,跳跃次数+1,更新边界为
farthest
Step 4:选择武器
- 选用:贪心算法 + 双边界
- 理由:通过贪心策略(每次选择最远可达)避免了BFS的冗余,O(n)一次遍历解决
🔑 模式识别提示:当题目出现"最少次数到达终点"+"每个位置有多种选择",优先考虑贪心分层跳跃模式
🔑 解法一:BFS层序遍历(直觉法)
思路
把数组看作图,用BFS求最短路径:每次跳跃相当于走一层,第一次到达终点时就是最少步数。
图解过程
示例:nums = [2,3,1,1,4]
初始:queue=[0], steps=0
位置: 0 1 2 3 4
值: 2 3 1 1 4
↑ 当前位置
第1次跳跃(处理位置0):
从0跳到1,2 → queue=[1,2], steps=1
位置: 0 1 2 3 4
↑ ↑ 可选落脚点
第2次跳跃(处理位置1,2):
从1能跳到2,3,4 → 发现4是终点!
从2能跳到3
→ 返回steps=2 ✅
Python代码
from typing import List
from collections import deque
def jump_bfs(nums: List[int]) -> int:
"""
解法一:BFS层序遍历
思路:把跳跃问题转化为图的最短路径问题
"""
n = len(nums)
if n == 1: # 已在终点
return 0
queue = deque([0]) # 起点
visited = {0}
steps = 0
while queue:
size = len(queue)
steps += 1
# 处理当前层所有位置
for _ in range(size):
pos = queue.popleft()
# 尝试从当前位置跳到所有可达位置
for next_pos in range(pos + 1, min(pos + nums[pos] + 1, n)):
if next_pos == n - 1: # 到达终点
return steps
if next_pos not in visited:
visited.add(next_pos)
queue.append(next_pos)
return steps
# ✅ 测试
print(jump_bfs([2, 3, 1, 1, 4])) # 期望输出:2
print(jump_bfs([2, 3, 0, 1, 4])) # 期望输出:2
print(jump_bfs([0])) # 期望输出:0
print(jump_bfs([1, 1, 1, 1])) # 期望输出:3
复杂度分析
- 时间复杂度:O(n²) — 最坏情况下每个位置要展开其所有可达位置,如
[1,1,1,..1]需要O(1+2+3+...+n) = O(n²)- 具体地说:如果
n=10000,可能需要约5000万次操作
- 具体地说:如果
- 空间复杂度:O(n) — 队列和visited集合最多存储n个位置
优缺点
- ✅ 思路直观,符合"最短路径"的直觉
- ✅ 代码结构清晰,易于理解
- ❌ 时间复杂度高,大规模数据会超时
- ❌ 有大量重复计算,访问了很多不必要的中间位置
🏆 解法二:贪心分层跳跃(最优解)
优化思路
BFS的问题在于逐个尝试所有可达位置。其实我们不需要知道具体跳到哪个位置,只需知道:
- 当前跳跃能覆盖的范围(
current_end) - 在这个范围内,下一跳最远能到哪(
farthest)
当到达current_end时,必须跳一次,更新边界为farthest。
💡 关键想法:贪心地维护"分层边界",每层内找到能跳得最远的位置作为下一层的边界。
图解过程
示例:nums = [2,3,1,1,4]
初始化:
current_end = 0 (第1跳的边界)
farthest = 0 (目前能到达的最远位置)
jumps = 0
遍历过程:
i=0: nums[0]=2
farthest = max(0, 0+2) = 2 (在index=0能跳到2)
到达current_end → jumps++ → current_end=2
[2, 3, 1, 1, 4]
^-边界-^ (第1跳能覆盖0~2)
i=1: nums[1]=3
farthest = max(2, 1+3) = 4 (在index=1能跳到4,更远!)
未到边界,继续
i=2: nums[2]=1
farthest = max(4, 2+1) = 4 (还是4)
到达current_end=2 → jumps++ → current_end=4
[2, 3, 1, 1, 4]
^-----^ (第2跳能覆盖3~4,已包含终点!)
停止遍历(i不到n-1就结束了)
返回jumps=2 ✅
可视化分层:
层0(起点): [0]
/ \
层1(第1跳): [1] [2] → 覆盖范围0~2
|\ /|
层2(第2跳): [3][4] → 从1能跳到4(终点)
再看一个例子:nums = [5,9,3,2,1,0,2,3,3,1,0,0]
初始:current_end=0, farthest=0, jumps=0
i=0: farthest=0+5=5, 到达边界 → jumps=1, current_end=5
层1覆盖:0~5
在这个范围内:
i=1: farthest=max(5,1+9)=10
i=2: farthest=max(10,2+3)=10
...
i=5: farthest=10, 到达边界 → jumps=2, current_end=10
层2覆盖:6~10
在这个范围内farthest能到11 → 已包含终点
→ 返回jumps=2
Python代码
def jump(nums: List[int]) -> int:
"""
解法二:贪心分层跳跃(最优解)
思路:维护当前跳跃边界和最远可达位置,贪心地选择最佳跳板
"""
n = len(nums)
if n == 1: # 特殊情况:已在终点
return 0
jumps = 0 # 跳跃次数
current_end = 0 # 当前跳跃能到达的最远位置(边界)
farthest = 0 # 在当前边界内探索到的下一跳最远位置
# 注意:只需遍历到n-2,因为到达n-1就是终点了
for i in range(n - 1):
# 贪心:在当前范围内找到能跳得最远的位置
farthest = max(farthest, i + nums[i])
# 到达当前跳跃的边界,必须跳一次
if i == current_end:
jumps += 1
current_end = farthest # 更新边界为下一跳能到达的最远位置
# 剪枝:如果已经能到达终点,提前结束
if current_end >= n - 1:
break
return jumps
# ✅ 测试
print(jump([2, 3, 1, 1, 4])) # 期望输出:2
print(jump([2, 3, 0, 1, 4])) # 期望输出:2
print(jump([0])) # 期望输出:0
print(jump([1, 1, 1, 1])) # 期望输出:3
print(jump([5, 9, 3, 2, 1, 0, 2, 3, 3, 1, 0, 0])) # 期望输出:3
print(jump([10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0])) # 期望输出:1
复杂度分析
- 时间复杂度:O(n) — 只需一次线性遍历,每个位置访问一次
- 具体地说:如果
n=10000,只需10000次操作,比BFS的5000万次快了5000倍!
- 具体地说:如果
- 空间复杂度:O(1) — 只使用常数个变量
为什么这是最优解?
- 时间已达理论下限:必须至少遍历一次数组来获取信息,O(n)已是最优
- 空间也达到最优:O(1)常数空间,无需额外数据结构
- 贪心策略正确性:每次选择局部最优(最远可达)保证了全局最优(最少跳跃)
- 代码简洁高效:核心逻辑不到10行,易于理解和实现
⚡ 解法三:动态规划(完整性补充)
思路
定义dp[i]为到达位置i的最少跳跃次数,从前往后推导。
Python代码
def jump_dp(nums: List[int]) -> int:
"""
解法三:动态规划
思路:dp[i] = 到达位置i的最少跳跃次数
"""
n = len(nums)
dp = [float('inf')] * n
dp[0] = 0 # 起点不需跳跃
for i in range(n):
# 从位置i能跳到的所有位置
for j in range(i + 1, min(i + nums[i] + 1, n)):
dp[j] = min(dp[j], dp[i] + 1)
return dp[n - 1]
# ✅ 测试
print(jump_dp([2, 3, 1, 1, 4])) # 期望输出:2
print(jump_dp([2, 3, 0, 1, 4])) # 期望输出:2
复杂度分析
- 时间复杂度:O(n²) — 双层循环
- 空间复杂度:O(n) — dp数组
优缺点
- ✅ 思路清晰,DP思想经典
- ❌ 时间和空间都不如贪心解法
- ❌ 面试中如果先说DP会被追问"能否优化"
🐍 Pythonic 写法
利用enumerate和条件表达式的简化版:
def jump_pythonic(nums: List[int]) -> int:
"""Pythonic写法:更紧凑的贪心实现"""
jumps, current_end, farthest = 0, 0, 0
for i, jump_range in enumerate(nums[:-1]): # 排除最后一个元素
farthest = max(farthest, i + jump_range)
if i == current_end:
jumps += 1
current_end = farthest
if current_end >= len(nums) - 1: # 提前终止
break
return jumps
特点:
- 用
enumerate同时获取索引和值 - 用切片
nums[:-1]避免越界判断 - 代码更紧凑,保持了可读性
⚠️ 面试建议:先写解法二的标准贪心版本展示清晰思路,再提Pythonic写法展示语言功底。面试官更看重你的算法思维,而非代码行数。
📊 解法对比
| 维度 | 解法一:BFS | 🏆 解法二:贪心分层(最优) | 解法三:DP |
|---|---|---|---|
| 时间复杂度 | O(n²) | O(n) ← 时间最优 | O(n²) |
| 空间复杂度 | O(n) | O(1) ← 空间最优 | O(n) |
| 代码难度 | 中等(需要BFS模板) | 简单(10行核心代码) | 中等(DP思想) |
| 面试推荐 | ⭐ | ⭐⭐⭐ ← 首选 | ⭐⭐ |
| 适用场景 | 教学理解用 | 面试首选,工程实用 | 备选方案 |
为什么贪心是最优解:
- 时间O(n)已达理论最优:必须至少遍历一次数组,不可能更快
- 空间O(1)也是最优:只用几个变量,不可能更省
- 贪心策略简洁正确:局部最优(选最远跳板)=全局最优(最少跳跃)
- 代码简洁易懂:核心逻辑不到10行,面试中5分钟内可以写出并讲清楚
面试建议:
- 先用30秒简述BFS思路(O(n²)),表明你理解问题本质是"最短路径"
- 立即优化到🏆贪心解法(O(n)),展示优化能力:"我们不需要逐个尝试,只需贪心地维护最远可达边界"
- 重点画图讲解分层跳跃过程,用
[2,3,1,1,4]演示如何分层 - 强调为什么这是最优:时间空间都达到理论极限,贪心策略正确性
- 对比第66题:66题只需一个变量记录最远位置,67题需要两个变量记录当前边界和下一跳最远位置
🎤 面试现场
模拟面试中的完整对话流程,帮你练习"边想边说"。
面试官:请你解决一下跳跃游戏II,求最少跳跃次数。
你:(审题30秒)好的,这道题要求用最少的跳跃次数到达数组末尾,每个位置的值表示最大跳跃步数。这和刚才做的"能否到达"不同,这里要求次数最少。
我的第一个想法是用BFS层序遍历,把问题转化为图的最短路径,时间复杂度O(n²)。但这样会有很多重复计算。
更优的方法是贪心分层跳跃:我维护两个变量——current_end表示当前跳跃的边界,farthest表示在这个边界内能探索到的下一跳最远位置。当我遍历到边界时,就必须跳一次,然后更新边界为farthest。这样只需O(n)时间,O(1)空间。
面试官:很好,请写一下代码并解释。
你:(边写边说)
def jump(nums):
n = len(nums)
if n == 1:
return 0 # 已在终点
jumps = 0 # 跳跃次数
current_end = 0 # 当前跳跃边界
farthest = 0 # 下一跳最远位置
for i in range(n - 1): # 只需遍历到倒数第二个
# 贪心:在当前范围内找最远可达位置
farthest = max(farthest, i + nums[i])
# 到达边界,必须跳一次
if i == current_end:
jumps += 1
current_end = farthest # 更新为下一跳边界
return jumps
核心思想是分层跳跃:每次跳跃覆盖一个范围,在这个范围内贪心地找到下一跳能到达的最远位置,作为新的边界。
面试官:测试一下?
你:用[2,3,1,1,4]走一遍:
- i=0:farthest=2,到达边界→jumps=1,current_end=2(第1跳覆盖0~2)
- i=1:farthest=4,未到边界
- i=2:到达边界→jumps=2,current_end=4(第2跳覆盖3~4,包含终点)
- 结果:2次跳跃 ✅
再测边界情况[0]:直接返回0,正确 ✅
面试官:为什么循环是range(n-1)而不是range(n)?
你:因为我们的目标是到达最后位置,而不是"从"最后位置跳出去。当current_end到达或超过n-1时,就已经能到达终点了,不需要再处理最后一个元素。这也是一个小优化。
高频追问
| 追问 | 应答策略 |
|---|---|
| "还有更优解吗?" | 时间O(n)已经是最优(必须遍历),空间O(1)也是最优,无法再优化。如果数据有特殊性质(如有序、稀疏)可以讨论特殊优化。 |
| "如果数组非常大,无法一次加载到内存?" | 可以流式处理:逐段读取,维护全局的current_end和farthest,核心算法不变。 |
| "能否用DP解决?" | 可以,定义dp[i]为到达i的最少步数,但时间O(n²)、空间O(n),不如贪心。面试中会被追问优化。 |
| "这道题和第66题(能否到达)有什么区别?" | 66题只需一个变量记录max_reach,判断是否>=n-1即可;67题需要分层,用两个变量(边界+最远位置)记录每次跳跃的覆盖范围,这是本质区别。 |
| "如果某个位置值为0怎么办?" | 题目保证能到达终点,所以0不会成为死路。我们的算法通过farthest会跨越0的位置。 |
🎓 知识点总结
Python技巧卡片 🐍
# 技巧1:enumerate同时获取索引和值
for i, val in enumerate(nums):
farthest = max(farthest, i + val)
# 技巧2:切片排除最后元素
for i in range(len(nums) - 1): # 等价于
for i, _ in enumerate(nums[:-1]):
# 技巧3:多变量同时赋值
jumps, current_end, farthest = 0, 0, 0
# 技巧4:提前终止循环
if current_end >= n - 1:
break # 已能到达终点,无需继续
💡 底层原理(选读)
为什么贪心策略是正确的?
这道题的贪心正确性可以用反证法证明:
- 假设存在某个最优解,在第k跳时没有选择"最远可达"位置,而是选了一个较近的位置
- 那么在第k+1跳时,从"较近位置"能到达的范围必然 ⊆ 从"最远位置"能到达的范围
- 这意味着选"最远位置"不会让后续变差,甚至可能让后续更优
- 因此"不选最远"不可能是最优解,矛盾!
贪心 vs DP 的选择:
- DP适用场景:子问题间有复杂依赖,需要保存中间状态
- 贪心适用场景:局部最优能导出全局最优,有"无后效性"
- 本题中,每次选择最远可达位置不会影响之前的决策,符合贪心特征
与第66题的技术区别:
| 维度 | 第66题(能否到达) | 第67题(最少次数) |
|---|---|---|
| 核心变量 | 1个:max_reach | 2个:current_end, farthest |
| 关注点 | 全局最远可达 | 分层边界 |
| 复杂度 | O(n), O(1) | O(n), O(1) |
| 策略 | 单一贪心 | 贪心分层 |
算法模式卡片 📐
- 模式名称:贪心分层跳跃(Greedy Jump with Layering)
- 适用条件:
- 求"最少步数"到达目标
- 每步有多种选择,选择范围由当前状态决定
- 局部最优(选最远)能推导全局最优
- 识别关键词:
- "最少跳跃次数"
- "最多能跳k步"
- "到达最后位置"
- 模板代码:
def min_jumps(nums):
n = len(nums)
if n == 1:
return 0
jumps = 0 # 跳跃次数
current_end = 0 # 当前层边界
farthest = 0 # 下一层最远位置
for i in range(n - 1):
farthest = max(farthest, i + nums[i]) # 贪心更新
if i == current_end: # 到达边界,开启新一层
jumps += 1
current_end = farthest
return jumps
易错点 ⚠️
-
循环范围错误:
- ❌ 错误:
for i in range(n)→ 会多处理最后一个元素 - ✅ 正确:
for i in range(n-1)→ 只需到倒数第二个 - 原因:目标是到达最后位置,不是从最后位置起跳
- ❌ 错误:
-
边界更新时机错误:
- ❌ 错误:在
farthest更新后立即更新current_end - ✅ 正确:只在
i == current_end时更新 - 原因:必须等到遍历完当前层所有位置,才知道下一跳最远能到哪
- ❌ 错误:在
-
跳跃次数初值错误:
- ❌ 错误:
jumps = 1(认为起点算一跳) - ✅ 正确:
jumps = 0 - 原因:起点不算跳跃,只有"从起点跳到其他位置"才算
- ❌ 错误:
-
忘记特判n==1:
- ❌ 错误:直接进入循环
- ✅ 正确:
if n == 1: return 0 - 原因:已在终点,无需跳跃,否则算法会返回错误结果
-
与第66题混淆:
- ❌ 错误:只用一个
max_reach变量 - ✅ 正确:需要
current_end和farthest两个变量 - 原因:67题需要分层计数,66题只需判断可达性
- ❌ 错误:只用一个
🏗️ 工程实战(选读)
这个算法思想在真实项目中的应用,让你知道"学了有什么用"。
-
场景1:网络路由优化 在计算机网络中,数据包从源主机到目标主机需要经过多个路由器。每个路由器有不同的"覆盖范围"(能直接到达的下一跳节点)。这道题的思想可以用于计算"最少跳数路由":贪心地选择每一跳能覆盖范围最大的路由器,减少总跳数,降低延迟。
-
场景2:游戏关卡设计 在跑酷或平台跳跃游戏中,关卡设计师需要确保玩家能通过关卡,并计算"理论最少跳跃次数"作为金牌达成标准。这道题的算法可以自动计算最优路径,辅助难度平衡。
-
场景3:资源分配优化 在云计算资源调度中,任务需要在不同数据中心间迁移。每个数据中心有"辐射范围"(能直接连接的其他中心)。使用贪心分层思想可以最小化任务迁移次数,降低网络开销。
🏋️ 举一反三
完成本课后,试试这些同类题目来巩固知识:
| 题目 | 难度 | 相关知识点 | 提示 |
|---|---|---|---|
| LeetCode 55. 跳跃游戏 | Medium | 贪心,单变量最远可达 | 第66课前置题,只需判断能否到达,比本题简单 |
| LeetCode 1306. 跳跃游戏III | Medium | BFS/DFS,可前可后跳 | 不再是"只能向右",可以双向跳,用BFS求最短路径 |
| LeetCode 1345. 跳跃游戏IV | Hard | BFS+哈希表,相同值跳跃 | 加入了"值相同的位置可互跳",需要哈希表优化 |
| LeetCode 1871. 跳跃游戏VII | Medium | 贪心+前缀和,范围跳跃 | 跳跃范围是区间[minJump, maxJump],需要前缀和优化 |
| LeetCode 1696. 跳跃游戏VI | Medium | 单调队列+DP,求最大得分 | 不是求次数,而是求路径最大分数,用单调队列优化DP |
📝 课后小测
试试这道变体题,不要看答案,自己先想5分钟!
题目:跳跃游戏III - 给定数组arr和起始位置start,每次可以向左或向右跳arr[i]步,判断能否跳到值为0的位置。
例如:arr = [4,2,3,0,3,1,2], start = 5,能否跳到0?
💡 提示(实在想不出来再点开)
这题不再是"只能向右跳",而是"可以双向跳",需要用BFS或DFS遍历所有可达位置,判断能否到达值为0的位置。注意要用visited集合防止重复访问形成死循环。
✅ 参考答案
from collections import deque
def canReach(arr: List[int], start: int) -> bool:
"""
跳跃游戏III:双向跳跃,判断能否到达0
思路:BFS遍历所有可达位置
"""
n = len(arr)
queue = deque([start])
visited = {start}
while queue:
pos = queue.popleft()
# 到达值为0的位置
if arr[pos] == 0:
return True
# 向左跳和向右跳
for next_pos in [pos - arr[pos], pos + arr[pos]]:
if 0 <= next_pos < n and next_pos not in visited:
visited.add(next_pos)
queue.append(next_pos)
return False
# 测试
print(canReach([4, 2, 3, 0, 3, 1, 2], 5)) # True
print(canReach([3, 0, 2, 1, 2], 2)) # False
核心思路:
- 用BFS层序遍历,从
start开始扩展 - 每个位置可以跳到
pos-arr[pos]或pos+arr[pos]两个方向 - 用
visited集合记录访问过的位置,防止死循环 - 一旦遇到
arr[pos]==0就返回True
时间复杂度:O(n),每个位置最多访问一次 空间复杂度:O(n),队列和visited集合
如果这篇内容对你有帮助,推荐收藏 AI Compass:github.com/tingaicompa… 更多系统化题解、编程基础和 AI 学习资料都在这里,后续复习和拓展会更省时间。