从零点检查到贪心覆盖范围
本文分析你提供的 canJump 初版和改进版,结合题目约束说明其中的正确性问题,并给出更稳定的贪心实现。核心结论是:能否到达终点不需要统计零的数量,而应维护当前已经能够覆盖到的最远下标。
官方题目: 55. 跳跃游戏。
一 题目模型与约束
给定一个非负整数数组 nums,起点是下标 0。numsi 表示从下标 i 最多可以向右跳多少步。只要存在一条路径到达最后一个下标,就返回 true,否则返回 false。
|--------|--------------------------------|
| 项目 | 题目规则 |
| 数组长度 | 1 <= nums.length <= 10^4 |
| 元素范围 | 0 <= numsi <= 10^5 |
| 移动方向 | 只能向右移动 |
| 跳跃含义 | 可以跳 1 到 numsi 步,也可以不超过这个上限 |
| 目标 | 判断最后一个下标是否可达 |
边界要特别注意:长度为 1 时,起点本身就是终点,即使 nums0 == 0,也应该返回 true。另一个常见误区是把"最多跳 k 步"理解成"必须跳超过 k 步";实际距离等于 numsj 时同样可以到达。
二 你的代码演进
|--------|-------------------------|-------------------------|
| 版本 | 主要策略 | 评价 |
| 初版 | 逐个查找零点,判断此前是否存在能够跨过它的位置 | 思路接近可行,但使用了严格大于,且边界不完整 |
| 改进版 | 去掉零点计数,直接对每个零点回溯检查 | 结构更简单,但核心严格大于问题仍然存在 |
| 推荐版 | 一次扫描维护最远可达位置 | O(n) 时间、O(1) 额外空间,逻辑更直接 |
三 初版分析
bool canJump(int* nums, int numsSize) {
if (nums0 == 0) return false;
for (int i = 0; i < numsSize; i++) {
if (numsi == 0) {
int p = 0, att = 0;
for (int j = i - 1; j >= 0; j--) {
att = i - j;
if (numsj > att) {
p++;
}
}
if (p == 0) return false;
}
}
return true;
}
初版已经抓住了一个重要事实:中间的零点可能成为阻塞点,只有存在某个此前位置能够越过它,路径才不会被截断。但实现仍有以下问题。
|--------------------------|-----------------|----------------------------|
| 问题 | 最小反例 | 影响 |
| 严格大于写成 >,排除了恰好能跳到的情况 | 1, 0 | 错误返回 false,正确答案是 true |
| 单元素数组直接检查 nums0 == 0 | 0 | 错误返回 false,起点同时是终点 |
| 检查的是所有零点,包括终点 | 1, 0 | 终点零值不代表失败,只要能到达即可 |
| 重复回溯检查 | 全是零点的长数组 | 最坏时间复杂度接近 O(n^2) |
| 独立 C 文件缺少 stdbool.h | 任何使用 bool 的编译单元 | 可能无法通过编译;应包含 <stdbool.h> |
其中最关键的修正是把 numsj > i - j 改为 numsj >= i - j。题目允许最大跳跃长度恰好等于目标距离。

图 1 1, 0 中,距离为 1,nums0 == 1 已经足够到达终点。
四 改进版分析
bool canJump(int* nums, int numsSize) {
if (nums0 == 0) return false;
for (int i = 0; i < numsSize; i++) {
if (numsi == 0) {
int p = 0, att = 0;
for (int j = i - 1; j >= 0; j--) {
att = i - j;
if (numsj > att) {
p++;
}
}
if (p == 0) return false;
}
}
return true;
}
改进版删除了 p、l、b 等全局计数逻辑,代码更容易读懂,但本质算法没有改变:仍然对每个零点向前搜索,并且仍然使用 numsj > att。因此 1, 0 和 0 这两个边界反例仍会失败。
|---------|------------------|--------------------------------|
| 检查项 | 改进版现状 | 结论 |
| 零点前驱搜索 | 仍然从 i - 1 向前检查 | 逻辑方向正确,但可被一次扫描替代 |
| 距离判断 | numsj > att | 错误,应为 numsj >= att |
| 起点为零 | 直接返回 false | 对 0 错误,应该先判断 numsSize == 1 |
| 非零数组 | 直接返回 true | 通常正确,但仍不如覆盖范围法直观 |
| 复杂度 | 最坏情况下嵌套回溯 | O(n^2) |
五 推荐的贪心抽象
从左到右扫描数组,维护变量 farthest,表示当前已经确认可达的最远下标。扫描到 i 时,如果 i > farthest,说明当前位置根本无法到达,答案立即是 false。否则用 i + numsi 更新最远覆盖范围。只要 farthest >= numsSize - 1,就可以返回 true。

图 2 贪心算法只维护一个最远可达边界,并不断扩展它。
#include <stdbool.h>
bool canJump(int* nums, int numsSize) {
int farthest = 0;
for (int i = 0; i < numsSize; ++i) {
if (i > farthest) {
return false;
}
if (i + numsi > farthest) {
farthest = i + numsi;
}
if (farthest >= numsSize - 1) {
return true;
}
}
return true;
}
六 正确性证明
不变量:扫描到下标 i 之前,farthest 等于从起点出发、经过已处理位置能够到达的最远下标。
- 若 i > farthest,则所有已确认可达的位置都无法跳到 i 或更右边,因此 i 不可达,返回 false 是正确的。
- 若 i <= farthest,则 i 可达;从 i 出发最多可以到达 i + numsi,更新 farthest 不会漏掉任何更远的可达位置。
- 若 farthest >= numsSize - 1,最后一个下标已经在覆盖范围内,因此返回 true。
扫描结束仍未触发失败时,所有被扫描到的位置都可达。由于数组的最后一个位置也在扫描范围内,函数返回 true。
七 复杂度对比
|--------|------------|----------|------------------|
| 方法 | 时间复杂度 | 额外空间 | 说明 |
| 初版 | 最坏 O(n^2) | O(1) | 每遇到零点都向前回溯 |
| 改进版 | 最坏 O(n^2) | O(1) | 删除计数变量,但没有改变嵌套搜索 |
| 贪心版 | O(n) | O(1) | 每个位置只访问一次 |
八 测试矩阵
|--------------------|--------|-------------------|
| 输入 | 期望 | 覆盖点 |
| 0 | true | 单元素,起点就是终点 |
| 1, 0 | true | 恰好跳 1 步,检验 >= 边界 |
| 2, 0, 0 | true | 一次跨过多个零点 |
| 3, 2, 1, 0, 4 | false | 经典阻塞零点 |
| 2, 0, 0, 1, 0 | true | 前面可以越过零点 |
| 0, 1 | false | 起点为零且终点不同 |
| 1, 1, 1, 1 | true | 连续恰好前进 |
| 1, 0, 1, 0 | false | 无法越过中间阻塞点 |
| 2, 3, 1, 1, 4 | true | 题目示例 |
| 10, 0, 0, 0, 0 | true | 大跳跃覆盖终点 |
九 结论与建议
你的改进版比初版更清晰,但仍不是算法层面的改进,因为两者都围绕每个零点进行回溯,最坏时间复杂度仍为 O(n^2)。同时,严格大于和单元素数组两个边界问题会导致错误答案。
推荐直接使用 farthest 贪心版本。它把题目转化为"维护可达区间的右端点",既避免了逐个分析零点,也自然覆盖了零值、终点、恰好跳到和连续大跳跃等边界情况。
如果只想在现有思路上做最小修补,至少需要把 numsj > att 改为 numsj >= att,并在函数开头处理 numsSize == 1;但从学习和维护角度,仍建议采用一次扫描的贪心写法。
十 来源与证据范围
官方题目页面: LeetCode 55 跳跃游戏,访问日期:2026 年 10 月 24 日。
个人代码来源:用户在本次对话中直接粘贴,证据类型为 user pasted。本文只评价这两段可见代码,不推断其是否已经通过在线评测。