📌 题目链接:55. 跳跃游戏 - 力扣(LeetCode)
🔍 难度:中等 | 🏷️ 标签:贪心算法、数组、动态规划思想(非DP解法)
⏱️ 目标时间复杂度:O(n)
💾 空间复杂度:O(1)
55 题《跳跃游戏》 是一道经典且极具教学意义的贪心算法入门题。它看似简单,却蕴含了“局部最优 → 全局最优”的核心思想,是面试中高频考察的思维模型之一。
本篇文章将带你彻底吃透这道题:从题目本质、贪心策略的构建逻辑,到代码实现细节、边界处理、复杂度分析,再到面试中可能被追问的变种问题。无论你是初学者还是备战面试的老手,这篇文章都值得你反复阅读!
🔍 题目分析
给定一个非负整数数组 nums,你从下标 0 出发,每个位置 i 的值 nums[i] 表示最多可以向前跳 nums[i] 步(即可以跳到 [i+1, i+nums[i]] 中的任意位置)。
目标:判断是否能到达最后一个下标(即 n-1,n = 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)是性能优化的关键。
🧭 解题思路(分步拆解)
-
初始化:设
rightmost = 0,表示起始位置(下标 0)可达。 -
遍历每个下标
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。
-
-
遍历结束仍未返回 → 说明终点不可达,返回
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!💪
📌 记住:当你在刷题时,不要只看答案,要像写这篇文章一样,深入思考每一步背后的原理、优化空间和面试价值。这才是真正提升算法能力的方式!