动态规划

189 篇文章 0 订阅
162 篇文章 0 订阅

对于一个函数来说,F(0)=1,F(1)=1,F(n)=F(n-1)+F(n-2),对于n>1来说是遵循这样一个式子。

 

 

编程实现斐波那契数列。递归的终止条件,就是当n==0的时候,return 0,当n==1的时候,return 1,否则当n>1的时候,return的就是函数f(n-1)+f(n-2)。通过递归定义,得到了这样一个递归程序。

 

当n=10时所需要的时间,这个性能非常的好。可以使用倍增数据规模的方式来考察函数相应的复杂度是怎么样的。

 

当n=10时,所需要的时间。当数据扩大2倍的时候,时间扩大的不是2倍,也不是4倍,说明它不是一个O(n)复杂度的算法,也不是一个O(n²)复杂度的算法。实际上实际多了25倍,说明这个算法要比O(n²)大得多。

 

当n=40的时候,运行所需要的时间。时间比当n=20时扩大了近1万倍。此时这个函数,它的时间复杂度是指数级的,也就是O(2^n)这个级别的。正因为如此,可以看到在实验过程中,当n=10扩大了两次,扩大到了n=40的时候,整个程序已经不能在1s之内解决问题了。

 

当n=42的时候,所需要的时间是3.4s。换句话说,只是在n=40的基础上,增添了两个数据,数据规模只是增加了2,但是时间性能却一下变成了原来的3倍。这个指数级的时间复杂度的增长,速度是非常非常快的。斐波那契这个数字本身增长也非常快,当f(42),即n=42的时候,这个数字已经非常大了。如果要计算更大的斐波那契数列的时候,返回值就需要设置为long类型,这样不会有整型溢出的问题。

 

 

斐波那契数列效率为什么这么慢?

如果要计算f(5)的话,就要相应地计算f(4)和f(3)

 

 

如果要计算f(4)的话,就需要相应地计算f(3)和f(2)

 

依次类推,得到整个斐波那契数列的递归树。在这棵树中,每一个都到叶子节点,也就是递归的终止条件。每个节点其实都是一次计算,在这棵树中进行了多次重复计算。比如,对于f(3)这个节点,在节点4之下计算了一次,在5的右侧又计算了1次,这两次计算就是重复的计算。同理,对于f(2)来说,在整个树列,其实计算了3次。事实上,这只是对于斐波那契数列5的计算,如果想要计算斐波那契数列f(100)的话,相应的这种重复计算量是非常非常大的。所以,就会有一个n很自然的想法,对于这种重复的计算,有没有可能只计算一次呢?

 

 

统计该函数调用了多少次:

 

当n=20的时候,几乎运行了2万多次

 

当n=40时,运行的次数。运行了这么多次这个函数,这是一个很大的数字。但是,这只是计算斐波那契40而已,这么多次运算,有大量的重复运算在里面。那么该如何避免大量重复运算呢?

 

通过一个数组保存n相对应的结果。这样的话,对于一个n来说,只会使用这种递归的方式计算一次。在第一次计算的时候,会为memo[n]附上一个值,当下一次n来临的时候,这个n就不再等于-1了,直接将值return回去就好了。设置为-1是因为,对于任意斐波那契数列来说,任何一个数都不可能为-1. 这个代码依然使用的是递归的方式,只不过有很多重复的计算过程,使用一个数组将这些重复的过程记录下来,下次再进行计算的时候就不再使用递归计算的方式来计算这个值了,而是直接通过之前记录的值,把它return回去。这样的一个方式就叫做记忆化搜索。

 

使用了一个递归搜索的过程,但是使用了一个memo进行记忆。所以叫做,记忆化搜索。

 

此时所需要的时间,n依然=40,但是可以在很短的时间将结果计算出来。不适用记忆化搜索的话需要的时间是1s多,它们之间是100万倍的时间差距。而此时斐波那契函数只调用了79次,是非常少的一个次数。此时的算法复杂度,其实已经成为一个O(n)级别的算法复杂度。这是因为对这个memo数组来说,每一个斐波那契函数只计算了1次,

 

当n=1000时所需要的时间,依然是6*10^-5,依然是一个纳秒级别的。这就是指数级的时间和线性的时间,它们之间的差别。不过此时fib(1000)计算出来的值是错误的,因为这个值非常大,已经超出了整数的最大值范围。

 

记忆化搜索实质上是在递归的基础上,添加上记忆化这个过程。递归是自上而下地解决问题,换句话说没有从最基本的问题开始解决,是假设最基本的问题已经解决了。这里是假设会求fib(n-1)和fib(n-2)这个数了,此时要求第fib(n)该怎么做呢?将fib(n-1)和fib(n-2)相加就可以了。在这个基础上,设置递归终止的条件,这是一个自上而下解决问题的过程。不过,对于这类问题,如果可以自上而下解决问题的话,也就能自下而上地解决问题。其实自上而下地思考问题是更容易的,而自下而上地思考问题是会更难的。

 

 

不过,对于该问题来说,自下而上思考问题非常简单。

这样的一个过程就是自下而上地解决问题,先解决一个小数据量的问题,然后层层递推,来解决对于更大地数据量而言,这个结果是怎样的。那么,通常,这个过程就被成为动态规划。这个代码的时间复杂度,也是O(n)级别的。

 

对于n=1000,这个计算速度是非常快的。这个计算时间的速度和记忆搜索法的时间速度是大致是在一个级别的,当n更大的时候,此时是一个O(n)级别的算法,对于一般的计算机来说完全可以承受百万、千万这样的计算。此时,使用这种动态规划的方式来解决问题它的性能其实是更优的。首先没有递归的调用,调用函数是有额外的开销的,其次,从空间来讲,递归调用会占用系统的栈空间的。其次,使用这种循环的话,其实每个memo[i]只访问了1次。但是,之前那种递归调用来处理的话,会产生额外的对这个memo的访问。在之前的记忆化搜索中,斐波那契数列调用的函数是2^n-1次,而不是n次。不管怎样,使用动态规划的方式,从性能来说也是一种更好的实现方式,

 

 

 

 

大多数动态规划问题,本质都是一个递归的问题。不过在递归的过程中,会发现重叠子问题。对于重叠子问题,可以使用记忆化搜索的方式进行处理,这是一种自顶向下的解决问题的方式,另外一种处理方式其实就是动态规划,动态规划和记忆化搜索本质上是一样的,只不过是自底向上的。其实对于很多问题来说,自顶向下地进行思考是更加容易的,前面所讲的诸多递归问题,其实都是自顶向下地在思考这些问题。有的时候使用动态规划的时候,会先自顶向下地思考这个问题,之后再用自底向上进行实现。在大多数情况下,记忆化搜索所获得的答案,通常都是能满足实际需求的。只不过使用动态规划的方式来实现,整个代码会更加简洁、更加清晰而已。甚至,在很多时候,给出了一个记忆化搜索的答案,对于一些面试官而言,也是一个动态规划的答案。

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值