程序员算法面试中,递归算法的空间复杂度你知道怎么求么?

一般算法的空间复杂度相信大家已经都掌握了

那么大家想一想递归算法的空间复杂度应该怎么分析呢

我这里用一个到简单的题目来举例

题目:求第n的斐波那契数

相信使用递归算法来求斐波那契数,大家应该再熟悉不过了

代码如下:

int fibonacci(int i) {
       if(i <= 0) return 0;
       if(i == 1) return 1;
       return fibonacci(i-1) + fibonacci(i-2);
}

可以看出代码非常简短,这时同学们就会有一种这种简短的代码,会有一种这时“高效”算法的错觉

我们来看这个递归算法的时间复杂度是多少

我在讲递归算法时间复杂度的文章里讲过 一场面试,带你彻底掌握递归算法的时间复杂度

求递归时间复杂度就是看它 每次递归进行了什么操作和递归了多少次

可以上面代码中每次递归都是O(1)的操作

再来看递归了多少次,

这里将就第n的斐波那契数,n为5为输入的递归过程 抽象成一颗递归树如下:
在这里插入图片描述

在这课二叉树中,我们可以看出每一个节点都是一次递归,每个节点都有两个子节点

那么我们这棵树有多少个节点

一个高度为k的二叉树最多可以有 2^k - 1个节点

所以我们该递归算法的时间复杂度为 O(2^n) ,这个复杂度是非常大的

随着n的增大,耗时是指数上升的。大家可以做一个实验,看看约着n的增大,这个递归求斐波那契的代码的耗时。

所以这种求斐波那契数的算法看似简洁,其实时间复杂度非常高,一般不推荐这样来写。

说完了时间复杂度,那在看看如何求递归算法的空间复杂度呢,这里我给大家一个公式

递归算法的空间复杂度 = 每次递归的空间复杂度 * 递归深度

首先看每次递归的空间复杂度,因为这个算法中我们可以看出每次递归所需要的空间大小都是一样的

而且就算是第N次递归,每次递归所需的栈空间也是一样的。

所以每次递归中需要的空间是一个常量,并不会随着n的变化而变化,每次递归的空间复杂度就是O(1)

求递归的空间复杂度,那么为什么要看递归的深度呢

每次递归所需的空间都被压到调用栈里(这是内存管理里面的数据结构,和我们算法里的栈原理是一样的)

看递归算法的空间消耗,就是要看调用栈所占用的大小

一次递归结束,这个栈就是就是把本次递归的数据弹出去。所以这个栈最大的长度就是 递归的深度。

我们在用这颗二叉树来举例

通过模拟递归的调用过程,来看一下调用栈大小的变化,二叉树的前序遍历就是递归的调用过程

如图所示,求第n的斐波那契数,n为5为输入的递归过程

递归过程
在这里插入图片描述

调用栈深度
在这里插入图片描述

通过这个举例,可以看出 递归第n个斐波那契数的话,递归调用栈的深度就是n

那么每次递归的空间复杂度是O(1), 调用栈深度为n,

最后 这个递归算法的空间复杂度就是 n * 1 = O(n)

那么刚刚这个递归求斐波那契数的算法 看上去简洁 可时间复杂度是 O(2^n),可以说非常耗时

// 时间复杂度:O(2^n) 
// 空间复杂度:O(n)
int fibonacci(int i) {
       if(i <= 0) return 0;
       if(i == 1) return 1;
       return fibonacci(i-1) + fibonacci(i-2);
}

罪魁祸首就是这里的两次递归,导致了 时间复杂度以指数上升

其实这里是可以优化的。 主要是减少递归的调用次数。

看如下优化后代码:

// 时间复杂度:O(n) 
// 空间复杂度:O(n)
int fibonacci(int first, int second, int n) {
    if (n <= 0) {
        return 0;
    }
    if (n < 3) {
        return 1;
    }
    else if (n == 3) {
        return first + second;
    }
    else {
        return fibonacci(second, first + second, n - 1);
    }
}

可以看出在递归的时候是线性的,时间复杂度是 O(n)

我们来总结一下 刚刚分析的几种斐波那契数的算法

在这里插入图片描述

从这我们可以看出,求斐波那契数的时候,使用递归算法并不一定是在性能上是最优的,但递归确实可以让代码看上去很简单。

最后带大家在分析一段代码,二分查找的递归实现

都知道二分查找的时间复杂度是logn,那么递归二分查找的空间复杂度是多少呢,

// 时间复杂度:O(logn) 
// 空间复杂度:O(nlogn)
int binary_search( int arr[], int l, int r, int x) {
    if (r >= l) {
        int mid = l + (r - l) / 2;
        if (arr[mid] == x)
            return mid;
        if (arr[mid] > x)
            return binary_search(arr, l, mid - 1, x);
        return binary_search(arr, mid + 1, r, x);
    }
    return -1;
}

我们依然看 每次递归的空间复杂度 和 递归的深度

首先我们先明确空间复杂度里面的n是什么, 二分查找的时候n就是指查找数组的长度

每次递归的空间复杂度 可以看出主要就是参数里传入的这个数组arr,也就是 O(n)

那么递归深度呢,二分查找的递归深度是logn ,递归深度就是调用栈的长度,以这段代码的空间复杂度为O(nlogn)

那么有同学问了,为什么网上很多人说递归二分查找 的空间复杂度是O(logn),而不是O(nlogn)呢

其实很多文章都没有说清楚,还是这个数组arr

如果我们把这个数组arr定义为全局变量而不是放在递归里面

那么 每次递归的空间复杂度为O(1) 和 递归的深度logn, 所以空间复杂度为O(logn),

代码如下:

// 时间复杂度:O(logn) 
// 空间复杂度:O(logn)
int arr[] = {2, 3, 4, 5, 8, 10, 15, 17, 20};
int binary_search(int l, int r, int n) {
    if (r >= l) {
        int mid = l + (r - l) / 2;
        if (arr[mid] == n)
            return mid;
        if (arr[mid] > n)
            return binary_search(l, mid - 1, n);
        return binary_search(mid + 1, r, n);
    }
    return -1;
}

希望通过这篇文章可以帮助大家对空间复杂度有进一步的认识 ,在算法面试的时候 才能更充分体现出自己对算法的理解和思考

猜你喜欢

转载自blog.csdn.net/youngyangyang04/article/details/106313759
今日推荐