保研面试 算法题_面试高频算法题:动态规划和递归例题讲解

Hello, 刚刚结束完一天的工作,忙着把算法题写完.那么今天继续讲解算法题第55题, 也是一道经典的面试题目,那我们来一起看一看吧.

Given an array of non-negative integers, you are initially positioned at the first index of the array.

Each element in the array represents your maximum jump length at that position.

Determine if you are able to reach the last index.

「Example1」

Input: nums = [2,3,1,1,4]
Output: true
Explanation: Jump 1 step from index 0 to 1, then 3 steps to the last index.

「Example2」

Input: nums = [3,2,1,0,4]
Output: false
Explanation: You will always arrive at index 3 no matter what. Its maximum jump length is 0, which makes it impossible to reach the last index.

来简单描述一下这道题目的意思, 大致上就是给定一个数组, 数组中的元素代表你可以往后跳的一个步数,比方nums[0]=2 代表它可以选择跳到nums[1] 和 nums[2] 这两步

我们来画个图,举个例子给大家看看

f397a18165ab539ffc41bb3e08353da3.png

我用四种不同的颜色代表每个元素可以跳跃的步数, 这下子是不是就很清楚了

接着我们来回顾一下「递归」「动态规划」的思想

  1. 「创建一个数组 存储动态规划的过程」
  2. 「寻找递归结束的条件(可以先画个树图出来帮助理解)」

我们先来看一下 动态规划我们要准备什么? 其实我们就是要保存, 每个点是否能跳跃到终点的记录

我们用-1,0,1来代表3种状态,分别是「不能到达」,「初始化」,「可以到达」三个阶段

a21e86ab167a45231d7f59a02b2ac3f0.png

我们先初始化dp的数组, 并且将终点设置为1, 因为终点肯定能到终点 接下来,我们来思考一下

「递归的结束条件应该是什么?」

我们先把递归图画出来

f5d281bdabfa2af1c0ea9e85835024bb.png

从这道例子中, 我们递归树应该是这样子

dabd4e240fd592faa88b5978abab04b6.png

我们的递归顺序,就是从左边的1开始,往右边数,最后才做2自己本身, 在这个例子中你会发现, 你无论选那一条路 你都能到终点对吧, 那我们就换一题来讲

08bfe86b8f9621542f71022dfc833933.png

我们对着下面这个题目, 画一下动态规划表

d7784c808f244dd482173f177af0f95d.png

所以, 我们的动态规划表应该是上图那样的结果. 1代表可到达的路径, -1代表不可到达

所以说, 我们递归结束的条件 应该是 「dp[i]==-1」「dp[i]==1」 为结束条件

如果 「dp[i]==0」 说明该节点尚未被计算,需要再次把这个节点进行递归 如果递归到了死胡同了, 我们就把该点标记成为-1退出则可

让我们来看一下递归代码

func recurrentJump(position int,nums []int, dp *[]int) bool{
 if (*dp)[position] == 1{
  return true
 }else if (*dp)[position] == -1{
  return false
 }
 maxJump := min(position + nums[position], len(nums)-1)
 for i:= position + 1; i<=maxJump; i++{
  result:=recurrentJump(i, nums, dp)
  if result {
   (*dp)[position] = 1
   return true
  }
 }
 (*dp)[position] = -1
 return false
}
func min(x int, y int) int{
 if (x < y){
  return x
 }
 return y
}

递归的主函数就在上面, 和我们描述的一致, 如果说「dp[i]==-1」「dp[i]==1」直接可以返回否则说明该节点尚未被计算

我们来看一下节点未被计算的时候需要做的循环

maxJump := min(position + nums[position], len(nums)-1)
for i:= position + 1; i<=maxJump; i++{
 result:=recurrentJump(i, nums, dp)
 if result {
  (*dp)[position] = 1
  return true
 }
}

首先 求最小的跳跃 次数 这里的「position + nums[i]」「nums长度」比较,取小的一位代表着该节点,最大能跳到哪里 然后我们再循环递归, 把该节点能跳的位置, 全部再递归一次

如果说返回值等于 「true」 则代表本节点可以跳跃到终点, 记录本节点为 「dp[position]=1」 然后返回 「true」

告诉上一层的节点, 你也可以通过我到达终点, 如下图所示

505b3503647bc889d6f8fa001ee281f5.png

然后我们在看一下主函数

func canJump(nums []int) bool {
 var dp = make([]int, len(nums))
 for i:=0; i<len(nums); i++ {
  dp[i]  = 0
 }
 dp[len(nums)-1] = 1
 ans:= recurrentJump(0, nums, &dp)
 return ans
}

这里, 没什么好讲的 就是初始化DP数组, 然后从下标0开始递归,

34aed0fe3481e2e8b84c039c4f1ddcd5.png

递归以及动态规划的例子, 那么很明显的问题就是...效率极低 那么我们也没有什么思路可以让效率更高一些呢?

顺便附带上GitHub的代码地址:LeetCode代码集合-持续更新中

那么就来讲解另外2种思路「1. 动态规划」「2. 优化版动态规划(优化的是空间复杂度)」
那我们就顺着这个思路往下走, 先来了解一下动态规划的算法去取代掉递归的算法

动态规划版

大家应该忘记,我们动态规划是需要一个表来存储以往的记录的吧,在这个算法里, 我们照样需要使用到这个数组

33755b8cb83979851eeace57483633c4.png

那么这个做法就是不使用递归, 用for循环做, 我们继续看代码

func nonRecurrentJump(nums[] int, dp *[]int) bool{
 for i:= len(nums)-2;i>=0;i-- {
  maxJump := min(i+nums[i], len(nums)-1) //获取跳跃的步数
  for j := i + 1; j<=maxJump; j++{
   if (*dp)[j] == 1{
    (*dp)[i] = 1
    break
   }
  }
 }
 if (*dp)[0] == 1{
  return true
 }
 return false
}

在这个算法中, 跟递归的很像,唯一的差别就是在于在第二个For的循环中,我们不在是进入递归了, 而是直接查看「dp[j]==1」是否成立, 而这段代码中我们的思路其实转变了, 递归的思想是从头开始找, 而这段代码是从后往前

de7c63e05a5b2621501c6a05c74f335a.png

我们把问题简化一下, 从需要到达「dp[len(nums)-1]」 变成 「dp[len(nums)-i]」
因为只要我们能到达 「2」 这个元素,说明我们可以达到终点, 那么从2往前的元素,只需要知道自己能不能到达2

这就是这个算法的核心所在, 如果可以到达 则把 「dp[i]=1」 并且跳出内层循环,直到最后的结果应该是这样

5697e62bf2a4baf8e581ade0205ff837.png

「中间的 1 和 0 是不会被赋值的, 因为它们无法到达 2」

最后, 我们判断 「dp[0]==1」 是否成立,如果成立代表从第0个出发,可以到达终点, 很明显我们比起递归而言要快将近「5倍」的时间

优化动态规划

我们在想深一层次, 我们真的需要这个数组吗?

其实不需要对吧, 为什么? 因为我们一直都是在与 「最新」 的一个元素做对比, 那我们为什么还需要一个数组去存储呢?
也就是说,我们可以去掉数组,将代码简化成这样...

func optimizeNonRecurrentJump(nums[] int) bool{
	aim := len(nums)-1 //终点
	for i:= len(nums)-2;i>=0;i-- {
		maxJump := min(i+nums[i], len(nums)-1)
		for j := i + 1; j<=maxJump; j++{
			if j == aim{
				aim = i //更新最近的终点
			}
		}
	}
	if aim == 0{
		return true
	}
	return false
}

新建 一个变量叫aim 永远拿它指向最后一个终点, 如果有比它更前的元素接近终点, 则更新它
最后我们判断 「aim==0」 是否成立,就知道我们第一个元素是否可以到达终点拉

b883f1dc687277b79e3008a80a910df5.png

这个方案比之前的省下了「0.2MB」的空间 又一次的进步。

创作不易, 希望留下你们的小赞赞,是最大的鼓励
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值