携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第16天,点击查看活动详情 。如果哪里写的不对,请大家评论批评。
最近刷题发现很多题都是使用动态规划来完成的,去学习动态规划的时候,大家都介绍斐波那契数列算是动态规划的入门题。(斐波那契数列并不是严格意义上的动态规划,因为它不涉及到求最值,用这个例子旨在说明重叠子问题与状态转移方程)
斐波那契数列
公式
斐波那契数列由0和1开始,之后的斐波那契数就是由之前的两数相加而得出。比如说在斐波拉契数列当中第一个数为0,第二个数为1,因此第三个数为前面两个数之和,因此第三个数为1,同理第四个数是第二个数和第三个数之和,因此第四个数为2,下面就是斐波拉契数的变化:
0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, ....
递归法
class Solution {
func fibonacci(_ n: Int) -> Int {
if (n < 2) {
return n
}
return fibonacci(n-1) + fibonacci(n-2)
}
}
分析
当我们求第6个斐波拉契数的时候,函数fibonacci的调用过程如下所示:
我们在调用fibonacci(6)的时候他会调用:[fibonacci(5)和fibonacci(4)],然后fibonacci(5)会调用[fibonacci(4)和fibonacci(3)],fibonacci(4)会调用[fibonacci(3)和fibonacci(2)]......
我们容易发现我们在函数调用的过程当中存在重复,比如下图两个相同的部分表示对fibonacci(4)重新计算,因为他们的调用树都是一样的:
可以看出求F(6)就会出现很多重复的计算,比如F(4)计算了两次,F(3)计算了三次,F(2)计算了五次,都是指数级的增长,斐波拉契数列的时间和空间复杂度都是O(2n)。
求解问题F(6),转成求F(5),F(4),从原问题可以分解成求子问题,子问题再分解成子子问题,。。。,直到再也不能分解,这种从原问题展开子问题进行求解的方式叫自顶向下
动态规划
我们反向思考一下,如果F(0),F(1)...F(5)都已经计算好并已经做了缓存,当我们使用F(6)的时候是不是直接取F(5)+F(4)就可以了?时间复杂度O(1)。计算好之后也缓存进来,如果在增加F(7)也可以直接提取F(6)+F(5),时间复杂度也是O(1)。
f(0) = 0
f(1) = 1
f(2) = 1
f(3) = f(1) + f(2) = 2
f(4) = f(3) + f(2) = 3
f(5) = f(4) + f(3) = 5
f(6) = f(5) + f(4) = 8
....
f(n) = f(n-1) + f(n-2)
图解
代码
class Solution {
func fibonacci(_ n: Int) -> Int {
if (n < 2) { return n }
var dp = Array(repeating: 0, count: n+1)
dp[1] = 1
for i in 2...n {
dp[i] = dp[i-1] + dp[i-2]
}
return dp.last!
}
}