1.算法的时间复杂度

1.1时间复杂度的计算

  • 如何衡量算法的执行时间?

衡量算法的执行时间,通常有两种方法。

(1) 事后统计的方法

这种方法理论上是可行的,但不是一个最好的解决方案。该方法有两个缺陷:(一)要想对设计的算法的运行性能进行评测,必须先依据问题编写出相应的算法并实际运行。(二)所得算法的执行时间依赖于计算机的硬件、软件等环境因素,有时容易掩盖算法本身的优劣。

因为事后统计方法更多的依赖于计算机的硬件、软件等环境因素,有时容易掩盖算法本身的优劣。因此,我们更多采用事前估算的方法来衡量算法的执行时间。

(2) 事前估算的方法

在编写程序前,通过分析某个算法的时间复杂度来判断哪个算法更优。

  • 时间频度的介绍

一个算法花费的时间与算法中语句的执行次数成正比例,哪个算法中语句执行次数多,它花费时间就多。一个算法中的语句执行次数称为语句频度或时间频度,记为T(n)。

示例:计算以下代码的时间频度T(n)

public

在时间频度T(n)中,n称为问题的规模,当n不断变化时,时间频度T(n)也会不断变化。如果我们想知道它变化时呈现什么规律。为此,我们引入时间复杂度概念。

  • 时间复杂度的定义

一般情况下,算法中基本操作重复执行的次数是问题规模n的某个函数,用T(n)表示,若有某个辅助函数f(n),使得当n趋近于无穷大时,T(n)/f(n)的极限值为不等于零的常数,则称f(n)是T(n)的同数量级函数。记作T(n)=O(f(n)),我们也称O(f(n))为算法的渐进时间复杂度,简称时间复杂度。

  • 时间复杂度的计算步骤

(1) 计算基本操作的执行次数T(n)

在做算法分析时,一般默认考虑最坏的情况。

(2) 计算T(n)的数量级f(n)

求T(n)的数量级f(n),只需要将T(n)做两个操作:(一)忽略常数项、低次幂项和最高次幂项的系数。(二)f(n)=(T(n)的数量级)。例如,在T(n)=4n^2+2n+2中,T(n)的数量级函数f(n)=n^2。

计算T(n)的数量级f(n),我们只要保证T(n)中的最高次幂正确即可,可以忽略所有常数项、低次幂项和最高次幂的系数。这样能够简化算法分析,将注意力集中在最重要的一点上:增长率。

(3) 用大O表示时间复杂度

当n趋近于无穷大时,如果T(n)/f(n)的极限值为不等于零的常数,则称f(n)是T(n)的同数量级函数。记作T(n)=O(f(n))。例如,在T(n)=4n^2+2n+2中,就有f(n)=n^2,使得T(n)/f(n)的极限值为4,也就是得到时间复杂度为O(n^2)。

切记,时间频度不相同时,但是时间复杂度有可能相同,如T(n)=n^2+3n+4与T(n)=4n^2+2n+1它们的时间频度不同,但时间复杂度相同,都为O(n^2)。

1.2 常见的时间复杂度介绍

常见的时间复杂度有:常数阶O(1),对数阶O(log2n),线性阶O(n),线性对数阶O(nlog2n),平方阶O(n^2),立方阶O(n^3),指数阶O(2^n)和阶乘阶O(n!)。

接下来,我们就来学习这些常见的时间复杂度。

  • 常数阶 O(1)

无论代码执行了多少行,只要是没有循环等复杂结构,那这个代码的时间复杂度就都是O(1)。

int

在上述代码中,没有循环等复杂结构,它消耗的时间并不随着某个变量的增长而增长,那么无论这类代码有多长,即使有几万几十万行,都可以用O(1)来表示它的时间复杂度。

  • 对数阶 O(log2n)

O(log2n)指的就是:在循环中,每趟循环执行完毕后,循环变量都放大两倍。

int

推算过程:假设该循环的执行次数为x次(也就是i的取值为2^x),就满足了循环的结束条件,即满足了2^x等于n,通过数学公式转换后,即得到了x = log2n,也就是说最多循环log2n次以后,这个代码就结束了,因此这个代码的时间复杂度为:O(log2n) 。

同理,如果每趟循环执行完毕后,循环变量都放大3倍,那么该代码的时间复杂度为:O(log3n) 。

  • 线性阶 O(n)
int

在上述代码中,for循环会执行n趟,因此它消耗的时间是随着n的变化而变化的,因此这类代码都可以用O(n)来表示它的时间复杂度。

  • 线性对数阶 O(nlog2n)
int

线性对数阶O(nlog2n) 其实非常容易理解,将时间复杂度为O(log2n)的代码循环n遍的话,那么它的时间复杂度就是n*O(log2n),也就是了O(nlog2n)。

  • 平方阶 O(n^2)
int

外层i的循环执行一次,内层j的循环就要执行n次。因为外层执行n次,那么总的就需要执行n*n次,也就是需要执行n^2次。因此这个代码的时间复杂度为:O(n^2)。

平方阶的另外一个例子:

int

当i=1的时候,内侧循环执行n次,当i=2的时候,内侧循环执行(n-1)次,......一直这样子下去就可以构造出一个等差数列:n+(n-1)+(n-2)+......+2+1 ≈ (n^2)/2。根据大O表示法,去掉最高次幂的系数,就可以得到时间复杂度为:O(n^2)。

同理,立方阶 O(n^3),参考上面的O(n^2)去理解,也就是需要用到3层循环。

  • 指数阶O(2^n)和阶乘阶O(n!)

指数阶O(2^n)指的就是:当n为10的时候,需要执行2^10次。

阶乘阶O(n!)指的就是:当n为10的时候,需要执行10*9*8*...*2*1次。

  • 常见的时间复杂度耗时比较

算法的时间复杂度是衡量一个算法好坏的重要指标。一般情况下,随着规模n的增大,T(n)的增长较慢的算法为最优算法。

双重for循环 python 双重for循环时间复杂度_王道论坛 三重循环时间复杂度


其中x轴代表n值,y轴代表T(n)值。T(n)值随着n的值的变化而变化,其中可以看出O(n!)和O(2^n)随着n值的增大,它们的T(n)值上升幅度非常大,而O(logn)、O(n)、O(nlogn)、O(n^2)随着n值的增大,T(n)值上升幅度相对较小。

常用的时间复杂度按照耗费的时间从小到大依次是:O(1) < O(logn) < O(n) < O(nlogn) < O(n^2) < O(n^3) < O(2^n) < O(n!)。

1.3 最好、最坏和平均时间复杂度

  • 最好和最坏时间复杂度

最好情况时间复杂度就是在最理想的情况下,执行这段代码的时间复杂度。

最坏情况时间复杂度就是在最糟糕的情况下,执行这段代码的时间复杂度。

来看看下面这段代码:

/**

因为元素element在数组中的位置是不确定的,有可能数组中的第一个元素就是element,那就就意味着只需循环一次即可,其时间复杂度就是 O(1);如果数组中不存在元素element或者是数组中的最后一个元素才是element,那就需要遍历整个数组,时间复杂度就是 O(n)。

在这里,O(1) 就是最好情况时间复杂度,O(n) 就是最坏情况时间复杂度。

  • 平均情况的时间复杂度

借助上面的例子继续来分析,要查找的元素element在数组中的位置,有n+1种情况:在数组的[0,n-1]索引位置中和不在数组中。

在这里我们引入概率论的相关知识,假设元素element在数组中与不在数组中的概率各为1/2,并且假设出现在索引[0, n-1]这n个位置的概率都是1/n。根据概率乘法法则,要查找的元素element出现在[0, n-1]中任意位置的概率就是1/2n。到这里,就可以得到这样的计算公式:1*(1/2n) + 2*(1/2n) + 3*(1/2n) + …… + n*(1/2n) + n*(1/2) = (3n + 1)/4。得到的这个值就是概率论中的加权平均值,也叫做期望值。根据这个加权平均值,去掉常数项、低次幂项和最高次幂项的系数,我们得到的平均时间复杂度也是 O(n)。

所以,平均时间复杂度就是:加权平均时间复杂度(亦称为期望时间复杂度)。

  • 最好、最坏和平均时间复杂度总结

算法中,如果不做特别的说明,我们讨论的时间复杂度均是最坏情况下的时间复杂度。这样做的原因是:最坏情况下的时间复杂度是算法在任何输入实例上运行时间的界限,这就保证了算法的运行时间不会比最坏情况更长。

2. 算法的空间复杂度

2.1 算法的空间复杂度介绍

算法在运行过程中所需的存储空间包括:(1)输入输出数据占用的空间;(2)算法本身占用的空间;(3)执行算法所需的辅助空间。其中输入和输出数据占用的空间取决于问题,与算法无关;算法本身占用的空间虽然与算法相关,但是一般其大小是固定的。所以,算法的空间复杂度(Space Complexity)是指算法在执行过程中需要的辅助空间,也就是除算法本身和输入输出数据占用的空间外,算法零时开辟的存储空间。

如果算法所需的辅助存储空间的数量是问题规模n的函数,通常计作:S(n)=O(f(n))。其中,n为问题的规模,分析方法与算法的时间复杂度类似。

案例一:计算嵌套循环执行次数

public

在以上代码中,由于算法中临时变量的个数与问题规模n无关,所以空间复杂度均为O(1)。

案例二:计算1+2+3+...+n的结果

public

以上的案例采用了递归,每次调用本身都要分配空间,所以空间复杂度为O(n)。

在做算法分析时,因为时间复杂度要比空间复杂度更容易出问题,所以一般情况下我们更多对时间复杂度进行研究。 从用户使用体验上看,更看重的程序执行的速度。一些缓存产品(redis)和算法(基数排序)本质就是使用空间来换时间。

另外,一般面试或者工作的时候没有特别说明的话,算法复杂度就是指时间复杂度。

2.2 时间复杂度和空间复杂度总结

举例:金融港到关谷广场有很多条路。如果选择路程近的,那么可能会堵车耗时(时间复杂度的性能变差);如果选择耗时短的,那么可能会绕路(空间复杂度的性能变差)。那么到底选择走哪一条路,就需要根据实际情况来综合考虑了。

编程算法中亦是如此,时间复杂度和空间复杂度往往是相互影响的。当追求一个较好的时间复杂度时,可能会使空间复杂度的性能变差,即可能导致占用较多的存储空间;相反的当追求一个较好的空间复杂度时,就可能会使时间复杂度的性能变差,即可能导致占用较长的运行时间。

因此,当设计一个算法(特别是大型算法)时,要综合考虑算法的各项性能,算法的使用频率,算法处理的数据量的大小,算法描述语言的特性,算法运行的机器系统环境等各方面因素,才能够设计出比较好的算法。