LeetCode 45. 跳跃游戏 II 深度解析:两种贪心思路与代码优化

87 阅读7分钟

在 LeetCode 的贪心算法专题中,“跳跃游戏 II” 是极具代表性的题目。它不仅要求我们判断能否到达终点,更需要找到最少的跳跃次数,对贪心策略的精准应用提出了更高要求。本文将详细拆解两种贪心思路的实现代码,分析其核心逻辑、存在的问题,并给出优化方案,帮助大家彻底掌握这道经典题目。

一、题目回顾:明确核心需求

给定一个长度为 n 的 0 索引整数数组 nums,初始位置在下标 0。每个元素 nums[i] 表示从索引 i 向后跳转的最大长度(即可以跳转到 i+j 处,其中 0 ≤ j ≤ nums[i] 且 i+j < n)。题目保证可以到达终点 n-1,要求返回到达终点的最小跳跃次数。

核心难点:如何通过“局部最优选择”实现“全局最优结果”——每一步都选择能让后续跳跃更高效的落点,最终实现最少跳跃次数。

二、解法一:逐步选最优落点(暴力贪心优化版)

1. 核心思路

从当前位置出发,遍历所有可到达的落点,选择“能让下一步跳得最远”的落点作为本次跳跃的目标。通过这种“每一步都选最优”的贪心策略,最终实现全局最少跳跃次数。

具体逻辑:

  • 记录当前位置 cur,以及跳跃次数 count;

  • 遍历 cur 可到达的所有范围(cur+1 到 cur+nums[cur]),计算每个落点 j 的“潜在最远可达位置”;

  • 选择潜在最远可达位置对应的落点作为新的 cur,跳跃次数 count+1;

  • 重复上述过程,直到到达终点。

2. 代码详解(jump_1)


function jump_1(nums: number[]): number {
  const numsLen = nums.length;
  if (numsLen === 1) return 0; // 特殊情况:数组长度为1,无需跳跃
  let count = 0; // 记录跳跃次数
  let cur = 0;   // 当前所在位置
  while (cur < numsLen - 1) { // 未到达终点时循环
    let maxNextReach = 0; // 下一步能到达的最远位置
    let aft = cur;        // 本次跳跃的目标落点
    // 遍历当前位置的所有可跳落点
    for (let j = cur + 1; j <= cur + nums[cur]; j++) {
      if (j === numsLen - 1) {
        return count + 1; // 提前终止:当前落点已是终点,直接返回次数+1
      }
      // 计算落点j的潜在最远可达位置,更新最优落点
      if (j - cur + nums[j] >= maxNextReach) {
        maxNextReach = j - cur + nums[j];
        aft = j;
      }
    }
    if (aft === cur) return -1; // 防御性判断(题目保证可达,实际不触发)
    cur = aft; // 跳转到最优落点
    count++;   // 跳跃次数递增
  }
  return count;
};

3. 关键细节说明

  • 提前终止逻辑:当遍历到的落点 j 已是终点(j === numsLen - 1)时,直接返回 count+1。因为从当前位置跳一步就能到达终点,无需继续后续遍历,提升效率;

  • 最优落点判断:核心是比较 j 的潜在最远可达位置(代码中 j - cur + nums[j] 等价于 j + nums[j],j - cur 是本次跳跃的步数,不影响最终结果),确保每次跳跃都能为后续铺垫最远的可达范围;

  • 特殊情况处理:数组长度为 1 时,初始位置就是终点,直接返回 0。

4. 优缺点分析

优点:逻辑直观,符合“找最优落点”的直觉,容易理解和调试;

缺点:存在嵌套循环(while 外层循环 + for 内层循环),时间复杂度为 O(n²),在数组长度较大时(如 n=10⁴)会出现超时问题,效率较低。

三、解法二:边界贪心(优化方向,需修正)

1. 核心思路

通过“边界”划分每一次跳跃的范围:用 border 表示当前跳跃能到达的最远边界,用 maxReach 表示遍历过程中能到达的全局最远位置。当遍历到 border 时,说明当前跳跃的范围已遍历完毕,需要进行一次跳跃(count+1),并将 border 更新为 maxReach。通过这种方式,用单层循环实现贪心策略,提升效率。

2. 原代码问题分析(jump_2)


function jump_2(nums: number[]): number {
  const numsLen = nums.length;
  if (numsLen === 1) return 0;
  let count = 0;
  let maxIndex = 0; // 表示当前步数可走的区域(冗余变量,未实际使用)
  let border = 0;
  let maxReach = 0;
  for (let i = 0; i <= maxReach; i++) { // 循环条件依赖动态变量,存在逻辑漏洞
    if (i > border) { // 遍历超边界时更新
      border = maxReach;
      count++;
    }
    if (i === numsLen - 1) return count; // 提前返回,未统计最后一次跳跃
    if (i + nums[i] >= maxReach) {
      maxIndex = i;
      maxReach = i + nums[i];
    }
  }
  return count;
};

3. 核心问题拆解

  • 循环条件不合理:for 循环的终止条件是 i ≤ maxReach,而 maxReach 是动态更新的。当遇到类似 [1,2] 的用例时,会出现“到达终点但未统计跳跃次数”的问题;

  • 提前返回逻辑错误:当 i === numsLen - 1 时直接返回 count,但此时可能尚未触发“边界更新”,导致跳跃次数少算一次。例如用例 [1,2],i=1 时到达终点,直接返回 count=0,正确结果应为 1;

  • 冗余变量:maxIndex 被定义为“当前步数可走的区域”,但代码中未实际使用,增加了逻辑复杂度。

4. 修正后的最优代码


function jump_2_optimized(nums: number[]): number {
  const numsLen = nums.length;
  if (numsLen === 1) return 0;
  let count = 0;        // 跳跃次数
  let border = 0;       // 当前跳跃的边界
  let maxReach = 0;     // 全局最远可达位置
  // 遍历到倒数第二个元素:最后一个元素无需跳跃
  for (let i = 0; i < numsLen - 1; i++) {
    maxReach = Math.max(maxReach, i + nums[i]); // 更新全局最远可达位置
    if (i === border) { // 遍历到当前边界,触发跳跃
      count++;
      border = maxReach; // 更新边界为全局最远可达位置
      if (border >= numsLen - 1) break; // 提前终止:边界已覆盖终点
    }
  }
  return count;
};

5. 修正点说明

  • 循环范围调整:将 for 循环条件改为 i < numsLen - 1,避免遍历到终点(最后一个元素无需跳跃);

  • 边界触发逻辑优化:当 i === border 时,说明当前跳跃范围已遍历完毕,此时更新 count 和 border,确保每一次跳跃都被正确统计;

  • 提前终止优化:若更新后的 border 已覆盖终点(border >= numsLen - 1),直接跳出循环,减少无效遍历;

  • 删除冗余变量:移除未使用的 maxIndex,简化逻辑。

四、两种解法对比

解法类型时间复杂度空间复杂度核心优势适用场景
逐步选最优落点(jump_1)O(n²)O(1)逻辑直观,易理解调试小规模数组,学习理解贪心思路
边界贪心(修正后 jump_2_optimized)O(n)O(1)效率最优,空间占用少大规模数组,算法面试最优解

五、核心总结与拓展思考

1. 贪心算法核心精髓

本题的两种思路均围绕“局部最优→全局最优”:要么选择“当前落点中后续能跳最远的”,要么选择“当前跳跃范围的最远边界”,本质都是通过每一步的最优选择,最终实现最少跳跃次数。

2. 必掌握的关键细节

  • 特殊情况处理:数组长度为 1 时直接返回 0;

  • 遍历范围控制:避免遍历到终点(最后一个元素无需跳跃);

  • 提前终止:当边界覆盖终点时直接跳出循环,提升效率。

3. 拓展思考

  • 若题目不保证“一定能到达终点”,如何修改代码?需增加判断:若 maxReach ≤ i 且未到达终点,说明无法继续前进,返回 -1;

  • 动态规划 vs 贪心:本题也可通过动态规划(dp[i] 表示到达 i 的最少跳跃次数)实现,但时间复杂度为 O(n²),空间复杂度为 O(n),远不如贪心算法高效。

通过本文的解析,相信大家已经掌握了“跳跃游戏 II”的两种核心思路及优化技巧。贪心算法的关键在于找到“局部最优”的判断标准,多练习、多拆解类似题目,就能逐步提升对贪心策略的敏感度~