算法描述了解决问题的具体步骤和过程,专业的程序员必须学会对算法的执行时间和占用空间进行分析,找出瓶颈进行针对性的优化
算法时间复杂度分析
- 事后统计分析方法:编写算法对应程序,统计其执行时间。但实际统计出来的时间会受到诸多因素的影响,例如程序设计语言(论执行效率汇编语言有话说),执行程序的环境(机器处理性能)等等
- 事前估算分析方法:认为算法的执行时间是问题规模 n 的函数
求解步骤
- 求出算法所有原操作的执行次数(也称为频度),它是问题规模 n( 一般是正整数 )的函数,用 T(n)表示
- 算法执行时间大致 = 原操作所需的时间 × T(n)。即 T(n)与算法的执行时间成正比,可以用 T(n)表示算法的执行时间
- 比较不同算法的 T(n)大小得出算法执行时间的好坏
复杂度的渐进表示法
T(n)是问题规模 n 的某个函数 f(n),记作:
O 表示随问题规模 n 的增大,算法执行时间的增长率和 f(n)的增长率相同
即 f(n)是 T(n)的上界,通常取最接近的上界
当问题规模 n 最够大时,可以忽略低阶项和常系数,我们只需求出 T(n)的最高阶,比较 T(n)本质上在比较最高数量级
时间复杂度的比较关系
实际上 log 的底数并不一定是 2,底数会根据具体的情况而变化,在后面的例题中我们会详细讲解
O(1) | 没有循环的算法。其执行时间与问题规模 n 无关 |
O(n) | 只有一重循环的算法。其执行时间与问题规模 n 的增长呈线性关系 |
就拿我们最熟悉的素数判断算法来说,常见的两种判断算法
- 算法一
for( i = 2; i if( i % n == 0 ){
printf("No");
break;
}
}
printf("Yes");
- 算法二
for( i = 2; i <= sqrt(n); i++ ){
if( i % n == 0 ){
printf("No");
break;
}
}
printf("Yes");
显然,算法一的时间复杂度是O(n),算法二的时间复杂度是O(sqrt(n)),相比之下算法二的效率更高。
在处理 5,13 这样小规模的数据时两者几乎没有差别(计算机的执行时间通常是以纳秒为单位,差了几纳秒时通常可以忽略)。
一旦给出 100000 这样大规模的数据时,我们就要花很长的时间去等算法一的程序出结果,如果再大,大的无法想象,可能到世界末日都等不出结果。
上下文和嵌套的关系
设算法 A 的复杂度
算法 B 的复杂度
AB互为上下文关系时:
AB互为嵌套关系时:
举个栗子
算法A
for( i = 0; i printf("%d", i);
}
算法B
for( j = 0; j printf("%d", j);
}
不难看出
当 AB 互为嵌套关系时
for( i = 0; i printf("%d", i);
for( j = 0; j printf("%d", j);
}
}
简化的算法时间复杂度分析
算法中的基本操作一般是最深层循环内的原操作。计算 T(n)时仅仅考虑基本操作的运算次数。
例题 1
求这段代码的时间复杂度
int cnt = 0;
for( k = 1; k <= n; k *= 2 ){
for( j = 1; j <= n; j++ ){
cnt++;
}
解:由题得,内层循环 n 次,设外层循环 x 次,由题设:
解得
同时两者互为嵌套关系,所以总时间复杂度为两者相乘
如果把 k *= 2 改为 k *= 3,又该如何计算呢?
显然我们只需要将底数换为 3 即可。所以 log 的底数要根据具体问题具体分析
例题 2
求这段代码的时间复杂度
int i = 0, s = 0;
while( s i++;
s += i;
}
解:变量 i 从 0 开始递增 1,s 可视为首项为 0,公差为 1 的等差数列的和。设循环次数为 x
循环结束时:
加上一个修正常量 k,把不等式变成等式
解得
只考虑最高数量级,即
算法的空间复杂度分析
空间复杂度用于量度一个算法在运行过程中「临时占用」的存储空间的大小。一般也作为问题规模 n 的函数,采用数量级形式描述,记作
如果一个算法的空间复杂度表示为 O(1),即空间复杂度与问题规模 n 无关,我们称这个算法为原地工作或就地工作算法。
平均时间复杂度
设一个算法的输入规模为 n,Dn是所有输入的集合,任一输入 I∈Dn,P(I) 是 I 出现的概率
T(I) 是算法在输入 I 下的执行时间,则算法平均时间复杂度为
算法的最坏时间复杂度 W(n)
算法的最好时间复杂度 B(n)
最好和最坏通常是指一种或几种特殊情况
例题
分析算法的平均时间复杂度。其中 1<=i<=n
intfun( int a[], int n, int i ){
int j, max = a[0];
for( j = 1; j <= i - 1; j++ ){
if( a[j] > max ){
max = a[j];
}
}
return max;
}
对于求前 i 个元素的最大值时,需要元素比较 i-1 次(最大值初始化为 a[0],从 a[1]开始比较) 在每种情况出现的概率相等,概率均为 1/n 的情况下 平均时间复杂度:
平均时间算法复杂度为 O(n)
当 i = n 时 ,有最坏时间复杂度 O(n)
当 i = 1 时,有最好时间复杂度 O(1)
平均时间复杂度的求解难度比较大,在实际问题中我们一般只关心最好时间复杂度和最坏时间复杂度。
递归算法的时空复杂度分析
递归的算法是指算法中出现调用自己的成分。在进入下一层递归之前,机器会把当前的所有状态压入堆栈存储起来,等到最后递归到了最后一层,再逐个弹出。所以递归算法在时间上和空间上和非递归算法有很大的区别。递归算法也称为变长时空分析,非递归算法分析也称为定长时空分析。两者的分析方法大有不同。
求解递归算法时空复杂度的一般步骤
- 确定问题规模 n
- 确定终止情况
- 确定递推情况
- 由递推关系求出 T(n)
- 用复杂度表示 T(n)
例题
分析时间复杂度和空间复杂度
voidfun( int a[], int n, int k ){
int i;
if( k == n - 1 ){
for( i = 0; i //执行n次
printf("%d\n", a[i]);
}
}else{
for( i = k; i //执行n-k次
a[i] += i * i;
}
fun(a,n,k+1);
}
}
时间复杂度
设 fun( a, n, k )的执行时间为 T(n),fun( a, n, k )的执行时间为 T( n, k ),有
递推关系
则
空间复杂度
设 fun( a, n, k )的空间为 S(n),fun( a, n, k )的空间为 S( n, k ),有
递推关系