算法是解决特定问题求解步骤的描述,在计算机中表现为指令的有限序列,并且每条指令表示一个或多个操作。
算法有五个基本特性:输入、输出、有穷性、确定性和可行性。
算法具有零个或多个输入,至少有一个或多个输出,输出的形式可以是打印输出,也可以是返回一个或多个值等。
算法在执行有限的步骤之后,自动结束而不会出现无限循环,并且每个步骤在可接受的时间内完成。
算法的每一步骤都具有明确的含义,不会出现二义性。
算法的每一步都必须是可行的,每一步都能够执行有限次数完成。
正确性、可读性、健壮性、高时间效率和低存储量
算法的正确性是指算法至少应该具有输入、输出和加工处理无歧义性、能正确反映问题的需求、能够得到问题的正确答案。
算法的“正确”分为四个层次
层次1要求最低,层次4要求最难,一般情况下,把层析3作为一个算法是否正确的标准。
算法设计的另一目的是为了便于阅读、理解和交流。
可读性是算法好坏很重要的标志。
当输入数据不合法是,算法也能做出相关处理,而不是产生异常或莫名其妙的结果。
时间效率指的是算法的执行时间,储存量需求指的是算法在执行过程中需要的最大存储空间,主要值算法程序运行时所占用的内存或外部硬盘存储空间。
设计算法应该尽量满足时间效率高和存储量低的要求。
通过设计好的测试程序和数据,利用计算机计时器对不同算法编制的查询的运行时间进行比较,从而确定算法效率的高低。
这种方法具有很大的缺陷:
在计算机查询编制前,依据统计方法对算法进行估算。
一个高级程序语言编写的查询在计算机上运行时所消耗的实际取决于以下因素:
第 1条当然是算法好坏的根本,第2条要由软件来支持,第4条要看硬件性能。
也就是说,抛开这些与计算机硬件、软件有关的因素,一个程序的运行时间,依赖于算法的好坏和问题的输入规模。所谓问题输入规模是指输入量的多少。
三种求和算法:
第一种
int i;
int sum = 0;
int n = 100;
for(i = 1;i<=n;i++){
sum += i;
}
System.out.println(sum);
第二种
int sum = 0;
int n = 100;
sum = (1+n)*n/2;
System.out.println(sum);
第三种
int x = 0;
int sum = 0;
int n = 100;
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
x++;
sum += x;
}
}
System.out.println(sum);
由此可见,第一种f(n)=n
,第二种需要f(n)=1
,第三种需要f(n)=n^2^
函数的渐进增长:给定两个函数
f(n)
和g(n)
,如果存在一个整数N,使得对于所有的n>N,f(n)
总是比g(n)
大,那么,f(n)
的增长渐进是快于g(n)
假设算法C执行次数是4n+8,算法D是2n2+1,那么
次数 | 算法C(4n+8) | 算法C’(n) | 算法D(2n2+1) | 算法D’(n2) |
---|---|---|---|---|
n =1 | 12 | 1 | 3 | 1 |
n =2 | 16 | 2 | 9 | 4 |
n =3 | 20 | 3 | 19 | 9 |
n =10 | 48 | 10 | 201 | 100 |
n =100 | 408 | 100 | 20001 | 10000 |
n =1000 | 4008 | 1000 | 2000001 | 1000000 |
由此可见,最高次项的常数不重要。
假设算法E是2n2+3n+1,算法F是2n3+3n+1,那么
次数 | 算法E(2n2+3n+1) | 算法E’(n2) | 算法F(2n3+3n+1) | 算法F’(n3) |
---|---|---|---|---|
n =1 | 6 | 1 | 6 | 1 |
n =2 | 15 | 4 | 23 | 8 |
n =3 | 28 | 9 | 64 | 27 |
n =10 | 231 | 100 | 2031 | 1000 |
n =100 | 20301 | 10000 | 200 | |
01 | 1000000 |
由此可见,最高次项指数大的,函数随着n的增长,结果也会变得增长特别快。
假设算法G执行次数是2n2,算法H是3n+1,算法I是2n2+3n+1那么
次数 | 算法G(2n2) | 算法H(3n+1) | 算法I(2n2+3n+1) |
---|---|---|---|
n =1 | 2 | 4 | 6 |
n =2 | 8 | 7 | 15 |
n =5 | 50 | 16 | 66 |
n =10 | 200 | 31 | 231 |
n =100 | 20000 | 301 | 20301 |
n =1000 | 2000000 | 3001 | 2003001 |
由此可见,判断个算法的效率时,函数中的常数和其他次要项常常可以忽略,而更应该关注主项(最高阶项)的阶数。
判断一个算法好不好,我们只通过少量的数据是不能做出准确判断的。根据刚才的几个样例,我们发现,如果我们可以对比这几个算法的关键执行次数函数的渐近增长性,基本就可以分析出:某个算法,随着n的增大,它会越来越优于另一算法,或者越来越差于另一算法。这其实就是事前估算方法的理论依据,通过算法时间复杂度来估算算法时间效率。
在进行算法分析时,语句总的执行次数T(n)是关于问题规模n的函数,进而分析T(n)随n的变化情况并确定T(n)的数量级。算法的时间复杂度,也就是算法的时间度量,记作:T(n)=O(f(n))。它表示随问题规模n的增大,算法执行时间的增长率和f(n)的增长率相同,称作算法的渐近时间复杂度,简称为时间复杂度。其中f(n)是问题规模n的某函数。
一般情况下,随着n的增大,T(n)增长最慢的算法为最优算法。
int sum = 0;
int n = 100;
sum = (1+n)*n/2;
sum = (1+n)*n/2;
sum = (1+n)*n/2;
sum = (1+n)*n/2;
sum = (1+n)*n/2;
System.out.println(sum);
以上代码执行8次,则时间复杂度为O(1)。
for(int i=0;i<n;i++){
//时间复杂度为O(1)的程序步骤
}
以上代码,时间复杂度为O(n)
int count = 1;
while(count<n){
count = count *2;
}
由于count乘以2之后,就距离n更近了一分,也就是有多少个2相乘会大于n,因此由2x=n,得到x=log2n,所以以上代码时间复杂度为O(logn)
for(int i=0;i<n;i++){
for(int j=0;<n;j++){
//时间复杂度为O(1)的程序步骤序列
}
}
以上代码,时间复杂度为O(n2)
for(int i=0;i<n;i++){
for(int j=0;<m;j++){
//时间复杂度为O(1)的程序步骤序列
}
}
以上代码,时间复杂度为O(n*m)
for(int i=0;i<n;i++){
for(int j=i;<m;j++){
//时间复杂度为O(1)的程序步骤序列
}
}
当i=0是,内循环执行了n次,当i=1时,内循环执行了n-1次,……,当i=n-1时,内循环执行了1次,所以总执行次数为
n+(n-1)+(n-2)+···+1 = n 2 2 {{n^2}\over 2} 2n2+ n 2 {n\over 2} 2n
最终得到这段代码的时间复杂度是O(n2)
void f(){
for(int i=0;i<n;i++){
function(i);
}
}
void function(int i){
System.out.println(i);
}
以上代码时间复杂度为O(n)
void f(){
for(int i=0;i<n;i++){
function(i);
}
}
void function(int i){
for(int j=i;j<n;j++){
System.out.println(i);
}
}
以上代码时间复杂度为O(n2)
算法的空间复杂度通过计算算法所需的存储空间实现,算法空间复杂度的计算公式记作:S(n)=O(f(n)),其中,n为问题的规模,f(n)为语句关于n所占存储空间的函数。