【LeetCode Hot100 刷题日记 (78/100)】55. 跳跃游戏 —— 贪心算法、数组、动态规划思想(非DP解法) 🧠

5 阅读5分钟

📌 题目链接:55. 跳跃游戏 - 力扣(LeetCode)

🔍 难度:中等 | 🏷️ 标签:贪心算法、数组、动态规划思想(非DP解法)

⏱️ 目标时间复杂度:O(n)

💾 空间复杂度:O(1)


55 题《跳跃游戏》 是一道经典且极具教学意义的贪心算法入门题。它看似简单,却蕴含了“局部最优 → 全局最优”的核心思想,是面试中高频考察的思维模型之一。

本篇文章将带你彻底吃透这道题:从题目本质、贪心策略的构建逻辑,到代码实现细节、边界处理、复杂度分析,再到面试中可能被追问的变种问题。无论你是初学者还是备战面试的老手,这篇文章都值得你反复阅读!


🔍 题目分析

给定一个非负整数数组 nums,你从下标 0 出发,每个位置 i 的值 nums[i] 表示最多可以向前跳 nums[i](即可以跳到 [i+1, i+nums[i]] 中的任意位置)。

目标:判断是否能到达最后一个下标(即 n-1n = nums.size())。

✅ 关键观察:

  • 不需要求出具体路径,只需判断可达性
  • 每一步的选择会影响后续的可达范围。
  • 如果某个位置无法被前面任何位置“覆盖”,则后续所有位置都不可达。

💡 面试提示:这类“是否可达”问题,往往可以用贪心BFS/DFS解决。但本题数据规模大(n ≤ 1e4),DFS/BFS 会超时或爆栈,必须用 O(n) 解法。


🎯 核心算法及代码讲解 —— 贪心算法(Greedy)

🌟 贪心策略的核心思想:

维护一个变量 rightmost,表示当前能够到达的最远下标。

我们遍历数组中的每一个位置 i

  • 如果 i <= rightmost,说明当前位置可达
  • 那么从 i 出发,最远能跳到 i + nums[i]
  • 于是我们更新 rightmost = max(rightmost, i + nums[i])
  • 一旦 rightmost >= n - 1,立即返回 true

如果遍历完所有位置,rightmost 仍未达到 n-1,则返回 false

❓ 为什么这个贪心策略是正确的?

  • 贪心选择性质:在每一步,我们都尽可能扩展可达范围(取最大值),这不会导致错过更优解。因为“能跳得更远”永远不劣于“跳得近”。
  • 最优子结构:能否到达终点,只取决于“当前最远能到哪”,与中间路径无关。

🧩 类比理解:想象你在一条路上开车,油箱容量随地点变化。rightmost 就是你当前能开到的最远加油站。只要在到达某个加油站前没断油(i <= rightmost),就能加满继续前进。

💻 C++ 核心算法代码(带详细行注释)

class Solution {
public:
    bool canJump(vector<int>& nums) {
        int n = nums.size();
        int rightmost = 0; // 初始化最远可达位置为 0(起点)
        
        for (int i = 0; i < n; ++i) {
            // 只有当当前位置 i 是可达的,才考虑从这里跳
            if (i <= rightmost) {
                // 更新最远可达位置:max(当前最远, 从 i 能跳到的最远)
                rightmost = max(rightmost, i + nums[i]);
                
                // 剪枝优化:一旦能到达或超过最后一个下标,直接返回 true
                if (rightmost >= n - 1) {
                    return true;
                }
            }
            // 注意:如果 i > rightmost,说明当前位置不可达,后续也不可能到达
            // 但由于循环继续,最终会返回 false
        }
        return false;
    }
};

关键点

  • 条件 if (i <= rightmost)可达性判断,避免从不可达位置“凭空跳跃”。
  • rightmost 单调不减,因此只需一次遍历。
  • 提前终止(early termination)是性能优化的关键。

🧭 解题思路(分步拆解)

  1. 初始化:设 rightmost = 0,表示起始位置(下标 0)可达。

  2. 遍历每个下标 i(从 0 到 n-1):

    • 步骤 A:检查 i 是否在当前可达范围内(i <= rightmost)。

      • 若否 → 跳过(实际意味着已断连,但继续循环无妨)。
      • 若是 → 进入步骤 B。
    • 步骤 B:计算从 i 能跳到的最远位置 i + nums[i]

    • 步骤 C:更新全局最远可达位置 rightmost = max(rightmost, i + nums[i])

    • 步骤 D:若 rightmost >= n-1,说明终点可达,立即返回 true

  3. 遍历结束仍未返回 → 说明终点不可达,返回 false


📊 算法分析

项目分析
时间复杂度O(n) :仅需一次遍历数组,每个元素访问一次。
空间复杂度O(1) :仅使用常数个额外变量(rightmost, i, n)。
是否原地操作是,无需额外数组或递归栈。
稳定性算法逻辑清晰,无浮点、无随机,结果确定。
适用场景所有“最大覆盖范围”、“可达性判断”类问题(如加油站、区间覆盖等)。

💼 面试加分项

  • 能解释为何不用 DFS/BFS(指数级 vs 线性)。
  • 能说出贪心成立的两个条件(贪心选择 + 最优子结构)。
  • 能对比 DP 解法(虽然本题 DP 是 O(n²),但可作为备选思路)。

💻 完整可运行代码

✅ C++ 版本

#include <bits/stdc++.h>
using namespace std;
using ll = long long;

class Solution {
public:
    bool canJump(vector<int>& nums) {
        int n = nums.size();
        int rightmost = 0;
        for (int i = 0; i < n; ++i) {
            if (i <= rightmost) {
                rightmost = max(rightmost, i + nums[i]);
                if (rightmost >= n - 1) {
                    return true;
                }
            }
        }
        return false;
    }
};

// 测试
signed main(){
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    cout.tie(nullptr);
    
    Solution sol;
    
    // 测试用例 1
    vector<int> nums1 = {2,3,1,1,4};
    cout << "Test 1: " << (sol.canJump(nums1) ? "true" : "false") << "\n"; // true
    
    // 测试用例 2
    vector<int> nums2 = {3,2,1,0,4};
    cout << "Test 2: " << (sol.canJump(nums2) ? "true" : "false") << "\n"; // false
    
    // 边界测试:单个元素
    vector<int> nums3 = {0};
    cout << "Test 3: " << (sol.canJump(nums3) ? "true" : "false") << "\n"; // true
    
    // 边界测试:全零(除第一个)
    vector<int> nums4 = {1,0,0,0};
    cout << "Test 4: " << (sol.canJump(nums4) ? "true" : "false") << "\n"; // false
    
    return 0;
}

✅ JavaScript 版本

/**
 * @param {number[]} nums
 * @return {boolean}
 */
var canJump = function(nums) {
    const n = nums.length;
    let rightmost = 0;
    
    for (let i = 0; i < n; i++) {
        if (i <= rightmost) {
            rightmost = Math.max(rightmost, i + nums[i]);
            if (rightmost >= n - 1) {
                return true;
            }
        }
    }
    return false;
};

// 测试
console.log(canJump([2,3,1,1,4])); // true
console.log(canJump([3,2,1,0,4])); // false
console.log(canJump([0]));         // true
console.log(canJump([1,0,0,0]));   // false

🌟 本期完结,下期见!🔥

👉 点赞收藏加关注,新文更新不迷路。关注专栏【算法】LeetCode Hot100刷题日记,持续为你拆解每一道热题的底层逻辑与面试技巧!

💬 欢迎留言交流你的解法或疑问!一起进步,冲向 Offer!💪


📌 记住:当你在刷题时,不要只看答案,要像写这篇文章一样,深入思考每一步背后的原理、优化空间和面试价值。这才是真正提升算法能力的方式!