LeetCode 45 跳跃游戏 II C 语言解题分析

从逐次选点到分层贪心

分析日期:2026 年 10 月 24 日 | 代码来源:用户粘贴

本文分析你提供的 jump 初版、参考实现和推荐写法,重点解释为什么初版会超时甚至停在原地,以及参考代码如何用"当前跳数层的右边界"在线性时间内得到最少跳跃次数。

官方题目: 45. 跳跃游戏 II。

一 题目模型与约束

给定非负整数数组 nums,numsi 表示从下标 i 最多可以跳 numsi 步。保证可以到达最后一个下标,要求返回到达最后一个下标所需的最少跳跃次数。

|--------|-----------------------------------------|
| 项目 | 题目规则 |
| 数组长度 | 1 <= nums.length <= 10^4 |
| 元素范围 | 0 <= numsi <= 1000 |
| 跳跃范围 | 从 i 可以到达 i + j,其中 0 <= j <= numsi |
| 可达性 | 题目保证一定可以到达最后一个下标 |
| 目标 | 返回最少跳跃次数 |

长度为 1 时,起点已经是终点,答案是 0。题目保证可达,因此算法不必处理"无法到达"的返回约定;但在通用代码中仍可以保留不可达保护。

二 两个版本的策略对比

|--------|-----------------------------------|---------------------------|
| 代码 | 核心做法 | 主要问题或优点 |
| 你的初版 | 从当前位置枚举下一跳,选择 numsj - j 最大的位置 | best 未按轮次重置;可能原地循环;j 可能越界 |
| 参考代码 | 扫描当前跳数层,维护下一层最远边界 | O(n),与最少跳数的层次结构一致 |
| 推荐整理版 | 保留参考算法,改名并删除变量遮蔽 | 语义更清楚,边界行为稳定 |

三 初版代码:超时与错误原因

int jump(int* nums, int numsSize) {
int f = 0, count = 0, i = 0, best = -1000;
while (i < numsSize) {
for (int j = i; j <= i + numsi; j++) {
if (numsj - j > best) {
best = numsj - j;
f = j;
}
}
i = f;
count++;
}
return count;
}

这段代码的直觉是:从当前下标可以跳到一段区间,选择"下一步潜力最大"的位置。但实现中有三个决定性问题。

|------------|-------------------------------------------|--------------------------|
| 问题 | 具体表现 | 后果 |
| best 是全局变量 | 只初始化一次,没有在每轮 for 前重置 | 前一轮的最优值会压制后一轮的候选 |
| 选择指标不完整 | 比较 numsj - j,而不是绝对到达点 j + numsj | 容易把"剩余潜力"和"当前覆盖边界"混在一起 |
| 没有终点终止条件 | while (i < numsSize) 只检查是否越过数组 | 到达最后一个下标后仍可能继续计算 |
| 访问可能越界 | j <= i + numsi 没有取 numsSize - 1 的最小值 | 例如 100, 1 会访问数组外 |
| 依赖严格大于更新 | 相等候选不会更新 f | 在第一个窗口就可能保持 f = i,导致原地循环 |

图 1 在 2,3,1,1,4 上,初版第一次扫描就可能得到 f = 0。

具体过程:i = 0 时,j = 0 得到 nums0 - 0 = 2,best 变成 2,f 变成 0;j = 1 得到 3 - 1 = 2,只是相等,不满足 >;j = 2 得到 1 - 2 = -1。循环结束后 i = f = 0,下一轮完全重复,因此表现为超时。

即使把比较式改成 j + numsj,也不能只靠这一个改动解决问题:best 仍然需要按当前跳跃层重置,循环仍需要限制到数组末尾以内,并且要在到达终点时停止。

四 参考代码:分层贪心

int jump(int* nums, int numsSize) {
int i = 0, end = 0, best = 0, s = 0;
for (int i = 0; i < numsSize - 1; i++) {
if (numsi + i > best) best = numsi + i;
if (i == end) {
s++;
end = best;
}
}
return s;
}

参考代码把每一次跳跃看成一层:end 是当前跳数能够覆盖到的最右边界;在扫描这一层内的所有位置时,best 记录下一层能够覆盖到的最远位置。当 i 走到 end,说明当前层已经扫描完,必须再跳一次,于是 s 加一,并把 end 推进到 best。

|--------|-------------------|--------------|
| 变量 | 含义 | 更新时机 |
| i | 当前正在扫描的位置 | 每次循环加 1 |
| end | 当前跳数层的右边界 | i == end 时更新 |
| best | 从当前层所有位置出发的最远可达位置 | 扫描每个 i 时更新 |
| s | 已经确定需要的跳跃次数 | 到达当前层边界时加 1 |

图 2 2,3,1,1,4 的两个跳跃层:先扫描 0,再扫描 1 和 2。

五 推荐整理版

int jump(int* nums, int numsSize) {
int jumps = 0;
int end = 0;
int farthest = 0;

for (int i = 0; i < numsSize - 1; ++i) {
if (i + numsi > farthest) {
farthest = i + numsi;
}

if (i == end) {
++jumps;
end = farthest;
}
}

return jumps;
}

整理版保留参考代码的算法,只做了两点改善:变量名直接表达含义,并删除了外层多余的 i 声明,避免内层 for 的 i 遮蔽外层变量。题目保证可达,因此不需要额外的不可达分支。

  • 循环只扫描到 numsSize - 2,因为到达最后一个下标后不需要再发起跳跃。
  • farthest 使用绝对下标 i + numsi,直接表达"下一层能到哪里"。
  • i == end 时才增加 jumps,避免对同一层中的多个位置重复计数。

六 正确性证明

不变量:在扫描当前层时,end 表示使用 jumps 次跳跃可以覆盖到的最右位置;farthest 表示从当前层所有可达位置出发,使用 jumps + 1 次跳跃可以覆盖到的最右位置。

  • 扫描 i 时,i <= end,说明 i 在当前跳数层内可达,因此可以用 i + numsi 更新 farthest。
  • 当 i == end,当前层已经扫描完。此时 farthest 汇总了这一层所有候选位置的最远下一层边界,增加一次跳跃并令 end = farthest,不会漏掉更优路径。
  • 每次增加 jumps 都是在当前层耗尽后发生,因此 jumps 表示到达新边界所需的最少层数。
  • 循环在 numsSize - 1 之前结束,最后一次边界扩展覆盖终点时,返回的 jumps 即为最少跳跃次数。

七 复杂度对比

|----------|---------------------|----------|-----------------|
| 方法 | 时间复杂度 | 额外空间 | 评价 |
| 初版 | 可能不终止;修正后最坏 O(n^2) | O(1) | 逐轮枚举并且存在边界和状态问题 |
| 参考 / 整理版 | O(n) | O(1) | 每个位置只扫描一次 |

初版在当前写法下不适合只用复杂度衡量,因为存在 i = f 的原地循环;如果先修正状态重置、终点停止和数组边界,逐轮选点的最坏情况仍会重复扫描很多候选位置,通常为 O(n^2)。

八 测试矩阵

|-----------------|----------|-------------|
| 输入 | 期望跳数 | 覆盖点 |
| 0 | 0 | 单元素,起点就是终点 |
| 2,3,1,1,4 | 2 | 官方示例,验证两层边界 |
| 2,3,0,1,4 | 2 | 零值不一定阻塞 |
| 1,1,1,1 | 3 | 每次只能前进一格 |
| 1,2,1,1 | 2 | 边界推进 |
| 5,1,1,1,1,1 | 1 | 第一步直接覆盖终点 |
| 2,0,2,0,1 | 2 | 跨过零点后继续推进 |
| 100,1 | 1 | 检查初版的越界风险 |
| 1,0,1,0 | 2 | 恰好到达零点仍可继续 |

九 结论与建议

初版的主要问题不是单纯"循环写得慢",而是状态定义没有和跳跃层对齐:best 没有按轮次重置,且 f 可能保持原值,导致在合法输入上原地循环。与此同时,未限制 j 的上界会产生越界访问。

参考代码的核心思想是正确且高效的:把所有"用同样跳数能到达的位置"视为一层,扫描这一层时计算下一层的最远边界;当扫描到当前边界时,跳数加一。推荐使用整理版,时间复杂度 O(n),额外空间 O(1)。

十 来源与证据范围

官方题目页面: LeetCode 45 跳跃游戏 II,访问日期:2026 年 10 月 24 日。

个人代码来源:用户在本次对话中直接粘贴,证据类型为 user pasted。本文评价的是题目约束下的算法行为,并补充了 C 语言数组边界、循环终止和变量遮蔽等实现层面的风险。

相关推荐
LabVIEW开发1 小时前
ESO 极大望远镜主动光学:LabVIEW 实时模块 + 多核 COTS 替代专用硬件的路径
labview·1024程序员节·labview知识·labview功能·labview程序
夜不会漫长1 小时前
C++ 多态学习笔记
1024程序员节
nington011 小时前
金融信创国产AD重构:2027倒计时下的技术选型与落地路径
其他·1024程序员节
LabVIEW开发1 小时前
ISRO 卫星应答机可搬移 ATE:模块化仪器 + LabVIEW 把测试时间砍掉一半
labview·1024程序员节·labview知识·labview功能·labview程序
Yang96111 小时前
自适应放电加实时纠偏,解析TY-60H在矿石能源的接续表现
1024程序员节
吹什么轩1 小时前
LangChain 全解:为什么大模型应用需要一层“框架“
1024程序员节
一个平凡而乐于分享的小比特1 小时前
CODESYS V3.5 SP10 移植至 T536 平台实战指南(二)
1024程序员节·codesys·t536
yuniko-n1 小时前
【JUC】读写锁 StampedLock
java·1024程序员节
csdn_aspnet1 小时前
.NETCore SqlSuagr 根据 mysql 数据表按顺序生成简单编号
mysql·.netcore·sqlsugar·1024程序员节·sequence·.net8·编号