目录
2.5 算法的特性
2.5.1 输入输出
2.5.2 有穷性
2.5.3 确定性
2.5.4 可行性
2.6 算法设计的要求
2.6.1 正确性
2.6.2 可读性
2.6.3 健壮性
2.6.4 时间效率高和存储量低
2.7 算法效率的度量方法
2.7.1 事后统计方法
2.7.2 事前分析估算方法
2.8 函数的渐近增长
2.9 算法时间复杂度
2.9.1 算法时间复杂度的定义
2.9.2 推导大O阶方法
2.9.3 常数阶
2.9.4 线性阶
2.9.5 对数阶
2.9.6 平方阶
2.10 常见的时间复杂度
2.11 最坏情况与平均情况
2.12 算法空间复杂度
2.14 结尾语
算法具有5个基本特征:输入、输出、有穷性、确定性和可行性。
算法具有零个或多个输入。
算法至少有一个或多个输出。
有穷性:指算法在执行有限的步骤之后,自动结束而不会出现无限循环,并且每一个步骤再可接受的时间内完成。
确定性:算法的每一步骤都具有确定的含义,不会出现二义性。
可行性:算法的每一步都必须是可行的,也就是说,每一步都能够通过执行优先次数完成。
正确性: 算法的正确性是指算法至少应该具有输入、输出和加工处理无歧义性、能正确反映问题的需求、能够得到问题的正确答案。
但是算法的“正确”通常在用法上有很大的差别,大体分为以下四个层次。
1. 算法程序没有语法错误。
2. 算法程序对于合法的输入数据能够产生满足要求的输出结果。
3. 算法程序对于非法的输入数据能够得到满足规格说明的结果。
4. 算法程序对于精心选择的,甚至刁难的测试数据都有满足要求的输出结果。
可读性:算法设计的另一目的是为了便于阅读、理解和交流。
健壮性:当输入数据不合法时,算法也能做出相关处理,而不是产生异常或莫名其妙的结果。
时间效率指算法的执行时间,对于同一个问题,如果有多个算法能够解决,执行时间短的算法效率高,执行时间长的效率低。
存储量需求指的是算法在执行过程中需要的最大存储空间,主要指算法程序运行时所占用的内存或外部硬盘存储空间。
算法设计应尽量满足时间效率高和存储量低的需求。
事后统计方法:这种方法主要是通过设计好的测试程序和数据,利用计算机计时器对不同算法编制的程序的运行时间进行比较,从而确定算法效率的高低。
缺陷:
事前分析估算方法:在计算机程序编制前,依据统计方法对算法进行估算。
一个用高级程序语言编写的程序在计算机上运行时所消耗的时间取决于下列因素:
1. 算法采用的策略、方法。---算法好坏的根本
2. 编译产生的代码质量。---由软件来支持
3. 问题的输入规模。
4. 机器执行指令的速度。---看硬件性能
即,抛开与计算机硬件、软件相关的因素,一个程序的执行时间,依赖于算法的好坏和问题的输入规模。所谓问题输入规模是指输入量的多少。
我们在分析一个算法的运行时间时,重要的是把基本操作的数量与输入规模关联起来,即基本操作的数量必须表示成输入规模的函数。
可以这样认为,随着n值的越来越大他们在时间效率上的差异也就越来越大。
当n=1时,算法A效率不如算法B(次数比算法B多一次),而当n=2时,两者效率相同;当n>2时,算法A就开始优于算法B了,随着n的增加,算法A比算法B越来越好了(执行的次数比B要少)。于是,可以得出结论,算法A总体上要好过算法B。
此时给出定义,输入规模n在没有限制的情况下,只要超过一个数值N,这个函数就总是大于另一个函数,我们称函数是渐近增长的。
从中可以发现,随着n的增大,后面的+3还是+1其实不影响最终的算法变化,例如算法A'与算法B‘,所以,我们可以忽略这些加法常数。
例子2,算法C是4n+8,算法D是2n²+1
当n≤3的时候,算法C要差于算法D(算法C次数比较多), 但当n>3后,算法C的优势就越来越优于算法D了,到后来更是远远胜过。而当后面的常数去掉后,发现其实结果没有发生改变。甚至哪怕去掉与n相乘的常数,这样的结果也没发生改变,算法C'的次数随着n的增长,还是远小于算法D‘。即,与最高次项相乘的常数并不重要。
例子3,算法E是2n²+ 3n +1,算法F是2n³ + 3n +1
当n = 1的时候,算法E与算法F结果相同,但当n>1后,算法E的优势就要开始优于算法F,随着n的增大,差异非常明显。通过观察发现,最高次项的指数大的,函数随着n的增长,结果也会变得增长特别快。
例子4,算法G是2n²,算法H是3n +1,算法I是2n² +3n +1
这组数据就很清楚。当n的值越来越大时,会发现,3n+1已经没法和2n²的结果相比较,最终几乎可以忽略不计。也即是说,随着n值变得非常大以后,算法G其实已经很趋近与算法I。于是可以得出结论,判断一个算法的效率时,函数中的常数和其他次要项常常可以忽略,而更应该关注主项(最高阶项)的阶数。
如果可以对比算法的关键执行次数函数的渐近增长性,基本就可以分析出:某个算法,随着n的增大,它会越来越优于另一算法,或者越来越差于另一算法。这其实就是事前估算方法的理论依据,通过算法时间复杂度来估算算法时间效率。
在进行算法分析时,语句总的执行次数T(n)是关于问题规模n的函数。进而分析T(n)随n的变化情况并确定T(n)的数量级。算法的时间复杂度,也就是算法的时间量度,记作:T(n) = O(f(n))。它表示随问题规模n的增大,算法执行时间的增长率和f(n)的增长率相同,称作算法的渐近时间复杂度,简称为时间复杂度。其中f(n)是问题规模n的某个函数。
用大写O()来体现算法时间复杂度的记法,称之为达O记法。一般情况下,随着n的增大,T(n)增长最慢的算法为最优算法。
与问题的大小无关(n的多少),执行时间恒定的算法,我们称之为具有O(1)的时间复杂度,又叫常数阶。
注意:不管这个常数是多少,都记作O(1),而不能是O(3)、O(12)等其他任何数字。
对于分支结构而言,无论是真,还是假,执行的次数都是恒定的,不会随着n的变大而发生变化,所以单纯的分支结构(不包含在循环结构中),其时间复杂度也是O(1)。
线性阶的循环结构比较复杂,要确定某个算法的阶次,我们常常需要确定某个特定语句或某个语句集执行的次数。因此,我们要分析算法的复杂度,关键就是要分析循环结构的运行情况。
下面这段代码,它的循环的时间复杂度为O(n),因为循环体中的代码必须要执行n次。
int i;
for(i = 0; i < n; i++)
{
/*时间复杂度为O(1)的程序步骤序列*/
}
int count = 1;
while (count < n)
{
count = count * 2;
/*时间复杂度为O(1)的程序步骤序列*/
}
一个循环嵌套的例子:
int i,j;
for(i = 0; i < n; i++)
{
for (j = 0; j < n; j++)
{
/*时间复杂度为O(1)的程序步骤序列*/
}
}
对于外层循环,不过是内部这个时间复杂度为O(n)的语句,再循环n次。所以这段代码的时间复杂度为O(n²)。
如果外层循环次数改为m,时间复杂度就是O(mxn)。
int i,j;
for(i = 0; i < m; i++)
{
for (j = 0; j < n; j++)
{
/*时间复杂度为O(1)的程序步骤序列*/
}
}
所以可以总结出,循环的时间复杂度等于循环体的复杂度乘以该循环运行的次数。
那下面这个循环嵌套,它的时间复杂度是多少呢?
int i,j;
for (i = 0; i < n; i++)
{
for (j = i; j < n; j++) /*注意j = i而不是0*/
{
/*时间复杂度为O(1)的程序步骤序列*/
}
}
由于当i = 0时,内循环执行了n次,当i =1时,执行了n - 1次,......当i = n - 1时,执行了1次。所以总的执行次数为:
用推导大O阶的方法,第一条,没有加法常数不予考虑;第二条,只保留最高阶项,因此保留n²/2;第三条,去除这个项相乘的常数,也就是去除1/2,最终这段代码的时间复杂度为O(n²)。
继续看例子,对于方法调用的时间复杂度又如何分析。
int i,j;
for (i = 0; i < n; i++)
{
function(i);
}
void function(int count)
{
print(count);
}
function函数的时间复杂度为O(1),所以整体时间复杂度为O(n)。
假设function是下面这样:
void function(int count)
{
int j;
for (j = count; j < n; j++)
{
/*时间复杂度为O(1)的程序步骤序列*/
}
}
这和之前的例子一样,只不过是把嵌套内循环放到了函数中,所以最终的时间复杂度为O(n²)。
我们查找一个有n个随机数字数组中的某个数字,最好的情况是第一个数字就是,那么算法的时间复杂度为O(1),但也有可能这个数字在最后一个位置,那么算法的时间复杂度就是O(n),这是最坏的一种情况。
最坏情况运行时间是一种保证,那就是运行时间将不会更坏了。在应用中,这是一种最重要的需求,通常,除非特别指定,我们提到的运行时间都是最坏情况的运行时间。
而平均时间也就是从概率的角度看,这个数字在每一个位置的可能性是相同的,所以平均的查找时间为n/2次后发现这个目标元素。
平均运行时间是所有情况中最有意义的,因为它是期望的运行时间。也即是说,我们运行一段程序代码时,是希望看到平均运行时间的。可实际上,平均运行时间很难通过分析得到,一般都是通过运行一定数量的实验数据后估算出来的。
对算法的分析,一种方法是计算所有平均情况的平均值,这种时间复杂度的计算方法称为平均时间复杂度。另一种是计算最坏情况下的时间复杂度,这种方法称为最坏时间复杂度。一般在没有特殊说明的情况下,都是指最坏时间复杂度。
一个空间换时间的例子:要判断某年是不是闰年,如果写一个算法,也就意味着每次给一年份,都要通过计算得到是否是闰年的结果。还有一个办法,事先建立一个有2050个元素的数组(年数略比现实多一点),然后把所有的年份按下标的数字对应,如果是闰年,此数组项的值就是1,如果不是值为0。这样,所谓的判断某一年是否是闰年,就变成了查找这个数组的某一项的值是多少的问题。此时,我们的运算是最小化了,但是硬盘上或者内存中需要存储这2050个0和1。
算法的空间复杂度通过计算算法所需的存储空间实现,算法空间复杂度的计算公式记作:S(n) = O(f(n)),其中,n为问题的规模,f(n)为语句关于n所占存储空间的函数。
一般情况下,一个程序在机器上执行时,除了需要存储程序本身的指令、常数、变量和输入数据外,还需要存储对数据操作的存储单元。若输入数据所占空间只取决于问题本身,和算法无关,这样只需要分析该算法在实现时所需的辅助单元即可。若算法执行时间所需的辅助空间相对于输入数据量而言是个常数,则称此算法为原地工作,空间复杂度为O(1)。
2.13 总结回顾
算法的定义:算法是解决特定问题求解步骤的描述,在计算机中为指令的有限序列,并且每条指令表示一个或多个操作。
算法的特性:有穷性、确定性、可行性、输入、输出。
算法的设计要求:正确性、可读性、健壮性、高效率和低存储量需求。
算法的度量方法:事后统计方法(不科学、不准确)、事前分析估算方法。
函数的渐近增长:给定两个函数f(n)和g(n),如果存在一个整数N,使得对于所有的n > N,f(n)总是比g(n)大,那么,我们就说f(n)的增长渐近快于g(n)。于是,我们可以得出一个结论,判断一个算法好不好,我们只通过少量的数据是不能做出准确判断的,如果我们可以对比算法的关键执行次数函数的渐近增长性,基本就可以分析出:某个算法,随着n的变大,它会越来越优于另一算法,或者越来越差于另一算法。
推导大O阶:
得到的结果就是大O阶。
推导大O阶很容易,但是如果得到运行次数的表达式却是需要数学功底!!!
常见的时间复杂度所耗时间的大小排列:
最后,给出了关于算法最坏情况和平均情况的概念,以及空间复杂度的概念。
<这段简直就是大多数人的现实。。。。,全文摘抄>
很多学生,学了四年计算机专业,很多程序员,做了很长时间的编程工作,却始终都弄不明白算法的时间复杂度的估算,这是很可悲的一件事。因为弄不清楚,所以也就不深究自己写的代码是否效率低下,是不是可以通过优化让计算机更加快速高效。
他们通常的借口是,现在CPU越来越快,根本不用考虑算法的优劣,实现功能即可,用户感觉不到算法好坏造成的快慢。可事实真是这样吗?还是让我们用数据来说话吧。