1. 问题背景与核心挑战
跳跃游戏II是LeetCode上经典的贪心算法练习题(编号45),题目要求在一个非负整数数组中,从第一个元素出发,每个元素的值代表你能跳跃的最大长度,求到达最后一个位置的最少跳跃次数。这个问题看似简单,但隐藏着几个关键挑战:
- 时间复杂度陷阱:暴力解法需要O(n^2)时间复杂度,在LeetCode上会直接超时
- 贪心选择证明:为什么局部最优能保证全局最优?这是算法正确性的核心
- 边界条件处理:当数组长度为1时如何处理?遇到0值时的跳跃策略是什么?
我在第一次刷这道题时,曾用动态规划方法实现,结果在测试用例[25000,24999,24998,...,3,2,1,0]上直接超时。这促使我深入研究贪心算法的优化方案。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 贪心算法框架解析
2.1 基本思路与双指针策略
贪心算法的核心在于:在每一步都选择能让你跳得最远的选项。具体实现时需要维护三个关键变量:
java复制int jumps = 0; // 跳跃次数
int currentEnd = 0; // 当前跳跃能到达的最远位置
int farthest = 0; // 所有可能位置中最远的可达位置
算法流程如下:
- 遍历数组(不访问最后一个元素)
- 更新当前能到达的最远位置(farthest)
- 当到达当前跳跃边界时,必须进行一次跳跃
关键点:为什么不需要访问最后一个元素?因为当currentEnd >= nums.length-1时就已经到达终点,不需要额外跳跃。
2.2 Java实现代码详解
java复制public int jump(int[] nums) {
int jumps = 0, currentEnd = 0, farthest = 0;
for (int i = 0; i < nums.length - 1; i++) {
farthest = Math.max(farthest, i + nums[i]);
if (i == currentEnd) {
jumps++;
currentEnd = farthest;
if (currentEnd >= nums.length - 1) break;
}
}
return jumps;
}
这段代码有几个精妙之处:
- 循环终止条件是
i < nums.length - 1而非i < nums.length if (currentEnd >= nums.length - 1) break是重要优化,提前终止不必要的计算- 时间复杂度O(n),空间复杂度O(1),是最优解
3. 算法正确性证明
3.1 数学归纳法验证
要证明贪心选择的正确性,可以用数学归纳法:
- 基本情况:当n=1时,不需要跳跃,算法返回0,正确
- 归纳假设:假设对于前k步都做出了最优选择
- 归纳步骤:在第k+1步,算法选择能到达最远位置的跳跃点,这保证了后续需要的跳跃次数最少
3.2 反证法说明
假设存在一个更优解,其某次跳跃没有选择最远可达位置:
- 那么后续必然需要更多跳跃来弥补这个"短跳"
- 这与"最少跳跃次数"的前提矛盾
- 因此贪心选择是正确的
4. 边界条件与特殊测试用例
4.1 典型测试用例分析
| 测试用例 | 预期结果 | 说明 |
|---|---|---|
| [2,3,1,1,4] | 2 | 标准案例 |
| [2,3,0,1,4] | 2 | 包含0值 |
| [1] | 0 | 单元素数组 |
| [1,2,3] | 2 | 连续递增 |
| [3,2,1,0,4] | -1 | 无法到达(但题目保证可解) |
4.2 实际编码中的坑点
- 数组越界:当nums.length=1时,循环条件要正确处理
- 零值处理:遇到0时,必须确保之前有足够的跳跃能力跳过这个位置
- 提前终止:当currentEnd已经≥n-1时要及时break,否则会多算跳跃次数
5. 算法优化与变种
5.1 性能优化技巧
-
反向查找法:从终点向前找能到达它的最近位置
java复制int position = nums.length - 1; int steps = 0; while (position != 0) { for (int i = 0; i < position; i++) { if (i + nums[i] >= position) { position = i; steps++; break; } } } return steps;虽然时间复杂度仍是O(n^2),但在某些特定场景下可能更快
-
BFS思路:将问题视为层级遍历,每层代表一次跳跃
java复制int level = 0, currentMax = 0, nextMax = 0, i = 0; while (currentMax < nums.length - 1) { level++; for (; i <= currentMax; i++) { nextMax = Math.max(nextMax, nums[i] + i); } currentMax = nextMax; } return level;
5.2 相关题目拓展
- 跳跃游戏I(LeetCode 55):判断是否能到达终点
- 视频拼接(LeetCode 1024):类似的区间覆盖问题
- 加油站问题(LeetCode 134):环形数组的贪心选择
6. 面试实战技巧
在面试中遇到这道题时,建议采用以下回答策略:
-
先陈述暴力解法:展示对问题的基本理解
"最直观的解法是用动态规划,dp[i]表示到达i位置的最少跳跃次数,但这样会有O(n^2)时间复杂度" -
引出贪心优化:展示算法优化能力
"通过观察可以发现,我们只需要维护当前能到达的最远位置,这样可以将复杂度降到O(n)" -
手写代码:写出标准解法后主动讨论
- 边界条件处理
- 循环终止条件的设置原因
- 提前终止的优化点
-
主动提出测试用例:展示全面的思维
- 正常案例
- 单元素数组
- 包含0的数组
- 全1数组
-
延伸讨论:展示知识广度
- 与动态规划的时间复杂度对比
- 贪心算法的适用场景
- 相关题目的共通之处
7. 个人实战经验分享
在刷这道题的过程中,我总结了几个关键经验:
-
调试技巧:当算法出错时,可以用小数组手动模拟执行过程。例如对于[2,3,1,1,4]:
- i=0: farthest=2, currentEnd=0 → jump=1, currentEnd=2
- i=1: farthest=max(2,1+3)=4
- i=2: farthest保持不变,i==currentEnd → jump=2, currentEnd=4
- 此时currentEnd≥4,提前退出
-
常见错误模式:
- 忘记处理单元素数组情况
- 循环条件写成i<nums.length导致额外跳跃
- 没有及时更新currentEnd导致重复计数
-
性能优化感悟:
- 贪心算法常常能优化掉不必要的计算
- 在LeetCode上,从400ms优化到1ms的感觉非常美妙
- 算法优化的本质是发现并消除冗余计算
这道题之所以被列为热题100,是因为它完美展现了贪心算法的精髓——通过局部最优选择达到全局最优。掌握这类问题的解法,对培养算法思维很有帮助。建议读者可以尝试用相同思路解决LeetCode 1024(视频拼接),体会算法的通用性。
