题目描述
给定一个长度为 n 的 0 索引 整数数组 nums。初始位置为 nums[0]。
每个元素 nums[i] 表示从索引 i 向后跳转的最大长度。换句话说,如果你在 nums[i] 处,你可以跳转到任意 nums[i + j] 处:
0 <= j <= nums[i]i + j < n
返回到达 nums[n - 1] 的 最小跳跃次数。生成的测试用例可以到达 nums[n - 1]。
示例 1:
输入: nums = [2,3,1,1,4]
输出: 2
解释: 跳到最后一个位置的最小跳跃数是 2。
从下标为 0 跳到下标为 1 的位置,跳 1 步,然后跳 3 步到达数组的最后一个位置。
示例 2:
输入: nums = [2,3,0,1,4]
输出: 2
提示:
1 <= nums.length <= 10^40 <= nums[i] <= 1000- 题目保证可以到达
nums[n-1]
算法思路
核心思想:贪心算法
这道题要求找到到达数组末尾的 最小跳跃次数。我们可以使用贪心算法来解决。 核心思想是在每一步都选择跳跃到 下一步最远可达的位置,而不是只跳到当前最远可达的位置。 这样做可以保证我们以最少的跳跃次数到达终点。
从 0 到 1 的思考过程:
-
最少跳跃次数: 题目明确要求最小跳跃次数,这意味着我们需要在每一步都做出最优的选择,以减少总的跳跃次数。
-
每一步的最优选择: 在当前位置
i,我们可以跳跃的范围是[i+1, i + nums[i]]。 为了最小化跳跃次数,我们应该选择下一步能够跳得 最远 的位置作为我们的下一个跳跃点。 也就是说,在所有可以从当前位置i到达的下一步位置中,我们要选择那个能够到达更远距离的位置。 -
维护当前可达范围和下一步最远范围: 我们需要维护两个关键变量:
end: 表示当前跳跃次数下,能够到达的最远边界。 初始时end = 0,表示从起始位置跳 0 步能到达的最远距离就是起始位置本身。maxReach: 表示从当前可达范围内,下一步能够到达的最远距离。 在遍历当前可达范围内的每个位置时,都更新maxReach。
-
遍历数组并更新跳跃次数: 我们遍历数组,当遍历到当前可达范围的边界
end时,意味着我们需要进行下一次跳跃了。 此时,我们将跳跃次数steps加 1,并将新的end更新为maxReach。 新的end就是在上次跳跃的基础上,通过选择下一步最优跳跃点,能够到达的最远距离。 -
提前到达终点: 在更新
end的过程中,我们需要判断新的end是否已经到达或超过数组的末尾。 如果是,则说明我们已经到达终点,可以直接返回当前的跳跃次数steps。
复杂度分析
- 时间复杂度: O(n),其中 n 是
nums数组的长度。虽然有一个内层循环隐含在maxReach = max(maxReach, i + nums[i])中,但实际上我们只遍历了数组一次。 - 空间复杂度: O(1),我们只使用了常数个变量 (
steps,maxReach,end,i)。
代码实现
func jump(nums []int) int {
n := len(nums)
if n == 1 { // 如果数组长度为 1,无需跳跃,直接返回 0
return 0
}
steps := 0 // 跳跃次数
maxReach := 0 // 当前能跳到的最远位置
end := 0 // 当前跳跃次数下能到达的最远边界
for i := 0; i < n; i++ {
maxReach = max(maxReach, i+nums[i]) // 更新当前能跳到的最远位置
if i == end { // 到达当前可达边界
steps++ // 增加跳跃次数
end = maxReach // 更新新的可达边界
if end >= n-1 { // 如果新的可达边界已经到达或超过终点,则提前结束
break
}
}
}
return steps
}
过程梳理:
- 初始化:
steps = 0(跳跃次数初始化为 0)maxReach = 0(最远可达距离初始化为 0,即起始位置)end = 0(当前跳跃次数下,能到达的最远边界,初始为 0)
- 遍历
nums数组: 对于数组中的每个位置i(从 0 到数组长度 - 1):- 更新当前位置能到达的最远距离:
maxReach = max(maxReach, i + nums[i])。 始终记录从当前可达范围内,下一步能跳到的最远距离。 - 判断是否到达当前可达范围边界:
if i == end。 如果当前位置i等于当前可达范围的边界end,则说明需要进行下一次跳跃了。- 进行跳跃:
steps++(跳跃次数加 1)。 - 更新新的可达范围边界:
end = maxReach(将新的可达范围边界更新为maxReach)。 新的end是在上次跳跃的基础上,通过选择下一步最优跳跃点,能够到达的最远距离。 - 检查是否到达终点:
if end >= n-1。 如果新的可达范围边界end已经到达或超过数组的最后一个索引n-1,则说明已经到达终点,直接break循环。
- 进行跳跃:
- 更新当前位置能到达的最远距离:
- 返回
steps。
示例解析
示例 1: nums = [2,3,1,1,4]
- 初始状态:
steps = 0,maxReach = 0,end = 0 - i = 0:
nums[0] = 2,maxReach = max(0, 0 + 2) = 2,i == end(0 == 0) 成立:steps = 0 + 1 = 1end = maxReach = 2end < n - 1(2 < 4) 不成立,继续循环
- i = 1:
nums[1] = 3,maxReach = max(2, 1 + 3) = 4,i == end(1 == 2) 不成立 - i = 2:
nums[2] = 1,maxReach = max(4, 2 + 1) = 4,i == end(2 == 2) 成立:steps = 1 + 1 = 2end = maxReach = 4end >= n - 1(4 >= 4) 成立,break循环
最终结果: steps = 2
示例 2: nums = [2,3,0,1,4]
- 初始状态:
steps = 0,maxReach = 0,end = 0 - i = 0:
nums[0] = 2,maxReach = max(0, 0 + 2) = 2,i == end(0 == 0) 成立:steps = 0 + 1 = 1end = maxReach = 2end < n - 1(2 < 4) 不成立,继续循环
- i = 1:
nums[1] = 3,maxReach = max(2, 1 + 3) = 4,i == end(1 == 2) 不成立 - i = 2:
nums[2] = 0,maxReach = max(4, 2 + 0) = 4,i == end(2 == 2) 成立:steps = 1 + 1 = 2end = maxReach = 4end >= n - 1(4 >= 4) 成立,break循环
最终结果: steps = 2
关键点
- 贪心策略: 每一步都选择下一步能跳得最远的位置,保证跳跃次数最少。
- 可达范围边界
end:end变量是核心,它定义了在当前跳跃次数下,我们能够到达的最远范围。 每次跳跃后,end都会被更新,扩大可达范围。 - 步数计数
steps: 在到达当前可达范围边界end时,才增加跳跃次数steps,保证了跳跃次数的最小化。 - 提前到达终点判断: 在更新
end后,立即判断是否到达终点,避免不必要的遍历,提高效率。
附:在考虑使用贪心前可以进行思考
初步分析
-
问题类型识别: 本题要求计算从起始位置跳到最后一个位置的最小跳跃次数,属于最优化问题。
-
是否可能使用贪心? 是的,贪心算法适用于此问题。每一步选择跳跃到最远的位置,能够尽量减少跳跃次数,符合最优解的要求。
贪心策略探索与验证
贪心策略的定义: 每次选择跳跃到当前位置可到达的最远位置 i + nums[i],从而减少跳跃次数。
贪心策略的合理性分析:
- 反例构建: 对于数组 [1, 2, 3, 4, 5],从 i = 0 跳到 i = 1,再跳到 i = 3,最终到 i = 4,是最优解。如果直接跳到 i = 4,也符合贪心策略,依旧是最优解。
- 直觉上的解释: 选择跳得最远的位置,可以减少未来的跳跃次数,使得跳跃过程更加高效。
- 最优子结构性质: 问题具有最优子结构性质,每次跳跃后,选择最远位置能够构成全局最优解。
- 贪心选择性质: 每次跳跃选择最远位置,能够确保跳跃次数最少,因此局部最优选择能够导致全局最优。
- 无后效性: 新的跳跃决策将根据新的位置和新的跳跃范围做出,因此,之前的选择已经不再影响当前的决策。
正确性证明
-
数学归纳法: 假设贪心策略在第 k 步最优,若存在更优解,则必定与贪心选择相矛盾。因贪心策略选择的跳跃点总能覆盖更远的位置,因此从当前位置开始,贪心策略不会导致次优解。由此,可以通过归纳法证明贪心策略的正确性。
-
反证法: 假设存在一个更优解,该解的跳跃次数比贪心策略少。但因为贪心策略每次选择最远位置,这意味着任何更优解必定违背了贪心选择,矛盾。因此,贪心策略是最优的。
完整证明
基础假设:
- 假设贪心策略在第 k 步是最优的,即当前贪心策略选择跳到的最远位置能保证跳跃次数是最少的。
归纳目标:
- 证明如果贪心策略在前 k 步是最优的,则在第 k+1 步的选择也是最优的。
证明步骤
- 基础情况:
当 k = 1 时,表示我们从起始位置 0 开始,选择跳跃的第一个位置。贪心策略选择从当前位置跳到可以到达的最远位置(即选择能跳到的最大位置)。如果这个选择是最优的,那么跳跃次数为 1。
- 归纳假设:
假设在第 k 步,贪心策略选择的跳跃点是最优的,即从当前位置跳跃到的最远位置能保证最少的跳跃次数。
- 归纳步骤:
我们需要证明在第 k+1 步,贪心策略的选择仍然是最优的。假设存在一个不同的跳跃选择能够导致更少的跳跃次数。
• 贪心策略在当前位置选择的跳跃点 i + nums[i] 是当前跳跃范围内能够跳到的最远位置。
• 如果假设存在一个更优的解(即在第 k+1 步选择了不同的位置 j,使得跳跃次数更少),那么说明从当前位置 i 跳到位置 j 会导致更少的跳跃次数。
但根据贪心策略的选择,它已经选择了最远的跳跃点,从而让后续的跳跃范围最大化。如果我们选择 j 而不是贪心选择的 i + nums[i],则意味着我们选择的跳跃点没有最大化后续的可达范围,跳跃次数就会增加,或者我们可能会需要多次中途停顿。
由于我们选择的跳跃点是贪心策略下能跳到的最远位置,这就保证了我们总是跳到最优的地方,不可能有更优的跳跃点。
- 归纳结论:
通过以上推导可知,如果第 k 步的选择是最优的,那么第 k+1 步的选择也是最优的。因此,根据数学归纳法,贪心策略在每一步选择跳跃到最远的点,总能得到最优解。