欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 科技 > 名人名企 > 数据结构和算法|递归算法那些事(递归算法的时间复杂度、尾递归优化、斐波那契数列)

数据结构和算法|递归算法那些事(递归算法的时间复杂度、尾递归优化、斐波那契数列)

2024/10/24 17:32:58 来源:https://blog.csdn.net/caiziming_001/article/details/141022615  浏览:    关键词:数据结构和算法|递归算法那些事(递归算法的时间复杂度、尾递归优化、斐波那契数列)

对于文章的第一部分,递归算法的时间复杂度,来自于代码随想录文章:通过一道面试题目,讲一讲递归算法的时间复杂度!
对于第二节尾递归优化来自于B站:尾递归优化:你的递归调用是如何被优化的?

文章目录

  • 关于递归算法的时间复杂度
  • 尾递归优化
  • 斐波那契数列
    • 简单的递归方法
    • 动态规划的方法

关于递归算法的时间复杂度

对于题目:求 x 的 n 次方,首先请给出最简单的方法——迭代版本:

int function1(int x, int n) {int res = 1;for (int i = 0; i < n; i++) {res *= x;}return res;
}

这里的时间复杂度为 O(n),那么请问我们有没有时间复杂度更低的方法呢?

递归!好了我们先尝试一下递归。

int function(int x, int n) {if (n == 0) return 1;return function(x, n - 1) * x; 
}

递归算法的时间复杂度本质上是要看: 递归的次数 * 每次递归中的操作次数。

每次n-1,递归了n次时间复杂度是O(n),每次进行了一个乘法操作,乘法操作的时间复杂度一个常数项O(1),所以这份代码的时间复杂度是 n × 1 = O(n)。

那么,还有这样一个版本的递归算法:

int function(int x, int n) {if (n == 0) return 1;if (n == 1) return x;if (n % 2 == 1) return function(x, n / 2) * function(x, n / 2) * x;return function(x, n / 2) * function(x, n / 2)
}

关于该递归函数的时间复杂度分析,就需要搬出我们的二叉树进行辅助了。

当前这棵二叉树就是求x的n次方,n为16的情况,n为16的时候,进行了多少次乘法运算呢?

这棵树上每一个节点就代表着一次递归并进行了一次相乘操作,所以进行了多少次递归的话,就是看这棵树上有多少个节点。

熟悉二叉树话应该知道如何求满二叉树节点数量,这棵满二叉树的节点数量就是 2 3 + 2 2 + 2 1 + 2 0 = 15 2^3 + 2^2 + 2^1 + 2^0 = 15 23+22+21+20=15,可以发现:这其实是等比数列的求和公式,这个结论在二叉树相关的面试题里也经常出现。

所以,如果是求 x 的 n 次方,那么时间复杂度就是 O(n)

那么,我们应该如何写出 O(logn) 的递归算法呢?

int function(int x, int n) {if (n == 0) return 1;if (n == 1) return x;int t = function4(x, n / 2);// 这里相对于function3,是把这个递归操作抽取出来if (n % 2 == 1) {return t * t * x;}return t * t;
}

依然还是看他递归了多少次,可以看到这里仅仅有一个递归调用,且每次都是n/2 ,所以这里我们一共调用了log以2为底n的对数次。

每次递归了做都是一次乘法操作,这也是一个常数项的操作,那么这个递归算法的时间复杂度才是真正的O(logn)。

尾递归优化

这里的函数是写计算阶乘的函数:

// 普通递归版本
int factorial(int n) {if (n <= 1) return 1;return factorial(n - 1) * n;
}// 迭代版本
int factorial(int n) {int acc = 1;while (n > 0) {acc *= n;n -= 1;}return acc;
}// 尾递归版本
int factorial(int n) {if (n <= 1) return acc;return factorial(n - 1, acc * n);
}

尾递归优化既可以是语言级别的,也可以是编译器级别的,我们的 C++ 就是编译器级别的尾递归优化。

斐波那契数列

力扣题目链接

简单的递归方法

class Solution {
public:int fib(int n) {if (n == 0) return 0;if (n == 1) return 1;return fib(n - 1) + fib(n - 2);}
};

从上文的分析我们可以看到,我们想要计算出该递归算法的时间复杂度,需要进行明确递归树。

下面我们来分析其节点个数:
• 计算 fib(n) 需要计算 fib(n-1) 和 fib(n-2)。
• 计算 fib(n-1) 需要计算 fib(n-2) 和 fib(n-3)。
• 计算 fib(n-2) 需要计算 fib(n-3) 和 fib(n-4)。
为了计算时间复杂度,我们需要了解递归树的节点数。递归树的节点数代表了函数调用的总次数。由于每个节点会生成两个子节点,递归树的节点数随着树的高度呈指数增长。

• 根节点 fib(n) 有两个子节点,分别是 fib(n-1) 和 fib(n-2)。
• 每个子节点再生成两个子节点,形成更多的函数调用。

递归树的高度大约为 n,因为我们从 fib(n) 一直递归到 fib(0) 或 fib(1)。所以,树的总节点数大约是 2 的 n 次方(2^n),这是因为每个节点生成两个子节点的过程类似于二叉树的完全展开。

综上所述,时间复杂度为: O(n)

动态规划的方法

由于斐波那契满足明显的递推关系,所以我们可以很直观得使用动态规划进行解题:

我们先逐一分析动态规划五部曲:

  • dp数组的含义:dp[i]表示的是第 i 个数列的值
  • 确定递推公式:dp[i] = dp[i - 1] + dp[i - 2]
  • dp数组如何初始化:dp[0] = 0 dp[1] = 1
  • 确定遍历顺序:就直接往后遍历就可以了
  • 打印 dp 数组进行检查。
class Solution {
public:int fib(int n) {if (n < 2) return n;vector<int> dp(n, 1);for (int i = 2; i < n; i++) {dp[i] = dp[i - 1] + dp[i - 2];}return dp[n - 1];}
};

随后我们可以改进代码,也就是使用一种类似于滚动数组的思路,因为我们的递推公式只和前一个或者前两个有关,所以我们可以写如下代码:

class Solution {
public:int fib(int n) {if (n < 2) return n;vector<int> dp({0, 1, 1});for (int i = 2; i < n; i++) {int tmp = dp[1] + dp[2];dp[0] = dp[1];dp[1] = dp[2];dp[2] = tmp;} return dp[2];}
};

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com