hot100 跳跃游戏 II(45)

发布时间:2026/7/24 17:56:10

hot100 跳跃游戏 II(45) 贪心区间边界迭代模型跳跃游戏 II (Jump Game II) 的线性时间最优解法与底层机制剖析结论先行 (BLUF)本题的核心本质是将一维数组上的跳跃决策抽象为隐式广度优先搜索 (Implicit BFS)的区间层级推进模型。通过维护当前跳跃步数所能覆盖的右边界cur以及当前层级内所有节点能达到的最远右边界next算法在单次线性扫描O(n)内完成全局最优解判定。该解法无显式队列压栈开销额外空间复杂度达到理论极限O(1)最终走向是精准输出到达末尾索引n - 1的绝对最小跳跃次数。一、 问题本质与数据模型物理抽象对于长度为n的零索引整数数组nums题目给出的核心拓扑关系与约束模型如下非负步长约束每个位置nums[i]指定了从索引i出发向右跳跃的最大物理跨度。对于任意跳转目标j必须满足0 j nums[i]且i j n。终点可达性保障题目测试用例明确保证必定存在至少一条有效路径能够从起点0抵达终点n - 1。若将数组中的每一个索引i视为有向图中的一个顶点将从i可达的每一个位置i j其中1 j nums[i]视为一条边权为 1 的有向边则“求解到达n - 1的最小跳跃次数”完全等价于在权值为 1 的有向无环图 (DAG) 中求解从源点0到汇点n - 1的单源最短路径 (Single-Source Shortest Path)。1.1 显式 BFS 到隐式区间 BFS 的物理折叠在常规有向图的最短路径求解中广度优先搜索 (BFS) 依赖队列结构 (Queue) 来按层Level推进。第k层包含所有从源点出发恰好需要k步即可到达的节点。然而在一维跳跃数组中每一个节点的出边都是连续的物理区间从索引i最远可达i nums[i]。这种区间连续性赋予了数据拓扑一个极度优美的物理几何特性所有只需k步即可到达的节点索引在物理内存中构成了一个连续的闭区间[L_k, R_k]。第 0 层 (0 步可达)区间为[0, 0]。第 1 层 (1 步可达)区间为[1, nums[0]]。第 k 层 (k 步可达)区间为[R_(k-1) 1, R_k]其中R_k为第k-1层中所有节点能够推导出的最大覆盖边界即R_k max(j nums[j])其中j属于[L_(k-1), R_(k-1)]。基于这一发现我们无需构建复杂的邻接表也无需开辟任何显式队列来存储节点。只需在代码中使用两个标量指针cur相当于当前层的右边界R_k和next相当于下一层的右边界R_(k1)即可完成对 BFS 层的完全物理折叠。索引下标: 0 1 2 3 4 数组数值: [ 2 | 3 | 1 | 1 | 4 ] ├───┴─────┤ │ 第 1 步覆盖范围 (cur 2) └─────────┴─────────────┴─────┤ │ 第 2 步最远覆盖范围 (next 4)二、 贪心选择性质与最优子结构的严密数学证明为了证明采用贪心策略更新区间右边界能够保证取得全局最小值我们需要证明该模型满足贪心选择性质 (Greedy Choice Property)与最优子结构 (Optimal Substructure)。2.1 贪心选择性质证明命题在当前跳跃层级[L_k, R_k]内选择能够提供最远延伸距离i nums[i]的节点作为推导下一层级边界的基准必定能包含全局最优路径。证明替换法 / Exchange Argument设算法在第k步时可达的最大右边界为R_k。对于任意一个策略A其第k步可达的边界为R_k^A。假设在起点k 0时R_0 R_0^A 0命题成立。假设当k m时贪心策略维持的右边界满足R_m R_m^A。在第m 1步贪心策略遍历区间[R_(m-1) 1, R_m]内的所有节点i并取R_(m1) max(i nums[i])。而策略A在第m步选择的区间为[R_(m-1)^A 1, R_m^A]。因为根据归纳假设R_m R_m^A且已知前m层覆盖的物理区间是单调向右拓展的所以贪心策略在第m层可供选择的节点集合包含了策略A在第m层可供选择的节点集合。根据集合包含关系定义在较大集合上的最大值函数必大于或等于定义在子集上的最大值函数即R_(m1) R_(m1)^A。综上归纳贪心策略在任意步数k建立的右边界R_k均严格单调不小于任何其他非贪心策略建立的右边界R_k^A。因此当右边界首次覆盖或超越终点n - 1时所使用的步数k必定是全局最小步数。2.2 最优子结构证明命题若到达终点n - 1的最小跳跃路径为P (v_0, v_1, ..., v_k)其中v_0 0v_k n - 1则对于路径上的任意中间节点v_m(其中0 m k)子路径(v_0, ..., v_m)必为从起点到达v_m的最小跳跃路径。证明反证法假设存在一条从v_0到达v_m的更短跳跃路径P其步数为m m。那么我们可以将子路径(v_0, ..., v_m)替换为P拼接成新的全路径P P (v_m, ..., v_k)。新路径P的总步数为m (k - m) m k - m k。这与P是从v_0到n - 1的最小跳跃路径总步数为k假设矛盾。因此最优子结构成立。三、 算法演进与多范式深度对比在求解“跳跃游戏 II”这一经典算法问题时随着对求解视角与数据特性的理解加深算法结构经历了从动态规划到显式图遍历再到隐式贪心区间的演进。维解法范式时间复杂度空间复杂度核心原理与拓扑推导物理缺陷与工程瓶颈自底向上动态规划 (Bottom-Up DP)O(n^2)O(n)维护一维状态数组dp[i]表示到达索引i的最小步数。状态转移方程dp[i] min(dp[j] 1)其中j i且j nums[j] i存在大量重复的区间比较在全递减数组或长跳跃数组下内部循环产生严重的O(n^2)计算冗余显式队列广度优先搜索 (Explicit BFS)O(n)O(n)将数组构建为图使用QueueInteger存储当前层节点。逐个弹出节点并将其所有未访问的相邻可达节点压入队列记录层级变化频繁触发 Java 对象的压栈出栈与堆内存分配带来极大的 GC 压力与内存碎片化隐式贪心区间迭代法 (当前最优解)O(n)O(1)利用区间连续性将队列压栈抽象为cur与next双指针的移动在单次线性扫描中自发完成层级切换依赖数组元素的顺次物理内存布局若在稀疏图/无序链表中不可直接应用3.1 动态规划解法代码基准 (用于性能对比)// 动态规划解法时间 O(n^2)空间 O(n) class SolutionDP { public int jump(int[] nums) { int n nums.length; int[] dp new int[n]; // 初始化 dp 数组为极大值 java.util.Arrays.fill(dp, Integer.MAX_VALUE); dp[0] 0; // 起点步数为 0 for (int i 0; i n; i) { for (int j 1; j nums[i] i j n; j) { dp[i j] Math.min(dp[i j], dp[i] 1); } } return dp[n - 1]; } }四、 源码逐行拆解与微观执行细节下面针对当前全局最优的贪心区间边界迭代解法进行源码级拆解剖析。4.1 核心源码实现class Solution { public int maxDepth(int[] nums) { // 方法体逻辑 int ans 0; // 记录全局累计跳跃步数即 BFS 递归的层级数 int cur 0; // 记录当前跳跃步数所能覆盖的物理右边界 (当前层边界) int next 0; // 记录在当前层遍历过程中下一跳所能达到的最远右边界 (下一层边界) // 关键细节循环只需遍历到 nums.length - 2 即可 for (int i 0; i nums.length - 1; i) { // 实时维护下一跳能够触及的最远极限位置 next Math.max(next, i nums[i]); // 当物理扫描指针 i 触及当前层的右边界 cur 时触发层级切换 if (cur i) { cur next; // 强制将当前边界拓展至下一层的最远极限 ans; // 步数递增表示必须进行一次新的跳跃 } } return ans; } }4.2 核心微观逻辑解读1. 为什么循环终止条件是i nums.length - 1而非i nums.length这是该算法中最精妙的边界控制细节目标是到达n - 1根据题意当游标i成功到达n - 1时意味着我们已经处于终点不需要再发起任何新的跳跃。规避多余跳跃计数假设数组为nums [2, 3, 1, 1, 4]当i 3且cur 3时执行if (cur i)使得cur更新为next 4即末尾索引此时ans变为2。如果循环条件写为i nums.length即遍历到i 4当i 4时由于此时cur恰好等于4代码会再次触发if (cur i)条件导致ans被错误地递增为3因此在索引n - 1之前的最后一个位置即n - 2完成检查后若cur已经覆盖到n - 1程序自发终止循环返回的ans即为精确解。2.Math.max(next, i nums[i])的物理意义在指针i从L_k移动到R_k的过程中算法不断探测每个点作为起跳点能达到的最远位置i nums[i]并将这些探索结果的并集最大值实时存入next中。这个过程本质上是在当前 BFS 层的物理窗口内部执行最大值规约 (Max Reduction)。五、 算法执行状态机步进示例与推演为了极其直观地展现算法在运行期的内部状态变迁以下针对不同拓扑特征的测试用例进行状态机推演。5.1 标准测试用例 1nums [2, 3, 1, 1, 4]初始状态ans 0,cur 0,next 0,n 5(循环遍历索引i从0至3)循环步数扫描指针 i当前数值 nums[i]可达极限 i nums[i]下一层边界 next 变化条件判定 cur i当前层边界 cur 变化跳跃步数 ans状态机拓扑物理描述初始---0-00位于起点0准备进行第 1 次跳跃探索i 0020 2 2max(0, 2) 2True(0 0)cur变为2ans变为1完成第 1 步决策第 1 步最远覆盖至索引 2i 1131 3 4max(2, 4) 4False(2 ! 1)维持 2维持 1在第 1 步范围内探索发现从索引 1 起跳可直达末尾 4i 2212 1 3max(4, 3) 4True(2 2)cur变为4ans变为2触及第 1 步边界 2强制切换至第 2 步边界拓至 4i 3313 1 4max(4, 4) 4False(4 ! 3)维持 4维持 2扫描结束已达n - 2 3最终输出ans 25.2 包含零元素的特殊用例nums [2, 3, 0, 1, 4]初始状态ans 0,cur 0,next 0,n 5循环步数扫描指针 i当前数值 nums[i]可达极限 i nums[i]下一层边界 next 变化条件判定 cur i当前层边界 cur 变化跳跃步数 ans状态机拓扑物理描述初始---0-00位于起点0i 0020 2 2max(0, 2) 2True(0 0)cur变为2ans变为1第 1 步可达范围设定为[1, 2]i 1131 3 4max(2, 4) 4False(2 ! 1)维持 2维持 1发现索引 1 可拓展至 4i 2202 0 2max(4, 2) 4True(2 2)cur变为4ans变为2遇到 0 值陷阱但因next已被更新为 4安全跳过陷阱i 3313 1 4max(4, 4) 4False(4 ! 3)维持 4维持 2循环结束最终输出ans 25.3 严格单步递进用例nums [1, 1, 1, 1, 1]初始状态ans 0,cur 0,next 0,n 5循环步数扫描指针 i当前数值 nums[i]可达极限 i nums[i]下一层边界 next 变化条件判定 cur i当前层边界 cur 变化跳跃步数 ans状态机拓扑物理描述i 0010 1 11Truecur变为 1ans变为 1第 1 步只能到达索引 1i 1111 1 22Truecur变为 2ans变为 2第 2 步只能到达索引 2i 2212 1 33Truecur变为 3ans变为 3第 3 步只能到达索引 3i 3313 1 44Truecur变为 4ans变为 4第 4 步到达末尾索引 4最终输出ans 45.4 极端边界用例单元素数组nums [0]初始状态ans 0,cur 0,next 0,n 1循环判定for (int i 0; i nums.length - 1; i)-i 0条件不满足循环直接被跳过。返回值直接返回初始值ans 0。物理合理性验证由于起点即为终点跳跃次数理论上精确为 0程序输出与物理现实完全匹配。六、 复杂度分析与计算机底层执行6.1 时间复杂度分析严格线性O(n)遍历开销算法包含一个单层for循环循环变量i从0线性递增至n - 2共计执行n - 1次迭代。单次迭代指令数在每次迭代内部仅执行一次加法运算 (i nums[i])一次二元取最大值比较 (Math.max)一次条件分支判定 (cur i)若干次常数级的变量赋值。总体耗时算法的基本指令执行总次数为C * (n - 1)其中C为极小常数因此渐进时间复杂度为O(n)。即使在最坏的乱序或极端大跨度输入下时间复杂度也不存在任何退化风险。6.2 空间复杂度分析理论极限O(1)堆内存开销算法未申请任何动态数组、哈希表或队列结构堆内存分配为0字节。栈内存开销仅在当前函数栈帧 (Stack Frame) 中开辟了 3 个局部整型变量ans,cur,next以及循环变量i。总体占用额外空间复杂度为严格的O(1)。6.3 计算机体系结构与 CPU 硬件级执行优势1. 高度连续的内存访问与 L1/L2 Cache 预取 (Data Cache Line Prefetching)nums数组在 JVM 堆内存中占据一片连续的物理地址空间。算法对nums的读取是完全顺序的从索引 0 到n - 2。CPU 硬件预取器 (Hardware Prefetcher) 能够以 100% 的准确率提前将后续的 Cache Line通常为 64 字节加载至 CPU L1/L2 高速缓存中。这极大地避免了由于随机内存访问引发的 Cache Miss使内存访问延迟降低至接近 0 个 CPU 时钟周期。内存物理布局: [ nums[0] | nums[1] | nums[2] | nums[3] | ... ] ▲ ▲ ▲ ▲ CPU 预取方向: ═════════════════════════════════════════ (完全顺序读Cache 命中率 ~100%)2. 指令级并行 (ILP) 与分支预测 (Branch Prediction)在 JVM 编译期JIT 编译器如 C2 Compiler会将核心循环展开并编译为精简的汇编指令代码段; 示意汇编代码逻辑 mov eax, dword ptr [rbx r12*4] ; 读取 nums[i] add eax, r12d ; i nums[i] cmp eax, r13d ; 比较 (i nums[i]) 与 next cmovg r13d, eax ; 条件传送指令 (CMOVG): 无分支地更新 next Math.max cmp r14d, r12d ; 比较 cur 与 i jne .NEXT_ITER ; 若不相等跳转至下一轮循环 mov r14d, r13d ; cur next inc ebp ; ans .NEXT_ITER: inc r12d ; i无分支比较优化Math.max被编译为CMOVConditional Move条件传送指令避免了通过跳转指令实现分支从而彻底消除了分支预测错误 (Branch Misprediction)导致的 CPU Pipeline Flush流水线清空惩罚。七、 工业级拓展、防错性编程与变体延伸在实际工程落地或高难度面试场景中针对“跳跃游戏”系列问题往往需要考虑非理想情况下的防御性编程及路径还原需求。7.1 扩展场景 1终点不可达情况下的异常捕获跳跃游戏 I 与 II 的结合若题目取消“保证可达”的先验假设数组中可能存在无法越过的“0 值断崖”如nums [3, 2, 1, 0, 0, 0, 4]。原生代码在此类输入下可能会引发死循环或返回错误结果。防御性改造代码class SolutionSafe { public int jumpSafe(int[] nums) { if (nums null || nums.length 1) { return 0; } int ans 0; int cur 0; int next 0; for (int i 0; i nums.length - 1; i) { // 实时更新最远可达边界 next Math.max(next, i nums[i]); // 防御性判定如果探查到的最远边界连当前索引都无法超越说明陷入绝境 if (next i) { return -1; // 代表无法到达终点 } if (cur i) { cur next; ans; // 性能优化剪枝若当前拓展的边界已覆盖终点可提前退出 if (cur nums.length - 1) { break; } } } return cur nums.length - 1 ? ans : -1; } }7.2 扩展场景 2具体跳跃路径重构与还原 (Path Reconstruction)工业界不仅需要得知“最小跳跃次数”往往还需要获取具体的跳跃物理节点序列例如在路由规划、物理机器人路径避障中。路径还原算法设计原理为了以O(n)时间与O(1)额外空间不含输出结果集重构路径我们可以在每一次需要发起跳跃时即cur i触发时记录下在上一阶段窗口内促成next达到最大值的那个具体索引。import java.util.ArrayList; import java.util.List; class SolutionPathReconstruction { public ListInteger jumpPath(int[] nums) { ListInteger path new ArrayList(); path.add(0); // 路径起始点必为索引 0 if (nums.length 1) { return path; } int cur 0; int next 0; int bestIndex 0; // 记录当前窗口内能跳得最远的最佳起跳索引 for (int i 0; i nums.length - 1; i) { // 维护最佳起跳点 if (i nums[i] next) { next i nums[i]; bestIndex i; // 锁定贡献了最大跳跃距离的节点 } if (cur i) { cur next; // 如果 bestIndex 还没加入路径防止重复记录则压入路径集 if (path.get(path.size() - 1) ! bestIndex) { path.add(bestIndex); } // 若已经能触及末尾将末尾加入并提前结束 if (cur nums.length - 1) { break; } } } // 确保终点索引被正确追加 if (path.get(path.size() - 1) ! nums.length - 1) { path.add(nums.length - 1); } return path; } }路径还原执行示例对nums [2, 3, 1, 1, 4]运行上述路径还原代码输出路径序列[0, 1, 4]。含义从索引0出发跳跃至索引1数值 3再跳跃至末尾索引4总跳跃次数为2次路径精准无误。

相关新闻