目录
动态规划理论基础
LeetCode509. 斐波那契数
1. 思路
斐波那契数列大家应该非常熟悉不过了,非常适合作为动规第一道题目来练练手;因为这道题目比较简单,可能一些同学并不需要做什么分析,直接顺手一写就过了;但「代码随想录」的风格是:简单题目是用来加深对解题方法论的理解的。
通过这道题目让大家可以初步认识到,按照动规五部曲是如何解题的。对于动规,如果没有方法论的话,可能简单题目可以顺手一写就过,难一点就不知道如何下手了。
动态规划五部曲
-
确定dp数组及下标的含义
这里我们要用一个一维dp数组来保存递归的结果,dp[i]的定义为:第i个数的斐波那契数值是dp[i];
-
确定递推公式
为什么这是一道非常简单的入门题目呢?因为题目已经把递推公式直接给我们了:状态转移方程 dp[i] = dp[i - 1] + dp[i - 2];
-
dp数组如何初始化
题目中把如何初始化也直接给我们了,如下:dp[0] = 0;dp[1] = 1;
-
确定遍历顺序
从递归公式dp[i] = dp[i - 1] + dp[i - 2];中可以看出,dp[i]是依赖 dp[i - 1] 和 dp[i - 2],那么遍历的顺序一定是从前到后遍历的;
-
举例推导dp数组
按照这个递推公式dp[i] = dp[i - 1] + dp[i - 2],我们来推导一下,当N为10的时候,dp数组应该是如下的数列:0 1 1 2 3 5 8 13 21 34 55;
如果代码写出来,发现结果不对,就把dp数组打印出来看看和我们推导的数列是不是一致的;
2. 代码实现
2.1 代码实现一: 动态规划
# 动态规划
# time:O(N);space:O(N)
class Solution(object):
def fib(self, n):
"""
:type n: int
:rtype: int
"""
if n<=1: return n
dp = [0]*(n+1)
dp[0] = 0
dp[1] = 1
for i in range(2,n+1):
dp[i] = dp[i-1] + dp[i-2]
return dp[n]
2.2 代码实现二:动态规划 状态压缩
写出以上的代码之后,我们当然可以发现,我们只需要维护两个数值就可以了,不需要记录整个序列;
# 状态压缩 动态规划
# 不用记录整个斐波那契数列
# 只用记录当前值的前两个元素的值
# time:O(N);space:O(1)
class Solution(object):
def fib(self, n):
"""
:type n: int
:rtype: int
"""
if n<=1: return n
first,second = 0,1
for _ in range(n-1):
result = first + second
first = second
second = result
return result
2.3 代码实现三:递归做法
斐波那契数的题目用递归也是经典解法;
# 递归做法
# time:O(2^N);space:O(N)
class Solution(object):
def fib(self, n):
"""
:type n: int
:rtype: int
"""
if n == 0: return 0
if n == 1: return 1
return self.fib(n-1)+self.fib(n-2)
3. 复杂度分析
3.1 代码实现一: 动态规划
-
时间复杂度:O(N)
N为第N个斐波那契数,for循环在遍历时,遍历次数为O(N)的数量级;
-
空间复杂度:O(N)
需要一个大小为N的数列dp储存斐波那契数列;
3.2 代码实现二:动态规划 状态压缩
-
时间复杂度:O(N)
N为第N个斐波那契数,for循环在遍历时,遍历次数为O(N)的数量级;
-
空间复杂度:O(1)
只用了三个变量储存前中后连续三个斐波那契数;
3.3 代码实现三:递归做法
(容易错)透彻理解递归的复杂度:
相信很多同学对递归算法的时间复杂度都很模糊,那么这篇来给大家通透的讲一讲。同一道题目,同样使用递归算法,有的同学会写出了O(n)的代码,有的同学就写出了O(logn)的代码。
这是为什么呢?
如果对递归的时间复杂度理解的不够深入的话,就会这样!
那么我通过一道简单的面试题,模拟面试的场景,来带大家逐步分析递归算法的时间复杂度,最后找出最优解,来看看同样是递归,怎么就写成了O(n)的代码。
💡 面试题:求x的n次方
方法一:for循环迭代解法;time complexity