1. 跳跃游戏问题解析
今天我们来拆解LeetCode第55题"跳跃游戏",这是一个经典的贪心算法问题。题目描述很简单:给定一个非负整数数组nums,你最初位于数组的第一个位置,数组中的每个元素代表你在该位置可以跳跃的最大长度。判断你是否能够到达最后一个位置。
这个问题看似简单,但蕴含着深刻的算法思想。我第一次做这道题时,花了整整两个小时才理解透彻。现在让我们一起来剖析它的核心逻辑。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 问题分析与解法思路
2.1 暴力解法与优化
最直观的解法是使用递归或回溯,尝试所有可能的跳跃路径。但这种解法的时间复杂度是指数级的,对于较长的数组会非常低效。
javascript复制// 暴力递归解法(不推荐)
function canJump(nums) {
return backtrack(0);
function backtrack(position) {
if (position === nums.length - 1) return true;
const maxJump = Math.min(position + nums[position], nums.length - 1);
for (let nextPos = position + 1; nextPos <= maxJump; nextPos++) {
if (backtrack(nextPos)) return true;
}
return false;
}
}
这个解法虽然直观,但对于长度为1000的数组就会超时。我们需要更聪明的解法。
2.2 贪心算法思路
贪心算法的核心思想是:在每一步选择中都采取当前状态下最优的选择,从而希望导致全局最优的结果。对于跳跃游戏,我们可以这样思考:
- 维护一个变量maxReach,表示当前能够到达的最远位置
- 遍历数组,更新maxReach
- 如果在某个位置i > maxReach,说明无法到达i,自然也到不了终点
- 如果maxReach >= 数组最后一个位置,说明可以到达终点
3. JavaScript实现详解
3.1 基础实现
javascript复制function canJump(nums) {
let maxReach = 0;
for (let i = 0; i < nums.length; i++) {
if (i > maxReach) return false;
maxReach = Math.max(maxReach, i + nums[i]);
if (maxReach >= nums.length - 1) return true;
}
return true;
}
这个实现的时间复杂度是O(n),空间复杂度是O(1),非常高效。
3.2 边界情况处理
在实际编码中,我们需要考虑一些边界情况:
- 空数组:应该返回false还是true?根据题意,空数组应该返回false
- 单元素数组:直接返回true
- 第一个元素为0:如果数组长度>1,则无法移动
修改后的代码:
javascript复制function canJump(nums) {
if (nums.length === 0) return false;
if (nums.length === 1) return true;
let maxReach = 0;
for (let i = 0; i < nums.length; i++) {
if (i > maxReach) return false;
maxReach = Math.max(maxReach, i + nums[i]);
if (maxReach >= nums.length - 1) return true;
}
return false;
}
4. 算法优化与变种
4.1 反向贪心解法
另一种思路是从终点向前回溯,找到能够到达终点的最左边的位置,然后把这个位置作为新的终点,继续向前查找:
javascript复制function canJump(nums) {
let lastPos = nums.length - 1;
for (let i = nums.length - 2; i >= 0; i--) {
if (i + nums[i] >= lastPos) {
lastPos = i;
}
}
return lastPos === 0;
}
这种解法同样高效,有时在特定场景下更直观。
4.2 记录跳跃路径
如果需要记录具体的跳跃路径,我们可以稍作修改:
javascript复制function canJumpWithPath(nums) {
let maxReach = 0;
let lastJumpPos = 0;
const path = [0];
for (let i = 0; i < nums.length; i++) {
if (i > maxReach) return { canJump: false, path: [] };
if (i + nums[i] > maxReach) {
maxReach = i + nums[i];
if (i !== lastJumpPos) {
path.push(i);
lastJumpPos = i;
}
}
if (maxReach >= nums.length - 1) {
if (lastJumpPos !== nums.length - 1) {
path.push(nums.length - 1);
}
return { canJump: true, path };
}
}
return { canJump: true, path };
}
5. 常见错误与调试技巧
5.1 典型错误
- 忽略边界条件:忘记处理空数组或单元素数组的情况
- 循环条件错误:有些同学会写成i <= maxReach,这会导致数组越界
- 提前返回:在maxReach更新前就检查i > maxReach,导致逻辑错误
5.2 调试技巧
当遇到问题时,可以添加一些console.log来观察变量变化:
javascript复制function canJump(nums) {
console.log('Input:', nums);
let maxReach = 0;
for (let i = 0; i < nums.length; i++) {
console.log(`i=${i}, maxReach=${maxReach}`);
if (i > maxReach) {
console.log('Cannot reach position', i);
return false;
}
maxReach = Math.max(maxReach, i + nums[i]);
console.log('Updated maxReach:', maxReach);
if (maxReach >= nums.length - 1) {
console.log('Can reach the end');
return true;
}
}
return true;
}
6. 性能分析与测试用例
6.1 时间复杂度分析
两种解法的时间复杂度都是O(n),因为我们只需要遍历数组一次。空间复杂度都是O(1),只使用了常数级别的额外空间。
6.2 测试用例设计
好的测试用例应该包括:
- 常规情况:[2,3,1,1,4] → true
- 无法到达的情况:[3,2,1,0,4] → false
- 边界情况:
- [] → false
- [0] → true
- [1,0,1] → false
- 大数情况:[10000,0,0,...,0] → true
- 全零情况:[0,0,0,0] → false (长度>1)
javascript复制const testCases = [
{ input: [2,3,1,1,4], expected: true },
{ input: [3,2,1,0,4], expected: false },
{ input: [], expected: false },
{ input: [0], expected: true },
{ input: [1,0,1], expected: false },
{ input: Array(10000).fill(0).concat([10000]), expected: true },
{ input: [0,0,0,0], expected: false }
];
testCases.forEach(({input, expected}, i) => {
const result = canJump(input);
console.log(`Test case ${i+1}:`, result === expected ? 'PASS' : 'FAIL');
});
7. 实际应用场景
跳跃游戏算法虽然简单,但其思想可以应用于:
- 网络路由选择:选择最优的下一跳
- 游戏AI路径规划:判断角色能否到达目标位置
- 资源分配问题:如何用最少的资源覆盖最大范围
- 广告投放策略:选择最优的投放节点
理解这个算法后,你会发现很多实际问题都可以抽象成类似的模型。
8. 扩展思考
8.1 最少跳跃次数问题
LeetCode第45题是这道题的进阶版,要求计算到达终点的最少跳跃次数。我们可以用类似的贪心思路:
javascript复制function jump(nums) {
let jumps = 0;
let currentEnd = 0;
let farthest = 0;
for (let i = 0; i < nums.length - 1; i++) {
farthest = Math.max(farthest, i + nums[i]);
if (i === currentEnd) {
jumps++;
currentEnd = farthest;
}
}
return jumps;
}
8.2 多维跳跃游戏
如果问题扩展到二维甚至多维空间,算法会变得复杂得多。这种情况下通常需要结合动态规划或其他高级算法。
9. 个人经验分享
我在刷这道题时,最初尝试用动态规划来解决,虽然可行但不够高效。后来理解了贪心算法的思路后,才真正体会到算法的美妙之处。几个关键点:
- 不要一开始就追求最优解,先写出可行解再优化
- 画图帮助理解:在纸上画出数组和跳跃路径非常直观
- 多写测试用例,特别是边界情况
- 理解算法背后的思想比记住代码更重要
这道题教会我的是:有时候,局部最优的选择确实能导致全局最优的结果。这种思想在很多算法问题中都适用。
