力扣hot100之55:跳跃游戏
力扣hot100之55:跳跃游戏

LeetCode 55:跳跃游戏(Jump Game)题解

Jump Game 是 LeetCode Hot 100 里面一道非常经典的贪心题。

这道题表面上看只是一个数组跳跃问题: 给定一个数组,每个位置上的数字表示你从当前位置最多可以跳多远,问你最终能不能到达最后一个位置。

题目本身不算难,但它非常适合用来理解 贪心算法 的一个核心思想:

并不是每一步都真的把路径走出来,而是维护“当前最远能到哪”。

很多人第一次做这题时,会下意识地去想:

  • 从当前位置可以跳 1 步、2 步、3 步……

  • 那是不是应该用递归或者回溯把所有跳法都试一遍?

这样当然能做,但效率会非常差。 而你给出的这份 Java 代码,用的是这道题最经典、最优雅的一种写法:

贪心维护最远可达位置

这种方法的关键就在于: 我们根本不需要关心“具体怎么跳”,只需要关心“到目前为止,最远能到哪里”。

本文就结合这段代码,系统地讲一下这道题的思路。


题目介绍

给定一个非负整数数组 nums

数组中的每个元素 nums[i] 表示你在位置 i 时,最多可以向前跳跃的长度。

一开始你站在数组的第一个位置,也就是下标 0 现在需要判断:你是否能够到达数组的最后一个位置。

例如:

nums = [2,3,1,1,4]

从下标 0 出发,最多能跳 2 步。 一种可行路径是:

  • 0 跳到 1

  • 再从 1 跳到最后一个位置

所以答案是:

true

而如果是:

nums = [3,2,1,0,4]

虽然前面一开始看起来能跳很远,但最终会卡在下标 3,因为那里是 0,无法再继续向前,所以答案是:

false

示例

示例 1

输入:nums = [2,3,1,1,4]
输出:true

因为你可以这样跳:

  • 0 -> 1 -> 4

所以可以到达最后一个位置。

示例 2

输入:nums = [3,2,1,0,4]
输出:false

不管怎么跳,都会在下标 3 附近被卡住,无法继续到达最后一个位置。


最直接的想法是什么?

如果一开始按最朴素的思路去想,这道题很容易想到“搜索”:

  • 从当前位置出发,尝试所有可能跳到的位置

  • 对每个下一步继续递归

  • 看有没有哪条路径能到达终点

这个思路本质上就是回溯 / DFS。

但问题在于:

  • 同一个位置可能会被重复访问很多次

  • 搜索树分支非常多

  • 最坏情况下会超时

所以虽然这题可以用递归去理解,但真正高效的做法并不是“试所有跳法”,而是要抓住一个更本质的信息:

当前最远能到达的位置。


核心思路:维护最远可达位置

你这段代码的核心思想其实可以概括成一句话:

遍历数组时,持续维护当前能到达的最远下标 maxPos

如果当前遍历到的位置 i 已经超过了 maxPos,说明这个位置根本到不了,那后面更不可能到达,直接返回 false

否则,只要当前位置还能到,我们就尝试用它来更新最远可达范围:

maxPos = max(maxPos, i + nums[i])

其中:

  • i 是当前位置

  • nums[i] 是从这里最多能跳多远

  • i + nums[i] 就是从当前位置出发最远能到达的位置

不断更新后,如果能顺利遍历完整个数组,就说明最后一个位置一定可达。


Java 代码

下面是你给出的代码,我这里保留原始逻辑,并加上注释:

class Solution {
    public boolean canJump(int[] nums) {
        int maxPos = 0;

        for (int i = 0; i < nums.length; i++) {
            // 如果当前位置已经超过最远可达范围,说明这里到不了
            if (i > maxPos) return false;

            // 更新当前最远可达位置
            maxPos = Math.max(maxPos, i + nums[i]);
        }

        return true;
    }
}

代码拆解

这段代码非常短,但它把贪心思想体现得非常完整。 下面一步一步来看。


1. maxPos 表示什么?

int maxPos = 0;

maxPos 表示:

在遍历到当前时刻之前,我们最远能够到达的下标。

初始值设为 0,因为一开始我们就站在下标 0 这个位置。

随着遍历进行,如果当前位置可以到达,我们就利用这个位置继续扩展最远可达范围。

所以 maxPos 可以理解成一个“可达边界”。


2. 为什么要遍历整个数组?

for (int i = 0; i < nums.length; i++)

因为我们想逐个检查:

  • 当前位置能不能到

  • 如果能到,能不能帮我们把最远范围推得更远

这和很多人直觉里“真的跳来跳去”不一样。

这里的遍历并不是在模拟一条具体路径,而是在做一件更高层次的事情:

扫描所有当前可达的位置,并用它们更新全局最远可达位置。

所以看似是普通遍历,实际上是在不断扩大我们的“覆盖范围”。


3. 为什么 i > maxPos 时就可以直接返回 false

if (i > maxPos) return false;

这是整道题最关键的一句。

因为如果当前下标 i 已经比我们最远能到达的位置还要大,说明:

  • 下标 i 根本不可达

  • 那么它后面的位置自然也更不可能通过它到达

也就是说,只要出现一个“走不到”的位置,后面整个过程就断掉了。

例如:

nums = [3,2,1,0,4]

遍历到下标 4 时,会发现当前的 maxPos 最多只能到 3 这说明位置 4 根本到不了,那答案就一定是 false

所以一旦出现:

i > maxPos

直接返回 false 就可以了。


4. 为什么更新公式是 max(maxPos, i + nums[i])

maxPos = Math.max(maxPos, i + nums[i]);

如果当前位置 i 是可达的,那么从这里出发,最远可以跳到:

i + nums[i]

但我们原本已经有一个历史最远可达位置 maxPos

所以新的最远可达位置,应该取两者最大值:

max(maxPos, i + nums[i])

这一步就是贪心策略的核心:

每到一个能到达的位置,就尽可能更新全局最远范围。

我们并不关心具体是从哪个点跳过去的,只关心“目前能覆盖到哪里”。


手动模拟一遍:可达情况

我们用经典样例来走一遍:

nums = [2,3,1,1,4]

初始:

maxPos = 0

下标 0

  • i = 0

  • i <= maxPos,说明当前位置可达

  • 从这里最远能到:

0 + 2 = 2

更新后:

maxPos = 2

下标 1

  • i = 1

  • 1 <= 2,说明可达

  • 从这里最远能到:

1 + 3 = 4

更新后:

maxPos = 4

此时其实已经能覆盖到最后一个位置了。


下标 2

  • 2 <= 4

  • 可达

  • 从这里最远能到:

2 + 1 = 3

更新后 maxPos 仍然是 4


下标 3

  • 3 <= 4

  • 可达

  • 最远能到:

3 + 1 = 4

maxPos 不变。


下标 4

  • 4 <= 4

  • 可达

遍历结束,返回:

true

手动模拟一遍:不可达情况

再看这个例子:

nums = [3,2,1,0,4]

初始:

maxPos = 0

下标 0

  • 可达

  • 最远能到:

0 + 3 = 3

更新:

maxPos = 3

下标 1

  • 1 <= 3

  • 可达

  • 最远能到:

1 + 2 = 3

maxPos 仍然是 3


下标 2

  • 2 <= 3

  • 可达

  • 最远能到:

2 + 1 = 3

maxPos 不变。


下标 3

  • 3 <= 3

  • 可达

  • 最远能到:

3 + 0 = 3

还是不能往前推进。


下标 4

这时:

i = 4
maxPos = 3

因为:

4 > 3

说明位置 4 根本到不了,直接返回:

false

为什么这个方法是正确的?

因为这道题真正关心的并不是“路径本身”,而是“可达范围”。

假设我们已经知道:

  • 从前面某些位置出发,最远能到达 maxPos

那么对于当前下标 i

如果 i > maxPos

说明当前位置不可达。 而一个不可达的位置,不可能帮助我们继续往前推进,所以后面也不可能成功。

如果 i <= maxPos

说明当前位置可达。 那么它就有资格用自己的跳跃能力来扩大可达范围:

i + nums[i]

于是只要一路扫描并不断更新最远边界,就可以正确判断是否能到终点。

这正是贪心策略的本质:

每一步都维护当前最重要的信息——最远可达位置。


复杂度分析

时间复杂度

只遍历了一次数组,每个位置只处理一次,所以时间复杂度是:

O(n)

空间复杂度

只使用了一个额外变量 maxPos,所以空间复杂度是:

O(1)

这也是这道题贪心解法最大的优势: 既快,又省空间。


这道题真正想考什么?

这道题表面上是在“跳格子”,但本质上考察的是贪心思维。

第一,能不能抓住真正重要的信息

很多人一开始会被“具体跳法”吸引,去想到底怎么跳。 但这题真正重要的其实不是路径,而是:

当前最远能到哪里。

一旦抓住这个核心,问题就会一下子变得简单很多。

第二,能不能用局部信息推全局结果

每次更新 maxPos,都是在做一个局部最优选择:

  • 当前能到的点里,谁能让我走得更远?

而通过不断维护这个局部最优,最终就能判断整个数组是否可达终点。

这就是典型的贪心思想。


和“跳跃游戏 II”有什么区别?

这道题和后面的“跳跃游戏 II”很像,但目标不同。

跳跃游戏(本题)

只问:

能不能到达最后一个位置

所以只需要维护最远可达范围,返回布尔值。

跳跃游戏 II

问的是:

最少跳几次能到最后一个位置

那时候除了可达性,还要进一步考虑“分层推进”和“步数统计”。

所以这道题其实是“跳跃游戏 II”的基础版。


总结

这道题的核心思想可以概括成一句话:

遍历数组时,持续维护当前最远可达位置;如果某个位置已经超过这个范围,说明无法到达终点。

具体做法就是:

  1. maxPos 表示当前最远可达下标

  2. 遍历数组中的每个位置

  3. 如果当前位置 i > maxPos,说明当前位置不可达,直接返回 false

  4. 否则更新:

maxPos = max(maxPos, i + nums[i])
  1. 如果顺利遍历完数组,说明最后一个位置可达,返回 true

这样就能在:

O(n)

的时间复杂度和:

O(1)

的空间复杂度下解决问题。

这是一道非常经典、也非常值得反复理解的贪心题。 因为它会让你真正体会到:

  • 有时候不需要枚举所有路径

  • 真正关键的是抓住问题中的核心状态

  • 贪心并不是“随便选”,而是维护最重要的全局信息

把这道题真正吃透之后,再去看:

  • 跳跃游戏 II

  • 加油站

  • 分发饼干

  • 区间覆盖类题目

你会更容易理解“贪心到底在贪什么”。

因为“跳跃游戏”本身就是贪心入门题里非常经典的一道模板题。

如果对您有帮助的话,能否支持一下博主?
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇