给定一个长度为 n 的 0 索引整数数组 nums。初始位置在下标 0。
每个元素 nums[i] 表示从索引 i 向后跳转的最大长度。换句话说,如果你在索引 i 处,你可以跳转到任意 (i + j) 处:
0 <= j <= nums[i]且i + j < n
返回到达 n - 1 的最小跳跃次数。测试用例保证可以到达 n - 1。
示例 1:
输入: nums = [2,3,1,1,4]
输出: 2
解释: 跳到最后一个位置的最小跳跃数是 2。
从下标为 0 跳到下标为 1 的位置,跳 1 步,然后跳 3 步到达数组的最后一个位置。
示例 2:
输入: nums = [2,3,0,1,4]
输出: 2
提示:
1 <= nums.length <= 1040 <= nums[i] <= 1000- 题目保证可以到达
n - 1
1. 生活案例:公交接力挑战
想象你要去一个很远的地方(终点),但你身上没钱,只能靠刷脸坐免费公交。
-
规则:
- 每一站的公交车都有不同的最大行驶距离(数组中的数字)。
- 你可以选择在当前车能到的任何一站下车,换乘那一站的另一辆车。
-
你的目标:用最少的换乘次数到达终点。
-
你的贪心策略:
- 你坐在第一辆车上,眼睛盯着窗外所有你能下车的站点。
- 你心里在盘算: “如果我在 A 站下,下一辆车最远能带我到哪?如果在 B 站下,下一辆又最远能到哪?”
- 你并不急着下车,直到第一辆车快没油了(到达了当前航程的最远边界),你才决定: “好,就在刚才看好的那一站换乘,因为那一站的接力车能跑得最远!”
2. 代码解析与“生活化”注释
代码的核心在于实时维护一个“当前能达到的最远范围”,并在必须换乘时才增加步数。
JavaScript
/**
* @param {number[]} nums - 每一站公交车能跑的最大距离
* @return {number} - 到达终点的最少换乘次数(步数)
*/
var jump = function(nums) {
let n = nums.length;
// 如果只有一站,原地就是终点,跳 0 次
if (n === 1) return 0;
let steps = 0; // 跳跃次数(换乘次数)
let farthest = 0; // 坐在当前车上时,眼光所及能达到的“最远接力点”
let end = 0; // 当前这这一跳能跑到的“边界限制”
// 注意:我们不需要遍历最后一项,因为到达最后一项前,
// 我们已经通过前面的“接力”确保能跳过或恰好落在那了。
for (let i = 0; i < n - 1; i++) {
// 生活化解释:
// 坐在车上,每经过一站 i,都看看这一站的接力车能跑多远 (i + nums[i])
// 永远记录下那个最给力的“接力候选人”
farthest = Math.max(farthest, i + nums[i]);
// 当我走到了当前这辆车的“油尽灯枯”点(边界)
if (i === end) {
// 必须换乘了!
steps = steps + 1;
// 换乘到刚才看好的那个最给力的候选车上,
// 现在的最远边界就变成了那个候选车能跑到的地方
end = farthest;
}
}
return steps;
};
3. 为什么代码这样写?(算法本质)
- 不是动态规划:虽然这题可以用 DP 做,但复杂度是 。用贪心可以降到 。
- 贪心的智慧:我们不是在每一站都跳,而是在不得不跳的时候(到达
end),才选择之前路上看到的潜力最大的那一站进行“虚拟换乘”。 - 局部最优 -> 全局最优:每一次换乘都确保下一次的覆盖范围最大化,从而保证了总跳跃次数最少。
总结
这道题的关键在于区分 farthest(眼光) 和 end(脚步) :
- 你的眼光(
farthest)一直在搜寻最有潜力的下一跳。 - 你的脚步(
end)只有在没路可走时才真正迈出一步,并瞬间跟上眼光的进度。