算法(algorithm)
- 是指令的集合,是为解决特定问题而规定的一系列操作
- 它是明确定义的可计算过程,以一个数据集合作为输入,并产生一个数据集合作为输出
- 一个算法通常来说具有一下五个特性:
1)输入:一个算法应该以待解决的问题的信息作为输入
2)输出:输入对应指令集处理后得到的信息
3)可行性:算法是可行的,即算法中的每一条指令都是可以实现的,均能在有限的时间内完成
4)有穷性:算法执行的指令个数是有限的,每个指令又是在有限时间内完成的,因此整个算法也是在有限时间内可以结束的
5)确定性:算法对于特定的合法输入,其对应的输出是唯一的,即当算法从一个特定输入开始,多次执行同一指令集结果总是相同的 - 简单的说,算法就是计算机解题的过程
- 在这个过程中,无论是形成解题思路还是编写程序,都是在实施某种算法
- 前者是算法的逻辑形式,后者是算法的代码形式
评价算法优劣的依据:复杂度(时间复杂度和空间复杂度)
- 算法的复杂度体现在运行该算法时的计算机所需资源的多少上,计算机资源最重要的是时间和空间资源,因此复杂度分为时间和空间复杂度
- 时间复杂度是指执行算法所需要的计算工作量
- 空间复杂度是指执行这个算法所需要的内存空间
时间复杂度(Time Complexity)
-
时间频度
1)一个算法执行所耗费的时间,从理论上是不能算出来的,必须上机运行测试才能知道,但我们不可能也没有必要对每个算法都上机测试
2)一个算法花费的时间与算法中语句的执行次数成正比例,哪个算法中语句执行次数多,它花费时间就多
3)一个算法中的语句执行次数称为语句频度或时间频度,表示为T(n),n表示问题的规模 -
时间复杂度
1)有时我们想知道它变化时呈现什么规律,想知道问题的规模,而不是具体的次数,此时引入时间复杂度
2)一般情况下,算法中基本操作重复执行的次数是问题规模n的某个函数,用T(n)表示
3)若有某个辅助函数f(n),使得当n趋近于无穷大是,T(n)/f(n)的极限值为不等于零的常数,则称f(n)是T(n)的同数量级函数。记作T(n)=O(f(n)),称O(f(n))为算法的渐近时间复杂度,简称时间复杂度。或者说:时间复杂度就是时间频度去掉低阶项和首项常数
4)注意:时间频度与时间复杂度是不同的,时间频度不同但时间复杂度可能相同
eg. 某两个算法的时间频度是T(n) = 100000 n 2 + 10 n + 6 100000n^2+10n+6 100000n2+10n+6 T ( n ) = n 2 T(n)=n^2 T(n)=n2,但是时间复杂度都是T(n)=O( n 2 n^2 n2)
最坏时间复杂度和平均时间复杂度
- 最坏情况下的时间复杂度称最坏时间复杂度。一般不特别说明,讨论的时间复杂度均是最坏情况下的时间复杂度。
- 这样做的原因是:最坏情况下的时间复杂度是算法在任何输入实例上运行时间的上界,这就保证了算法的运行时间不会比任何更长。
- 在最坏情况下的时间复杂度为T(n)=O(n),它表示对于任何输入实例,该算法的运行时间不可能大于O(n)
- 平均时间复杂度是指所有可能的输入实例均以等概率出现的情况下,算法的期望运行时间。鉴于平均复杂度难计算,且有很多算法的平均情况和最差情况的复杂度是一样的,所以一般讨论最坏时间复杂度
- 为了进一步说明算法的时间复杂度,我们定义O、Ω、θ符号
1)O符号给出了算法时间复杂度的上界(最坏情况<=),比如T(n)=O( n 2 n^2 n2)
2)Ω符号给出了时间复杂度的下界(最好情况>=),比如呀T(n)=Ω( n 2 n^2 n2)
3)θ给出了算法时间复杂度的精确阶(最好和最坏是同一个阶=),比如T(n)=θ( n 2 n^2 n2)
时间复杂度计算
根本没有必要计算时间频度,即使计算处理还要忽略常量、低次幂和最高次幂的系数,所以可以采用如下简单方法:
(1)找出算法中的基本语句
算法中执行次数最多的那条语句就是基本语句,通常是最内层循环的循环体
(2)计算基本语句的执行次数的数量级
只需计算基本语句执行次数的数量级,这就意味着只要保证基本语句执行次数的函数中的最高次幂正确即可,可以忽略所有低次幂和高次幂的系数。这样能够简化算法分析,并且使注意力集中在最重要的一点上:增长率
(3)用大O记号表示算法的时间性能
将基本语句执行次数的数量级放入大O记号中
eg.
- 一个简单语句的时间复杂度为O(1)
int count = 0; - 100个简单语句的时间复杂度也为O(1)。(100是常数,不是趋向无穷大的n)
int count = 0;
count = 0;
count = 0;
count = 0;
count = 0;
…
count = 0; - 一个循环的时间复杂度为O(n)
int n=8, count=0;
for(int i=1;i<=n;i++){ //就算i<=10n+100,复杂度还是n
count++; //基本语句
}
T(n) = n
T(n) = O(n)
- 时间复杂度为O( l o g 2 n log_2n log2n)的循环语句
int n=8,count=0;
for(int i=1;i<=n;i*=2){
count++;
}
log2(n)效率非常高
- 时间复杂度为O( n 2 n^2 n2)的二重循环
int n=8,count=0;
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
count++;
}
}
- 时间复杂度为O( n l o g 2 n nlog_2n nlog2n)的二重循环
int n=8, count=0;
for(int i=1;i<=n;i*2){
for(int j=1;j<=n;j++){
count++;
}
}
- 时间复杂度为O( n 2 n^2 n2)的二重循环
int n=8,count=0;
for(int i=1;i<=n;i++){
for(int j=1;j<=i;j++){ //内循环执行的次数不确定
count++;
}
}
当i=1,语句执行1次,i=2,语句执行2次......
需要复杂的数学运算:1+2+3+...+n=(n+1)*n/2,这是时间频度
等于1/2n^2 + 1/2n
时间复杂度是O( n 2 n^2 n2)
常用的时间复杂度级别
- 常数阶O(1)
- 对数阶O( l o g 2 n log_2n log2n)
- 线性阶O(n)
- 线性对数阶O( n l o g 2 n nlog_2n nlog2n)
- 平方阶O( n 2 n^2 n2)
- 立方阶O( n 3 n^3 n3)
- …
- k次方阶O( n k n^k nk)
- 指数阶O( 2 n 2^n 2n)
- 阶乘阶O(n!)
上面各种时间复杂度级别,时间复杂度越来越高,执行效率越来越低
时间复杂度 | n=8(2^3) | n=10 | n=100 | n=1000 |
O(1) | 1 | 1 | 1 | 1 |
O(logn) | 3 | 3.322 | 6.644 | 9.966 |
O(n) | 8 | 10 | 100 | 1000 |
O(nlogn) | 24 | 33.22 | 664.4 | 9966 |
O(n^2) | 64 | 100 | 10000 | 1000000 |
空间复杂度(Space Complexity)
算法的存储量包括:
- 程序本身所占空间
- 输入数据所占空间
- 辅助变量所占空间
输入数据所占空间只取决于问题本身,和算法无关,则只需要分析除输入和程序之外的辅助变量所占额外空间。
空间复杂度是对一个算法在运行过程中临时占用的存储空间大小的量度,一般也作为问题规模n的函数,以数量级形式给出,记作: S(n) = O(g(n))
eg.
int fun(int n){
int i, j, k, s; //只需要四个空间
s = 0;
for(i=0;i<=n;i++){
for(j=0;j<=i;j++){
for(k=0;k<=j;k++){
s++;
}
}
}
return(s);
}
由于算法中临时变量的个数与问题规模n无关,所以空间复杂度均为S(n)=O(1)
void fun(int a[], int n, int k)
//数组a共有n个元素
{
int i;
if(k==n-1){
for(i=0;i<n;i++){
print("%d\n", a[i]); //执行n次
}
}else{
for(i=k;i<n;i++){
a[i]=a[i]+i*i; //执行n-k次
}
fun(a,n,k+1);
}
}
递归算法,每次调用本身都要分配空间,fun(a,n,0)的空间复杂度为O(n)
使用递归,效率低下,占用空间多,但思路简单
注意:
- 空间复杂度相比时间复杂度分析要少
- 对于递归算法来说,代码一般都比较简短,算法本身所占用的存储空间较少,但运行时需要占用较多的临时工作单元;若写成非递归算法,代码一般可能比较长,算法本身占用的存储空间较多,但运行时将可能需要较少的存储单元