# 算法效率的度量方法
算法的效率被称为算法之魂,直观的说,算法执行的越快,它的效率就越高,那么我们要怎么综合度量一个算法的效率呢?
对时间的测定有两种方法:
- 事后统计方法:这种方法主要是通过设计好的测试程序和数据,利用计算机对不同算法编制的程序的运行时间作比较,从而确定算法效率的高低。(有很大缺陷)
- 事前分析估算方法:在计算机程序编写前,依据统计方法对算法进行估算。经过总结,一个高级语言编写的程序在计算机上运行是所消耗的时间取决于下列因素:
- 算法所采用的方案策略
- 编译产生的代码质量
- 输入规模
- 机器执行指令的速度
我们在分析一个算法的运行时间时,总要把基本操作的数量和输入模式关联起来
# 函数的渐进增长
假设两个算法的输入规模都是n,算法A要做2n+3次操作,可以理解为:先进行一个n次的循环,再进行一个n次的循环,最后有3次运算。算法B要做3n+1次操作,理解同上,那么哪一个更快?
规模 | 算法A1(2n+3) | 算法A2(2n) | 算法B1(3n+1) | 算法B2(3n) |
---|---|---|---|---|
n=1 | 5 | 2 | 4 | 3 |
n=2 | 7 | 4 | 7 | 6 |
n=3 | 9 | 6 | 10 | 9 |
n=10 | 23 | 20 | 31 | 30 |
n=100 | 203 | 200 | 301 | 300 |
随着n的增加,可以看出A算法更优。
函数的渐进增长:给定两个函数f(n)和g(n),如果存在一个整数N,使得对于所有的n>N,f(n)总是比g(n)大,那么我们说f(n)的增长渐进快于g(n)。(加法常数可以忽略)
我们可以以3n+1,2n^2,n^3为例再次进行实验得到下面的结论:
算法效率主要和主项(最高次项)的阶数相关。
注意不要以偏概全,实验数据要丰富。
# 算法时间复杂度
在进行算法分析时,语句总的执行次数T(n)是关于问题规模n的函数,进而分析T(n)的随n的变化情况并确定T(n)的数量级。算法时间复杂度,也就是算法的时间量度,记作:T(n) = O(f(n))。它表示随问题规模n的增大,算法执行时间的增长率和f(n)的增长率相同,称作算法的渐进时间复杂度,简称为时间复杂度。其中f(n)是问题规模n的某个函数。
这样用大写O()来体现算法时间复杂度的记法,我们称之为大O记法。
一般情况下,随着输入规模n的增大,T(n)增长最慢的算法为最优算法。
# 推导大O阶方法
注意: 下面的方法并不精确,只是一个粗略的估计
- 用常数1取代运行时间中的所有常数
- 再修改后的运行次数函数中,只保留最高阶项
- 如果最最高阶想存在且不是1,则去除与这个项相乘的常数
- 得到的最后结果就是大O阶
# 常数阶
int sum = 0,n = 100;
cout<<"I am a students";
cout<<"这是数据结构与算法课程";
sun = (1+n)*n/2;
2
3
4
这段代码的大O阶是多少呢,这个只有一条和n相关的指令,很明显是O(1)。
# 线性阶
一般含有非浅套循环涉及线性阶,线性阶就是随着问题规模n的扩大,对应计算次数呈直线增长。
int i ,n =100, sum = 0;
for( i = 0;i<n;i++){
sum = sum + i;
}
//上面这段代码,循环的时间复杂度为O(n),因为循环体中的代码需要执行n次。
2
3
4
5
# 平方阶
如果循环是嵌套的,代码如下
int i,j,n = 100;
for(i = 0; i<n; i++){
for(j = 0;j<n;j++){
cout<<"数据结构与算法";
}
}
//这段代码的时间复杂度为O(n^2)
2
3
4
5
6
7
我们可以总结出,循环的时间复杂度等于循环体的复杂度乘改循环运行的次数
更改一下代码
int i,j,n = 100;
for(i = 0; i<n; i++){
for(j = i;j<n;j++){
cout<<"数据结构与算法";
}
}
2
3
4
5
6
这个总的执行次数应该是n+(n-1)+(n-2)+...+1 = n(n+1)/2 = n^2/2 + n/2
按照我们的规律,第一条忽略,因为没有常数相加;第二条只保留最高项,所以去掉n/2;第三条,去除与最高项相乘的常数,最终得到O(n^2)。
# 对数阶
int i = 1,n = 100;
while(i<n){
i = i*2;
}
2
3
4
由于是由2^x = n得到x = log~2~n,所以这个循环的时间复杂度为O(logn)。
# 函数调用的时间复杂度分析
int i ,j;
for(i = 0;i<n;i++){
function(i);
}
void function(int count){
cout<<count;
}
2
3
4
5
6
7
function的时间复杂度是O(1),所以循环的时间复杂度是O(n)
如果更改function如下:
void function(int count){
int j;
for(j = count;j<n;j++){
cout<<j;
}
}
2
3
4
5
6
这样就变成了O(n^2^)
void function(int count){
int j;
for(j = count;j<n;j++){
cout<<j;
}
}
n++;//执行一次
function(n);//n^2次
for(i = 0;i<n;i++){//n^2次
function(i);
}
for(i= 0;i<n;i++){//n^2次
for(j = i;j<n;j++){
cout<<j;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
得到的复杂度是O(n^2^)
例子 | 时间复杂度 | 术语 |
---|---|---|
123 | O(1) | 常数阶 |
3n+4 | O(n) | 线性阶 |
3n^2^+4n | O(n^2^) | 平方阶 |
3log~2~n+4 | O(logn) | 对数阶 |
3nlog~2~n+4 | O(nlogn) | nlogn阶 |
2^n^ | O(2^n^) | 指数阶 |
常用的时间复杂度所耗费的时间从小到大依次是:
O(1) < O(logn) < O(n) < O(nlogn) < O(n^2^) < O(n^3^) < O(2^n^) < O(n!) < O(n^n^)
# 最坏情况与平均情况
比如,当我们遍历一个数组来查找一个数字,最好的情况是第一个就找到O(1);最坏的情况是在最后O(n)
# 算法的空间复杂度
算法的空间复杂度通过计算算法所需的存储空间实现,算法的空间复杂度的计算公式记作S(n)=O(f(n)),其中,n为问题的规模,f(n)为语句关于n所占存储空间的函数。
程序所需要存储空间分为一下几个部分
- 固定部分,主要包括存放程序指令代码的空间,常数、简单变量、定长成分变量所占的空间等。
- 可变部分,这部分主要暴扣器与问题规模有关的变量所占空间递归工作栈所用的空间,以及动态申请的空间。