时间复杂度+常见复杂度解释

--------------------- 
作者:CrankZ 
来源:CSDN 
原文:https://blog.csdn.net/CrankZ/article/details/84726408 
版权声明:本文为博主原创文章,转载请附上博文链接!

前言
算法的效率

虽然计算机能快速的完成运算处理,但实际上,它也需要根据输入数据的大小和算法效率来消耗一定的处理器资源。要想编写出能高效运行的程序,我们就需要考虑到算法的效率。
算法的效率主要由以下两个复杂度来评估:
时间复杂度:评估执行程序所需的时间。可以估算出程序对处理器的使用程度。
空间复杂度:评估执行程序所需的存储空间。可以估算出程序对计算机内存的使用程度。

设计算法时,一般是要先考虑系统环境,然后权衡时间复杂度和空间复杂度,选取一个平衡点。不过,时间复杂度要比空间复杂度更容易产生问题,因此算法研究的主要也是时间复杂度,不特别说明的情况下,复杂度就是指时间复杂度。

本文只分析时间复杂度

什么是时间复杂度
了解时间复杂度之前,先了解时间频度

时间频度
一个算法执行所耗费的时间,从理论上是不能算出来的,必须上机运行测试才能知道。但我们不可能也没有必要对每个算法都上机测试,只需知道哪个算法花费的时间多,哪个算法花费的时间少就可以了。并且一个算法花费的时间与算法中语句的执行次数成正比例,哪个算法中语句执行次数多,它花费时间就多。一个算法中的语句执行次数称为语句频度或时间频度。记为T(n)。

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

一句话:T(n)就是时间频度,表示算法执行的次数

问题:T(n)随着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)的数量级
求T(n)的数量级,只要将T(n)进行如下一些操作:

忽略常量

低次幂和最高次幂的系数

令f(n)=T(n)的数量级。

3、用大O来表示时间复杂度
  当n趋近于无穷大时,如果lim(T(n)/f(n))的值为不等于0的常数,则称f(n)是T(n)的同数量级函数。记作T(n)=O(f(n))。

只保留最高阶项,最高阶项存在且不是1,则去除与这个项相乘的常数。

前面提到的时间频度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)),它称为算法的渐进时间复杂度,简称时间复杂度。

T(n)/f(n)的极限值为不等于零的常数什么意思?
首先你要知道T(n)是f(n)忽略常量、低次幂和最高次幂的系数。只保留能代表数量级的项。

所以T(n)肯定是小于等于f(n)的

那么,如果T(n)/f(n)的极限值等于0,那么就是说f(n)的增长趋势比T(n)大太多,那么就说明他们俩不是一个数量级的。

不是一个数量级什么意思呢?
就好比你玩LOL,你是黄铜1,他是黄铜3,虽然你比他高一点,但你们俩还是黄铜,是一个数量级的。。。大多数时候,通过段位就能判断你的水平,不会在意是你在你段位是1还是5。这里算法的时间复杂度也是一样,只保留核心项,其他的都去掉。

什么时候不是一个数量级呢?等你上白银或者黄金的时候,那就不是一个数量级的。。。

一个算法的执行时间与哪些因素有关
衡量一个算法的好坏不能简单的从这个算法所花费的时间来衡量。因为这个时间受多种因素影响。一般来说一个算法花费的时间有以下4点

计算机执行的速度->硬件层面
编译产生的代码质量->软件层面
算法的好坏(算法使用的策略)
问题规模
在给定软硬件环境下,其实就是你在自己电脑上写算法的时候,算法执行时间只受算法本身的好坏和要处理的问题的规模影响。这样就将4个影响因素减少为2个,简化了问题。

我们继续分析,

给定问题规模n之后,优秀的算法可能执行几次就搞定了,一般的算法可能执行很多很多次才搞定;

当给定算法时,问题规模n很小时,可能执行几次就搞定,而n很大时,就得执行很多次了。

所以算法优劣和问题规模n改变时,执行次数(基本操作数)将改变,所以执行次数就是算法优劣和问题规模n的函数。

既然执行时间受算法好坏和问题规模n的影响,那么执行时间就是它俩的函数。

要比较两个函数的增长情况,最好的办法是比较函数的一阶导,这样最精确,但是考虑到很多时候只需要大体了解算法的优劣就可以了,所以我们就直接考察对增长速度影响最大的一项,这一项就是函数的最高阶数。为了说明最高阶数对函数增长影响最明显,我们看两幅图。

图中4条曲线分别表示4种不同的执行次数表达式,从图中可以看出,只要最高项的阶数相同,4种表达式值受其他项的影响很小,随着n增大,几乎可以忽略不计,甚至可以忽略与最高项相乘的常数。

既然可以只考虑最高项的阶数,以简化问题,达到估算的目的,为何不这样做呢?

那总得给这种情况一个恰当的表示方式吧?和其他领域一样,还得用符号来表示,这个符号就是大名鼎鼎的大O符号。

推导大O阶有一下三种规则:
用常数1取代运行时间中的所有加法常数
只保留最高阶项
去除最高阶的常数
一般我们我们评估一个算法都是直接评估它的最坏的复杂度。

时间复杂度是一个量级的概念,而不是具体的值,比如O(5n)的量级是n,因为这个时间复杂度函数内最高量级是变量n的一次方

常见时间复杂度
下图来自维基百科


常数级别O(1)


O(1):算法复杂度和问题规模无关。换句话说,哪怕你拿出几个PB的数据,我也能一步到位找到答案。

理论上哈希表就是O(1)。因为哈希表是通过哈希函数来映射的,所以拿到一个关键字,用哈希函数转换一下,就可以直接从表中取出对应的值。和现存数据有多少毫无关系,故而每次执行该操作只需要恒定的时间(当然,实际操作中存在冲突和冲突解决的机制,不能保证每次取值的时间是完全一样的)。

执行次数
N=10,大约执行1次

N=100,大约执行1次

N=1000,大约执行1次

N=10000,大约执行1次

对数级别O(logN)


Tips:log的底数在大O符号的中是省去的。常见的底数为2

O(logN):算法复杂度和问题规模是对数关系。换句话说,数据量大幅增加时,消耗时间/空间只有少量增加(比如,当数据量从2增加到2^64时,消耗时间/空间只增加64倍)

执行次数
底数为2的情况

N=10,大约执行3次

N=100,大约执行7次

N=1000,大约执行10次

N=10000,大约执行13次

代码
int number = 1; // 语句执行一次
while (number < n) { // 语句执行logn次
  // 这里的2是log的底数
  // 底数在大O符号中是省去的
  number *= 2; // 语句执行logn次
}
线性级别O(N)


O(n):算法复杂度和问题规模是线性关系。换句话说,随着样本数量的增加,复杂度也随之线性增加

执行次数
N=10,大约执行10次

N=100,大约执行100次

N=1000,大约执行1000次

N=10000,大约执行10000次

代码
int i =0; // 语句执行一次
while (i < n) { // 语句执行n次
  print(i); //语句执行n次
  i++; // 语句执行n次
}
这个算法中代码总共执行了 3n + 1次,根据规则 2->3,因此该算法的时间复杂度是O(n)。

线性对数级别O(NlogN)


O(logn)的算法复杂度,典型的比如二分查找。设想一堆试卷,已经从高到底按照分数排列了,我们现在想找到有没有59分的试卷。怎么办呢?先翻到中间,把试卷堆由中间分成上下两堆,看中间这份是大于还是小于59,如果大于,就留下上面那堆,别的丢掉,如果小于,就留下下面那堆,丢掉上面。然后按照同样的方法,每次丢一半的试卷,直到丢无可丢为止。

假如有32份试卷,你丢一次,还剩16份 ,丢两次,还剩下8 份,丢三次,就只剩下4份了,可以这么一直丢下去,丢到第五次,就只剩下一份了。而  。也就是我们一次丢一半,总要丢到只有一份的时候才能出结果,如果有n份,那么显然我们就有:

也就是大约需要  次,才能得出“找到”或者“没找到”的结果。当然你说你三分查找,每次丢三分之二可不可以?当然也可以,但是算法复杂度在这里是忽略常数的,所以不管以2为底,还是以什么数为底,都统一的写成 log(n)的形式。

理解了这一点,就可以理解快速排序为什么是 O(nlogn)了。比如对一堆带有序号的书进行排序,怎么快呢?就是随便先选一本,然后把号码大于这本书的扔右边,小于这本书的扔左边。因为每本书都要比较一次,所以这么搞一次的复杂度是 O(n),那么快排需要我们搞多少次呢?这个又回到了二分查找的逻辑了,每次都把书堆一分为二,请问分多少次手里才能只剩下一本书呢?答案还是 logn。而从代码的角度来说,在到达大小为一的数列之前,我们也是需要作 logn次嵌套的调用。

执行次数
底数为2的情况

N=10,大约执行33次

N=100,大约执行664次

N=1000,大约执行9966次

N=10000,大约执行132877次

平方级别O(N^2)


O(n^2)计算的复杂度随着样本个数的平方数增长。这个例子在算法里面,就是那一群比较挫的排序,比如冒泡等等。 

执行次数
N=10,大约执行100次

N=100,大约执行10000次

N=1000,大约执行1000000次

N=10000,大约执行100000000次

代码
for (int i = 0; i < n; i++) { // 语句执行n次
  for (int j = 0; j < n; j++) { // 语句执行n^2次
     print('I am here!'); // 语句执行n^2
  }
}
上面的嵌套循环中,代码共执行 2*n^2 + n,则f(n) = n^2。所以该算法的时间复杂度为O(n^2 )

指数级别O(2^N)


如果一个算法的运行时间是指数级的(exponential),一般它很难在实践中使用

执行次数
N=10,大约执行1024次

N=100,大约执行2^100次

N=1000,大约执行2^1000次

N=10000,大约执行2^10000次

猜你喜欢

转载自blog.csdn.net/shanchahua123456/article/details/85020028