带你了解递归算法的时间复杂度

用这篇article来给大家通透的讲一讲递归算法的时间复杂度。

同一道题目,同样使用递归算法,有的同学会写出了O(n)的代码,有的同学就写出了O(logn)的代码。 

这是为什么呢?

如果我们对递归的时间复杂度理解的不够深入的话,就会发生这样的状况!

我们来通过一道简单的题目,带着大家一步一步的分析递归算法的时间复杂度,找出最优解。

来看看同样是递归,为什么就写成了O(n)的代码。

原题:求一位数n次方

我看到题目的第一思想就是使用一个for循环求出结果,如下图所示:

int fun1(int a, int n)
{
    int result = 1;    //任何数的0次方都是1
    int i = 0;
    for( i=0 ; i

我们看到,时间复杂度为O(n),这时我想:“有没有效率更好的算法呢?” 考虑一下递归算法。

接着我就写出了这样一个递归的算法,用递归的思想去解决这个问题:

int fun2(int a, int n)
{
    if(n == 0)
    {
        return 1;    //返回1,同上是因为0的次方都是1
    }
    return fun2(a, n-1) * a;
}

那这个代码的时间复杂度是多少?一些同学可能一看到递归就想到了O(logn),并不是这样的,递归算法的时间复杂度,从本质上看就是: 递归的次数 * 每次递归中的操作次数

看代码,递归了几次呢?

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

这个时间复杂度就没有达到预期,于是乎重新再写出了下面的递归算法的代码:

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

这份代码的时间复杂度又是多少呢? 这时候有些同学可能要陷入了沉思了......

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

带你了解递归算法的时间复杂度_第1张图片

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

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

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

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

带你了解递归算法的时间复杂度_第2张图片 将m带入数列的通项公式中

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

这个递归的算法依然还是O(n)啊, 很明显这次还是没有达到预期。

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

想一想刚刚给出的那份递归算法的代码,是不是有哪里比较冗余呢?仔细观察的同学就可以发现里面的秘密,里边有重复计算的部分。

又重新写出下面递归算法的代码:

int fun4(int a, int n)
{
    if(n == 0)
    {
        return 1;
    }
    int b = fun4(a, n / 2);    // 这里相对于上面的fun3,是把这个递归操作抽取出来
    if(n % 2 == 1)
    {
        return b * b * a;
    }
    return b * b;
}

再来看一下现在这份代码de时间复杂度是多少呢?

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

每次递归,做的都是一次乘法运算,这也是一个常数项的运算,这时候的递归算法的时间复杂度才是真正的O(logn)

最后写出的代码,将时间复杂度分析的非常清晰

总结

用上学时期的一道平时作业题目:求一位数的n次方。

逐步分析递归算法的时间复杂度,大家不要一看到递归就想到了O(logn)!

同样使用递归,有的同学可以写出O(logn)的代码,有的同学还可以写出O(n)的代码。

特别要注意的是fun3 实现的递归,非常容易让人感觉那就是O(logn)的时间复杂度,但其实那只是O(n)的算法!

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

这道题目如果不用到递归的思想,还是非常简单的。对于想用算法思想实现的同学,却又很考究算法的功底,尤其是大家对递归的理解,这也是我学编程的第一道题,所以才能够理解的那么透彻。最后,我想说一句:猪扒大冤种!哈哈哈

做题目,不仅仅只是做完就可以了,要去思考有没有更简单的方法,同一道题目,同样是递归,但是效率可以是不一样的!

你可能感兴趣的:(算法)