递归算法的时间复杂度(通过一道面试题来讲解)

本篇通过一道简单的面试题,逐步分析递归算法的时间复杂度,最后找到最优解

同一道题目,同样使用递归算法,既可以写出时间复杂度为O(n)的代码,也可以写出时间复杂度为O(logn)的代码。

why?

这是因为对递归算法的时间复杂度理解不够深入。

下面通过一道面试题,来逐步分析递归算法的时间复杂度,最后找出最优解

面试题:求x的n次方

  • 最直观的方式应该就是,一个for循环求出结果,代码如下:

    int function1(int x, int n) {
        int result = 1;  // 注意 任何数的0次方等于1
        for (int i = 0; i < n; i++) {
            result = result * x;
        }
        return result;
    }
    

    时间复杂度为O(n),此时面试官会说,有没有效率更好的算法呢?

    面试者如果此时没有思路,建议不要说不知道。可以和面试官探讨一下,询问:“可不可以给点提示”。面试官提示说:“考虑一下递归算法”.

  • 此时面试者就写出了如下代码,使用递归算法解决了这个问题:

    int function2(int x, int n) {
        if (n == 0) {
            return 1; // return 1 同样是因为0次方是等于1的
        }
        return function2(x, n - 1) * x;
    }
    

    面试官问:“那么这个代码的时间复杂度是多少?”。

    一些同学可能一看到递归就想到了O(logn),其实并不是这样,递归算法的时间复杂度本质上是要看: 递归的次数 * 每次递归中的操作次数

    那么这里递归了几次呢?

    每次递归n都做一次减1的操作,那么就是递归了n次,递归n次时间复杂度是O(n),每次执行一个乘法操作,而乘法操作的时间复杂度是一个常数项O(1),所以这段代码的时间复杂度是O(n) (即:O(n*1) = O(n) )

    这个时间复杂度可能没有达到面试官的预期。

  • 于是面试者又些出了如下递归算法的代码:

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

    面试官看到后微微一笑,问:“这段代码的时间复杂度又是多少呢?” 此时面试者陷入了沉思……

    首先看递归了多少次呢,可以把递归抽象出一棵满二叉树。刚刚同学写的这个算法,可以用一棵满二叉树来表示(为了方便表示,选择n为偶数16),如图:

    img

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

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

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

    这么如果是求x的n次方,这个递归树有多少个节点呢,如下图所示:(m为深度,从0开始)

    img

    时间复杂度忽略掉常数项-1之后,这个递归算法的时间复杂度依然是O(n)。对,你没看错,依然是O(n)的时间复杂度!

    此时面试官就会说:“这个递归的算法依然还是O(n)啊”, 很明显没有达到面试官的预期。

  • 那么O(logn)的递归算法应该怎么写呢?

    想一想刚刚给出的那份递归算法的代码,是不是有哪里比较冗余呢,其实有重复计算的部分。

    于是又写出如下递归算法的代码:

    int function4(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;
    }
    

    现在这段代码的时间复杂度是多少呢?

    这里仅有一个递归调用,而且每次递归操作的数据规模都除以2,所以这里一共调用了\(log_2 n\)次,每次递归操作都是一次乘法操作,这也是一个常数项的操作,所以这个递归算法的时间复杂度才是真正的O(logn)。

  • 恭喜你,通过了考验~~

posted @ 2024-10-11 19:12  hisun9  阅读(2)  评论(0编辑  收藏  举报