bigywater.tech
算法与数据结构 · LeetCode 55

LeetCode 55. 跳跃游戏

给定一个非负整数数组,你站在下标 0,每个元素表示从这个位置最多能向前跳的步数,问能否到达最后一个位置。动态规划是 O(n²):逐个位置推出能到的所有位置。贪心是 O(n):从左到右扫描,只维护"目前能到达的最远位置" farthest;如果扫描到的位置 i 已经超过 farthest,说明连这个位置都到不了,直接返回失败;每个能到达的位置都更新 farthest,一旦它够到(或超过)最后一个位置,就成功了。本文用真实数据展示漏掉"到不了"检查的后果,实测贪心和递推的耗时,给出五种语言的实现,并把扫描过程做成分步演示。

20,000
组随机对拍,与暴力解的结果全部一致
6 步
分步演示,每一帧都来自解法的真实执行,另附 5 种语言实现

题意与示例

完整题目见官方页面(链接在文末)。基准解是逐个位置向前推:reach[i] 为真时,把它能到的位置都标为真,是 O(n · 步数) 的另一种思路。一共 14 组用例,覆盖:官方两个示例、只有一个元素、卡在 0 上、一步就能越过 0 的情形:

真实实测 · 本机执行输出
输入 [[2,3,1,1,4]]
输出 true
输入 [[3,2,1,0,4]]
输出 false
输入 [[0]]
输出 true
输入 [[0,1]]
输出 false
输入 [[1,0]]
输出 true
输入 [[2,0,0]]
输出 true

陷阱:忘了检查"当前位置能不能到"

如果只是把所有位置的 i + nums[i] 取最大值,最后比较是否够到终点,就忽略了"某个位置根本到不了"这一点:例如 [3, 2, 1, 0, 4],从下标 0 能到下标 3,但下标 3 的值是 0,走不出去;下标 4 的值 4 让最大值变成 8,看起来能到终点,可是下标 4 本身就到不了。下面是这种写法与 solve 的真实结果:

真实实测 · 本机执行输出
# 没有「位置 i 已经到不了」检查的写法
[3, 2, 1, 0, 4]: 只看最终 farthest -> True    solve -> False
[0, 5]: 只看最终 farthest -> True    solve -> False
[1, 0, 5]: 只看最终 farthest -> True    solve -> False
[2, 3, 1, 1, 4]: 只看最终 farthest -> True    solve -> True
随机 20000 个数组:这种写法与正确答案不同的有 12221 个(61.1%)

[3, 2, 1, 0, 4]、[0, 5]、[1, 0, 5] 三组都被错误地判为 True,正确答案是 False。在我用的随机数据里(取值偏向 0,让"到不了"的情形占很大比例),这种写法与正确答案不同的有 12221 组,占 61.1%;这个比例取决于数据里 0 的多少,不同分布下会不同。正确的写法是在扫描每个位置时先检查 i > farthest。

贪心:五种语言的实现

farthest 从 0 开始。对每个位置 i:如果 i > farthest,返回 False;否则 farthest = max(farthest, i + nums[i]);如果 farthest >= n - 1,返回 True。Python、Java、Go、JavaScript、C++ 都是真实编译运行的。

Python · 解法代码def solve(nums): farthest = 0 # 目前能到达的最远位置 for i, step in enumerate(nums): if i > farthest: # 连位置 i 都到不了,后面更到不了 return False farthest = max(farthest, i + step) if farthest >= len(nums) - 1: # 已经能到达(或越过)最后一个位置 return True return True
真实实测 · Python · 前几组用例的输出
$ python3 solution.py   # 统一入口:逐行读取 cases.jsonl,调用上面的 solve,打印 JSON
[[2,3,1,1,4]]  →  true
[[3,2,1,0,4]]  →  false
[[0]]  →  true
[[0,1]]  →  false
[[1,0]]  →  true
[[2,0,0]]  →  true
Java · 解法代码class Solution { public boolean canJump(int[] nums) { int farthest = 0; // 目前能到达的最远位置 for (int i = 0; i < nums.length; i++) { if (i > farthest) return false; // 连位置 i 都到不了,后面更到不了 farthest = Math.max(farthest, i + nums[i]); if (farthest >= nums.length - 1) return true; // 已经能到达(或越过)最后一个位置 } return true; } }
真实实测 · Java · 前几组用例的输出
$ javac -d build Main.java Solution.java && java -cp build Main < cases.jsonl
[[2,3,1,1,4]]  →  true
[[3,2,1,0,4]]  →  false
[[0]]  →  true
[[0,1]]  →  false
[[1,0]]  →  true
[[2,0,0]]  →  true
Go · 解法代码func canJump(nums []int) bool { farthest := 0 // 目前能到达的最远位置 for i, step := range nums { if i > farthest { // 连位置 i 都到不了,后面更到不了 return false } if i+step > farthest { farthest = i + step } if farthest >= len(nums)-1 { // 已经能到达(或越过)最后一个位置 return true } } return true }
真实实测 · Go · 前几组用例的输出
$ go build -o bin . && ./bin < cases.jsonl
[[2,3,1,1,4]]  →  true
[[3,2,1,0,4]]  →  false
[[0]]  →  true
[[0,1]]  →  false
[[1,0]]  →  true
[[2,0,0]]  →  true
JavaScript · 解法代码function canJump(nums) { let farthest = 0; // 目前能到达的最远位置 for (let i = 0; i < nums.length; i++) { if (i > farthest) return false; // 连位置 i 都到不了,后面更到不了 farthest = Math.max(farthest, i + nums[i]); if (farthest >= nums.length - 1) return true; // 已经能到达(或越过)最后一个位置 } return true; }
真实实测 · JavaScript · 前几组用例的输出
$ node run.js < cases.jsonl
[[2,3,1,1,4]]  →  true
[[3,2,1,0,4]]  →  false
[[0]]  →  true
[[0,1]]  →  false
[[1,0]]  →  true
[[2,0,0]]  →  true
C++ · 解法代码#include <algorithm> #include <vector> using namespace std; class Solution { public: bool canJump(vector<int>& nums) { int farthest = 0; // 目前能到达的最远位置 for (int i = 0; i < (int)nums.size(); i++) { if (i > farthest) return false; // 连位置 i 都到不了,后面更到不了 farthest = max(farthest, i + nums[i]); if (farthest >= (int)nums.size() - 1) return true; // 已经能到达(或越过)最后一个位置 } return true; } };
真实实测 · C++ · 前几组用例的输出
$ clang++ -std=c++17 -O2 -o bin sol.cpp && ./bin < cases.jsonl
[[2,3,1,1,4]]  →  true
[[3,2,1,0,4]]  →  false
[[0]]  →  true
[[0,1]]  →  false
[[1,0]]  →  true
[[2,0,0]]  →  true
真实实测 · 本机执行输出
共 14 组用例(含边界),以暴力解的结果为基准逐组比对:
  Python      通过 14/14
  Java        通过 14/14
  Go          通过 14/14
  JavaScript  通过 14/14
  C++         通过 14/14

耗时实测

数组长度分别为 1000、3000、1 万、100 万,每个元素是 1 到 3 的伪随机数(保证能到达终点),每个规模取三次中最快的一次:

真实实测 · 本机执行输出
数组长度 n(每个元素 1 到 3,能到达) | 暴力解 | 本文解法
      1000 |     0.130 ms | 0.068 ms
      3000 |     0.374 ms | 0.211 ms
     10000 |     1.283 ms | 0.723 ms
   1000000 |      (未测,太慢) | 75.142 ms

贪心与递推两种做法在这批数据上都是线性增长(1000 到 1 万,贪心 0.068ms 到 0.723ms,约 10.6 倍),并且差距不大:因为每个元素的步数很小(1 到 3),递推的内层循环很短,退化不成平方,贪心的优势没有体现出来。如果步数很大,递推的内层循环会变长,那时才能看出差距,这里没有实测。100 万个元素时贪心用了 75.1ms。

随机对拍

Python 版本另外和递推做了随机对拍,数组长度 1 到 12,取值偏向 0/1/2/3,能到达和到不了的各占一部分:

真实实测 · 本机执行输出
随机对拍 20000 组(长度 1 到 12、取值偏向 0/1/2/3 的随机数组(0 出现得很多,这样「到不了」的情形也占一半左右)),solve 与 brute 的结果全部一致。

另外四种语言只在上面 14 组固定用例上验证过,没有做大规模随机对拍。

分步演示:farthest 怎么推进

下面的演示用官方的第二个示例 [3, 2, 1, 0, 4]。i = 0 时 farthest 变成 3,之后每个位置最远都只能到 3(i + nums[i] 都是 3),farthest 停在 3;扫描到 i = 4 时,它已经超过 farthest = 3,位置 4 到不了,返回 False。每一帧都是把上面的解法在这组输入上真实执行记录下来的,运行器还检查了演示的最终结果与 solve 的返回值一致。

跳跃游戏:[3, 2, 1, 0, 4]
当前位置 i当前已知能到达的范围

    参考与说明

    • 官方题目页:LeetCode 55. 跳跃游戏。本文只用自己的话概括了题意,完整约束以官方页面为准。
    • 数据来自本机真实运行,五种语言的版本写在页脚;耗时对比只用 Python 测量。
    • 没有覆盖:从后往前的贪心写法(维护"最左的能到达终点的位置");步数很大时递推的耗时。求最少跳数见第 45 题。
    京ICP备2026067502号-1京公网安备11010502063487号