【动归】C000_斐波那契数(递归 | 记忆化递归 | dp)

一、题目描述

The Fibonacci numbers, commonly denoted F(n) form a sequence, called the Fibonacci sequence, 
such that each number is the sum of the two preceding ones, starting from 0 and 1. That is,

F(0) = 0,   F(1) = 1
F(N) = F(N - 1) + F(N - 2), for N > 1.
Given N, calculate F(N).

Input: 2
Output: 1
Explanation: F(2) = F(1) + F(0) = 1 + 0 = 1.

二、题解

方法一:递归

  • 如果 N 等于 1 或等于 2,则返回 1。
  • 否则,通过 F n = F n 1 + F n 2 F_n =F_{n−1} +F_{n−2} 调用自身求解问题。
public int fib(int N) {
//    if (N == 0) return 0;
//    if (N == 1) return 1;
  if (N == 1 || N == 2) return 1;
  return fib(N - 1) + fib(N - 2);
}

复杂度分析

  • 时间复杂度: O ( 2 n ) O(2^n) ,整体算法复杂度就是子问题个数乘以解决一个子问题需要的时间。子问题个数,即递归树中的节点总数。
    • 例如: f ( 1 ) + f ( 2 ) f(1) + f(2) 就是一个加法操作,时间为 O(1),但整颗递归树为一棵完全二叉树。
  • 空间复杂度: O ( n ) O(n) ,栈空间随 N 增加而不断增长。

方法二:记忆化递归(自顶向下)

在这里插入图片描述

通过观察递归树可以发现暴力递归存在大量重复计算,这是导致算法低效的原因。如何解决? 既然有那么多重复计算,不如把这些重复计算的结果先记住,留给后面的的递归使用。
在这里插入图片描述

int[] memo = null;
public int fib(int N) {
  memo = new int[N + 1];
  return search(N);
}
private int search(int N) {
  if (N == 1 || N == 2) 
    return 1;
  if (memo[N] == 0)
    memo[N] =  search(N - 1) + search(N - 2);
  return memo[N];
}

复杂度分析

  • 时间复杂度: O ( N ) O(N) ,我们不再进行重复计算。
  • 空间复杂度: O ( N ) O(N) ,使用了空间大小为 N 的数组。

方法三:动态规划(自底向上)

上面的记忆化搜索叫做「自顶向下」,动态规划叫做「自底向上」。

啥叫「自顶向下」?注意我们刚才画的递归树,都是从上向下延伸,从一个规模较大的原问题向规模较小的子问题递归。

  • 比如说 f(20),向下逐渐分解规模,直到 f(1) 和 f(2) ,然后逐层返回答案,这就叫「自顶向下」

啥叫「自底向上」?反过来,我们直接从最底下,最简单,问题规模最小的 f(1) 和 f(2) 开始往上推,直到推到我们想要的答案 f(20),这就是动态规划的思路。这也是为什么动态规划一般都脱离递归,使用迭代完成计算。
在这里插入图片描述
啥叫「状态转移方程」?我们知道动态规划一般都会有一个描述问题结构的数学形式,这个状态转移就是这个数学方程的不同表现形式罢了。

public int fib2(int N) {
  if (N <= 1) return N;
  
  int[] memo  = new int[N + 1];
  memo[1] = memo[2] = 1;

  for (int i = 3; i <= N; i++) {
    memo[i] = memo[i - 1] + memo[i - 2];
  }
  return memo[N];
}

复杂度分析

  • 时间复杂度: O ( N ) O(N)
  • 空间复杂度: O ( N ) O(N)

方法四:dp(空间压缩)

其实我们不需要大小为 N 的 memo 数组来存储所有的状态,只需存储某个状态之前的两个状态即可。

public int fib3(int N) {
  if (N <= 1) return N;
  if (N == 2) return 1;
  
  int curr = 0;
  int pre1 = 1;
  int pre2 = 1;

  for (int i = 3; i <= N; i++) {
    curr = pre1 + pre2;
    pre2 = pre1;
    pre1 = curr;
  }
  return curr;
}

复杂度分析

  • 时间复杂度: O ( N ) O(N)
  • 空间复杂度: O ( 1 ) O(1)

结语:这里好像没有展示动态规划的另一个重要特性 「最优子结构」,斐波那契数列的例子严格来说不算动态规划,因为没有涉及求最值的环节。

发布了461 篇原创文章 · 获赞 102 · 访问量 3万+

猜你喜欢

转载自blog.csdn.net/qq_43539599/article/details/104564265