对于文章的第一部分,递归算法的时间复杂度,来自于代码随想录文章:通过一道面试题目,讲一讲递归算法的时间复杂度!
对于第二节尾递归优化来自于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++ 就是编译器级别的尾递归优化。