暴力递归
/*
斐波那契数,通常用 F(n) 表示,形成的序列称为 斐波那契数列 。该数列由 0 和 1 开始,后面的每一项数字都是前面两项数字的和。给出n求F(n).
*/
// 严格来讲,并不是一个正宗的动态规划问题,但是可以帮助我们理解动态规划的特性。
// 这题可以显示出一个动态规划问题的优化解题步骤:
/*暴力递归(自顶向下递,自下向上归) ----> 带备忘录的递归(自顶向下递,自下向上归)
*/
// 先看暴力递归
class Solution{
public int fib(int n){
// base case
if(n == 0 || n == 1)return n ;
// 递推关系
return fib(n-1)+fib(n-2);
}
}
// 递归的时间复杂度:递归函数的调用次数*递归函数本身的复杂度。
这题中递归函数本身就是两行代码,并没有任何的循环,所以时间复杂度是o(1),递归函数的调用次数大概是2^n,所以时间复杂度是o(2^n),二者相乘最终是o(2^n)
带备忘录的递归解法
将是一个树状结构转化为链表结构,将树的遍历计算转化为链表的遍历计算,速度大大提升。
// 在递归开始之前,先去备忘录查一下,看是否已经计算过了,如果已经计算过,就不再计算了。
class solution{
// 外层调用该方法,计算递归数据。
public int fib(int N){
// 备忘录全初始化为0,因为索引从0开始,而该数组存储的是斐波那契数,所以要+1.在动态规划中,无论是二维数组还是一维数组都是很常见。
int[] memo = new int[N + 1];
// 进行带备忘录的递归
return helper(memo,N);
}
private int helper(int[] memo,int n){
// base case
if(n == 0 || n == 1)return n;
// 已经计算的,不需要再计算了,剪枝操作
if(memo[n] != 0)return memo[n];
memo[n] = helper(memo,n - 1) + helper(memo,n - 2);
return memo[n];
}
}
// 这回算法时间复杂度变为o(n),但空间复杂度变为o(n)
dp 数组的迭代解法
class solution{
public int fib(int N){
if(N == 0)return 0;
// dp 数组里面的内容和memo是完全一样的,只不过把递归改成了for循环迭代而已。
int[] dp = new int[N + 1];
// base case
dp[0] = 0;
dp[1] = 1;
// 状态转移
for(int i = 2;i <= N;i++)
dp[i] = dp[i - 1] + dp[i - 2];
return dp[N];
}
}
// 进一步优化,可以不利用数组,将空间复杂度降为o(1)
class solution{
public int fib(int n){
// base case
if(n == 0 || n == 1)return n;
// 递推关系
int prev = 0,curr = 1;
for(int i = 2;i <= n;i++){
int sum = prev + curr;
prev = curr;
curr = sum;
}
return curr;
}
}
最后这种算法,才是真正的符合人的思维,递归这种思想想象起执行流程简直反人类,如果谁让我计算斐波那契数列的第5项,我能第一时间想到的解法就是,从后往前加就行了。而不是递到最后一层,再归到脸上来,太反人类了。
图片来自B站labuladong ,这人讲算法有一套,还出了一本算法书大家可看一看。
大家可以复制代码,去idea中debug一下,那样会对这个思想的体会更加深刻。