关于递归算法的时间复杂度,你还不够了解

开发 前端 算法
本篇通过一道面试题,一个面试场景,来好好分析一下如何求递归算法的时间复杂度。相信很多同学对递归算法的时间复杂度都很模糊,那么这篇Carl来给大家通透的讲一讲。

[[414048]]

本篇通过一道面试题,一个面试场景,来好好分析一下如何求递归算法的时间复杂度。

相信很多同学对递归算法的时间复杂度都很模糊,那么这篇Carl来给大家通透的讲一讲。

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

这是为什么呢?

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

那么我通过一道简单的面试题,模拟面试的场景,来带大家逐步分析递归算法的时间复杂度,最后找出最优解,来看看同样是递归,怎么就写成了O(n)的代码。

面试题:求x的n次方

想一下这么简单的一道题目,代码应该如何写呢。最直观的方式应该就是,一个for循环求出结果,代码如下:

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

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

如果此时没有思路,不要说:我不会,我不知道了等等。

可以和面试官探讨一下,询问:“可不可以给点提示”。面试官提示:“考虑一下递归算法”。

那么就可以写出了如下这样的一个递归的算法,使用递归解决了这个问题。

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

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

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

那再来看代码,这里递归了几次呢?

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

这个时间复杂度就没有达到面试官的预期。于是又写出了如下的递归算法的代码:

  1. int function3(int x, int n) { 
  2.     if (n == 0) { 
  3.         return 1; 
  4.     } 
  5.     if (n % 2 == 1) { 
  6.         return function3(x, n / 2) * function3(x, n / 2)*x; 
  7.     } 
  8.     return function3(x, n / 2) * function3(x, n / 2); 

面试官看到后微微一笑,问:“这份代码的时间复杂度又是多少呢?” 此刻有些同学可能要陷入了沉思了。

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

图片

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

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

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

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

图片

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

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

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

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

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

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

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

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

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

此时大家最后写出了这样的代码并且将时间复杂度分析的非常清晰,相信面试官是比较满意的。

总结

对于递归的时间复杂度,毕竟初学者有时候会迷糊,刷过很多题的老手依然迷糊。

本篇我用一道非常简单的面试题目:求x的n次方,来逐步分析递归算法的时间复杂度,注意不要一看到递归就想到了O(logn)!

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

对于function3 这样的递归实现,很容易让人感觉这是O(logn)的时间复杂度,其实这是O(n)的算法!

  1. int function3(int x, int n) { 
  2.     if (n == 0) { 
  3.         return 1; 
  4.     } 
  5.     if (n % 2 == 1) { 
  6.         return function3(x, n / 2) * function3(x, n / 2)*x; 
  7.     } 
  8.     return function3(x, n / 2) * function3(x, n / 2); 

可以看出这道题目非常简单,但是又很考究算法的功底,特别是对递归的理解,这也是我面试别人的时候用过的一道题,所以整个情景我才写的如此逼真,哈哈。

大厂面试的时候最喜欢用“简单题”来考察候选人的算法功底,注意这里的“简单题”可并不一定真的简单哦!

如果认真读完本篇,相信大家对递归算法的有一个新的认识的,同一道题目,同样是递归,效率可是不一样的!

 

责任编辑:姜华 来源: 代码随想录
相关推荐

2021-01-05 10:41:42

算法时间空间

2024-04-25 08:33:25

算法时间复杂度空间复杂度

2019-11-18 12:41:35

算法Python计算复杂性理论

2009-07-09 10:45:16

C#基本概念复杂度递归与接口

2021-09-17 10:44:50

算法复杂度空间

2021-06-28 06:15:14

算法Algorithm时间空间复杂度

2020-12-08 11:08:55

时间复杂度软件

2021-07-19 08:33:56

时间复杂度大O

2020-11-30 06:26:31

算法时间表示法

2020-02-06 13:59:48

javascript算法复杂度

2018-08-20 17:35:37

AI

2020-12-30 05:35:56

数据结构算法

2021-10-15 09:43:12

希尔排序复杂度

2024-05-20 09:04:29

时间复杂度代码

2014-12-10 09:23:14

2022-02-13 20:04:04

链表节点代码

2015-10-13 09:43:43

复杂度核心

2020-12-30 09:20:27

代码

2021-04-25 14:29:02

数据结构动态数组时间复杂度

2021-03-17 08:37:23

算法性能分析递归算法递归树
点赞
收藏

51CTO技术栈公众号