时间复杂度和空间复杂度
一、引言
设计算法一般是为了提高效率,这里的效率通常是指算法执行的是时间
那我们因该如何度量算法的执行时间呢?
事前分析估算方法:在计算机程序编写前,依据统计方法对算法进行评估
高级语言编写的程序在计算机上运行时所消耗的时间取决于下列因素:
序号 | 因素 |
---|---|
1 | 算法采用的策略,方案 |
2 | 编译产生的代码质量 |
3 | 问题的输入规模 |
4 | 机器执行指令的速度 |
由此可见,抛开这些与计算机硬件、软件有关的因素,一个程序的运行时间依赖于算法的好坏和问题的输入规模。(所谓的问题输入规模是指输入量的多少)
//例如求和算法
//第一种算法
int i,sum=0,n=100; //执行1次
for(i=1;i<=n;i++) //执行n+1次
sum+=i; //执行n次
//第二种算法
itn sum=o,n=100; //执行1次
sum=(n+1)*n/2; //执行1次
算法一执行的总次数是1+(n+1)+n=2n+2
算法二执行的总次数是1+1=2
如果我们把循环看做一个整体,忽略头尾判断的开销,那么这两个算法其实就是n和1的差距。
这就会有人产生疑问了,为啥将2n+2看作n,2看作1
因为我们研究算法的复杂度,侧重的是研究算法随着输入规模扩大增长量的一个抽象,而不是精确地定位需要执行多少次,因为如果这样的话,我们就又得考虑编译器优化等问题
例如执行次数3n+2看作n,n2+3n+1看作n2
在分析一个算法的运行时间时,重要的是把基本操作的数量和输入模式关联起来
如果还未明白的可以不用管,文章下面还有具体讲解
以及判断算法好不好,只通过少量的数据是不能做出准确判断的,很容易以偏概全
二、时间复杂度
2.1 算法时间复杂度的定义
算法时间复杂度的定义:
在进行算法分析时,语句的执行总次数T(n)是关于问题规模n的函数,进而分析T(n)随n的变化情况并确定T(n)的数量级算法时间复杂度,也就是算法的时间度量,记作:T(n)=0(f(n))
它表示随问题规模n的增大,算法执行时间的增长率和f(n)的增长率相同称作算法的渐进时间复杂度,简称时间复杂度
(关键需要知道执行次数==时间)
这样用大写O( )来体现算法时间复杂度的记法,称作为大O记法
一般情况下,随着输入规模n的增大,T(n)增长越慢的算法为最优算法
2.2 推导大0阶的攻略
推导大0阶的攻略:
1.用常数1取代运算时间中的所有加法常数
2.在修改后的运行次数函数中,只保留最高阶项
3.如果最高阶项存在且不是1,则去除与这个项相乘的常数
4.得到的最后结果就是大0阶
举几个例子:
//例1(常数阶):
int sum = 0 , n = 100;
printf( "We love X_P\n" );
printf( "We love X_P\n" );
printf( "We love X_P\n" );
printf( "We love X_P\n" );
printf( "We love X_P\n" );
printf( "We love X_P\n" );
sum = (1+n)*n/2;
//大O是多少?
O(8)???
这是初学者常常犯的错误,总认为有多少条语句就有多少。
正确结果是O(1)
分析如下:
不管打印多少"I love X_P\n"都与n的改变无关
因为T(n)是关于输入规模n的函数,只与n有关
还可以参照攻略1,所有加法常数都可以用1代替
//例2(线性阶):
//线性阶:一般含有非嵌套循环涉及线性阶,线性阶就是随着问题规模n的扩大,
//对应计算次数呈直线增长
int i,sum=0,n=100;
for(i=1;i<=n;i++)
sum+=i;
//大O是多少?
上面代码的时间复杂度为O(n),因为循环中代码需要执行n次
//例3(平方阶):
//刚才是单个循环,那么嵌套循环呢
int i,j,n=100;
for(i=0;i<n;i++)
for(j=0;j<n;j++)
printf("We love X_P\n");
//大O是多少?
时间复杂度为O(n2)
//那改成这样:
int i,j,n=100;
for(i=0;i<n;i++)
for(j=i;j<n;j++)
printf("We love X_P\n");
//大O又是多少
分析下,由于当i=0时,内循环执行了n次,当i=1时,内循环则执行n-1次…当i=n-1时,内循环执行1次,所以总的执行次数应该是:
n+(n-1)+(n-2)+.......+1=n(n+1)/2
分解下来就是
n^2^/2+n/2
再由求大O的攻略,第一条忽略,没有常数项;第二条只保留最高阶项,所以化为n2/2;第三条最高阶项存在且不是1,则去除与这个项相乘的常数,所以化为n2
由此得到时间复杂度为O(n2)
//例4(对数阶):
int i = 1, n = 100;
while( i < n )
i *= 2;
//大O是多少
由于每次i*2之后,就举例n更近一步,假设有x个2相乘后大于或等于n,则会退出循环。
于是由2x =n得到X=log2n,所以这个循环的时间复杂度为O(logn)。
2.3 常见的时间复杂度
例子 | 时间复杂度 | 装逼术语 |
---|---|---|
5201314 | O(1) | 常数阶 |
3n+4 | O(n) | 线性阶 |
3n2+4n+5 | O(n2) | 平方阶 |
3log2n+4 | O(logn) | 对数阶 |
2n+3nlog2n+14 | O(nlogn) | nlogn阶 |
n3+2n2+4n+6 | O(n3) | 立方阶 |
2n | O(2n) | 指数阶 |
常用时间复杂度所耗费时间从小到大依次是:
O(1) < O(logn) < (n) < O(nlogn) < O(n2) < O(n3) < O(2n) < O(n!) < O(nn)
三、空间复杂度
算法的空间复杂度通过计算算法所需的存储空间实现,算法的空间复杂度的计算公式记作:
S(n)=O(f(n))
其中,n为问题的规模,f(n)为语句关于n所占存储空间的函数。
通常,我们都是用“时间复杂度”来指运行时间的需求,是用“空间复杂度”指空间需求。
我们在写代码时,完全可以用空间来换去时间。
举个例子说,要判断某年是不是闰年,你可能会花一点心思来写一个算法,每给一个年份,就可以通过这个算法计算得到是否闰年的结果。
另外一种方法是,事先建立一个有2050个元素的数组,然后把所有的年份按下标的数字对应,如果是闰年,则此数组元素的值是1,如果不是元素的值则为0。这样,所谓的判断某一年是否为闰年就变成了查找这个数组某一个元素的值的问题。
第一种方法相比起第二种来说很明显非常节省空间,但每一次查询都需要经过一系列的计算才能知道是否为闰年。第二种方法虽然需要在内存里存储2050个元素的数组,但是每次查询只需要一次索引判断即可。
这就是通过一笔空间上的开销来换取计算时间开销的小技巧。到底哪一种方法好?其实还是要看你用在什么地方。