题目
给定一个非负整数数组 nums ,你最初位于数组的 第一个下标 。
数组中的每个元素代表你在该位置可以跳跃的最大长度。
判断你是否能够到达最后一个下标。
示例1
输入:nums = [2,3,1,1,4]
输出:true
解释:可以先跳 1 步,从下标 0 到达下标 1, 然后再从下标 1 跳 3 步到达最后一个下标。
示例2
输入:nums = [3,2,1,0,4]
输出:false
解释:无论怎样,总会到达下标为 3 的位置。但该下标的最大跳跃长度是 0 , 所以永远不可能到达最后一个下标。
思路
数组类的算法题,肯定少不了遍历;那么就开始遍历吧
DFS解法
对于第 i
位置上的元素 nums[i]
,会想到什么?
按照题目,大部分人的第一反应会是:下一步是哪?;
按照题意,第 i
位置上,能够到达的位置为 i + 1
、i + 2
... i + nums[i]
那么接下来就会想,那应该 跳
到哪个位置是最合适的呢?
这个时候,上一步这个问题是无法判断出来的,那怎么办?
没办法,只有每一个都去尝试一遍~
上述的过程,整理一下,其实就是 DFS(深度优先搜索)
算法的思想,递归下去,回溯上来,不过这道题的回溯很少,如果仅仅采用 DFS
,算法的时间复杂度会很高,下面就会看得到:
第一版 DFS
代码如下
/**
* @param {number[]} nums
* @return {boolean}
*/
var canJump = function (nums) {
// 定义标识,是否到达了最后一个位置
let ans = false;
let n = nums.length;
/**
*
* @param {Number} idx 当前起跳位置
* @returns null
*
* @description 传递一个起跳位置,从该位置开始,跳到所有可能的下一个位置
*/
const jump = (idx) => {
// 如果当前位置比数组长度大,不合法,不需要继续进行,第一次剪枝
if (idx >= n) return;
// 如果当前位置就是最后一个位置,或者已经跳到最后一个位置,不需要继续进行,第二次剪枝
if (idx === n - 1 || ans) {
ans = true;
return;
}
// 当前能跳的最大距离
let loop = nums[idx];
// 如果当前最大距离 比 idx -> n 之间的距离大,那么肯定能到达最后一个位置,第三次剪枝
if (loop >= (n - idx)) {
ans = true;
return;
}
// 依次,跳转到下一个位置,继续调用 jump 递归
for (let i = 1; i <= loop; i++) {
jump(idx + i);
}
}
// 从位置0开始,启动递归函数
jump(0);
return ans;
};
上面的解法,时间复杂度极高,提交 leetCode
的时候,直接显示超出时间限制了😭😭😭
思考了一下:其实还有一个很重要的 剪枝
操作忘记了,对于位置 i
,其实只需要进行一次调用 jump
函数就可以了,后面的都是重复操作;
那么可以使用一个辅助数组,用于标记 i
是否进行过调用 jump
,
第二版 记忆DFS
代码如下
增加了三行代码,都是 `vis` 相关,但是将时间复杂度优化到了接近 `O(n)`,这就是空间换时间了
/**
* @param {number[]} nums
* @return {boolean}
*/
var canJump = function (nums) {
// 定义标识,是否到达了最后一个位置
let ans = false;
let n = nums.length;
// 辅助函数,用于判断 i 是否经过计算
let vis = new Array(n).fill(false);
/**
*
* @param {Number} idx 当前起跳位置
* @returns null
*
* @description 传递一个起跳位置,从该位置开始,跳到所有可能的下一个位置
*/
const jump = (idx) => {
// 如果当前位置比数组长度大,不合法,不需要继续进行,第一次剪枝
// 增加一个剪枝操作,vis[i] 计算过
if (idx >= n || vis[idx]) return;
vis[idx] = true;
// 如果当前位置就是最后一个位置,或者已经跳到最后一个位置,不需要继续进行,第二次剪枝
if (idx === n - 1 || ans) {
ans = true;
return;
}
// 当前能跳的最大距离
let loop = nums[idx];
// 如果当前最大距离 比 idx -> n 之间的距离大,那么肯定能到达最后一个位置,第三次剪枝
if (loop >= (n - idx)) {
ans = true;
return;
}
// 依次,跳转到下一个位置,继续调用 jump 递归
for (let i = 1; i <= loop; i++) {
jump(idx + i);
}
}
// 从位置0开始,启动递归函数
jump(0);
return ans;
};
动态规划
上面的解法,思考的主要方向是 下一步是哪
那么对于位置 i
,思考一下,当前 i
是能够到达的么?如果能够到达,条件是什么呢?
不难想出:对于 i
如果是能够到达,必定在 i
之前存在一个 j
,使得 j + nums[j] >= i
那么就可以使用动态规划去解决这道题:
动态规划
解法代码如下
/**
* @param {number[]} nums
* @return {boolean}
*/
var canJump = function(nums) {
let n = nums.length;
// 新建一个dp数组,dp[i] 表示 i 位置能否到达
let dp = new Array(n).fill(false);
// 初始条件,位置 0 肯定能到达
dp[0] = true;
for(let i = 1; i < n; i++) {
// 遍历小于 i 的元素
for(let j = 0; j < i; j++) {
// 如果当前 dp[j] 为 true,表示dp[j] 可到达
// 如果 nums[j] + j >= i,表示 dp[j] 可到达
// 找到一个就可以了,中断循环 break
if(dp[j] && nums[j] + j >= i) {
dp[i] = true;
break;
}
}
}
// 题目所求即为 dp数组最后一个元素的值
return dp[n - 1];
};
发现没有,相比较于第一种算法,动态规划的解法代码量少了很多!!!
贪心
贪心算法的思想是,每次都选当前看起来最好的那个选择,不管整体影响,那么再来看这道题
对于位置 i
的元素能否到达,是不是也可以用一个动态变化的 当前最大能跳多远 -> jumpMax
的变量来判断呢?
比如,对于位置 2
,jumpMax
如果比 2
小,则位置 2
不能到达,否则能到达
那么在不能到达的情况下,还需要继续判断位置 2
以后的元素么?
答案是不需要的,自己体会下,比如对于 [0, 0, 1, 3]
,当前 i == 1; jumpMax == 0
, 位置 1
显然不能到达,位置 1
以后的也不能达到
贪心算法
代码如下
/**
* @param {number[]} nums
* @return {boolean}
*/
var canJump = function(nums) {
let n = nums.length;
// 当前最大能走多远,初始时为 nums 第一个元素的值
let jumpMax = nums[0];
for(let i = 0; i < n; i++) {
// 如果,当前最大能走的距离,比当前的索引 i 还少,则 位置 i 无法到达
if(jumpMax < i) return false;
// 否则更新一下当前最大能走的距离 -> i + nums[i] 为 i 位置上当前最远能走的距离
jumpMax = Math.max(jumpMax, i + nums[i]);
// 如果当前最大能走的距离,大于数组的长度,则最后一个位置能到达
if(jumpMax >= n - 1) return true;
}
// 上面的遍历,每个元素都能到达,最后一个元素也能到达
return true;
};
小结
上面三种解法,一个比一个代码量少,最后一个的时间复杂度,降到了最低的 O(n)
,效率最高,但同时也是最难想到的一种解法
算法这个东西,你不练,它就不理你~
LeetCode 👉 HOT 100 👉 跳跃游戏 - 中等题 ✅
合集:LeetCode 👉 HOT 100,有空就会更新,大家多多支持,点个赞👍
如果大家有好的解法,或者发现本文理解不对的地方,欢迎留言评论 😄