算法分析
什么是算法
算法字面意思,计算方法;
算法规定了求解给定类型问题所需的所有处理步骤
以及执行顺序
,使得问题能在有限时间内机械的求解,一个算法就是对特定问题求解步骤的一种描述,再具体一点,算法是一段有穷的指令序列;算法必须能使用某种语言描述;
例如:
计算1到5的和 ,这个需求,如何来实现,第一步做什么,第二步做什么,整个计算步骤和执行顺序统称为算法,如果最终能够在有限的步骤下求出正确的和,那这就是一个合格的算法;
算法的特点:
-
有穷性
算法必须在执行有穷步后结束
-
确定性
算法的每一个步骤都必须是明确定义的,
-
可行性
算法中的每一步都是可以通过已实现的操作来完成的
-
输入
一个算法具备0或多个输入
-
输出
一个算法有一个或多个输出,它们与输入有着特定的关系
算法与程序的区别,算法只是一种描述,可以使用任何语言,但是通常不能直接被计算机运行,而程序则是算法的具体实现,使用某种计算机语言;
算法设计应满足的要求
-
正确性:对于合法的输入产生符合要求的输出
-
易读性:算法应该尽可能易读,便于交流,这也是保证正确性的前提(注释可提高易读性)
-
健壮性:当输入非法数据时,算法可作出适当反应而不至于崩溃(例如输出错误原因);
-
时空性:指的是算法的时间复杂度和空间复杂度,算法分析主要也是分析算法的时间复杂度和空间复杂的,其目的是提高算法的效率;
算法分析
解决同一问题的算法可能有多种,我们希望从中选出最优的算法,效率高的,占用空间小的,为此我们就需要对算法进行评估和分析;
通常评估算法根据两个度量
- 时间复杂度:算法运行完成所需的总步数(标准操作),通常是问题规模的函数
- 空间复杂度:算法执行时所占用的存储空间,通常是问题规模的函数
时间复杂度
确定算法的计算量
-
合理选择一种或几种操作作为'标准操作',无特殊说明默认以赋值操作作为标准操作;
-
确定算法共执行多少次标准操作,并将此次数规定为算法的计算量
-
以算法在所有时输入下的计算量最大值作为算法的最坏情况时间复杂度
-
以算法在所有时输入下的计算量最小值作为算法的最好情况时间复杂度
-
以算法在所有时输入下的计算量平均值作为算法的平均情况时间复杂度
-
最坏和平均情况时间复杂度统称为时间复杂度;
-
它们通常拥有相同的大O表示法
-
如:最坏:O(n) 平均O(n/2) 忽略系数后都为O(N)
-
注意:时间复杂度通常以量级来衡量,也就是说不需要精确的计算到底执行了几步,而是得出其计算量的数量级即可,并忽略常数,因为当数量级足够大时,常数对于计算量的影响可以忽略不计;
如: (n-1)(n-2) 数量级为 n^2
时间复杂度使用大O表示,如O(1)
案例:
1.
void aFunction(){
int c = 10 + 20;
int d = c * c;
printf(d);
}
上列算法若以赋值运算作为标准操作,则该算法的计算量为2,其时间复杂度记为O(1),为什么是O(1)呢,是因为2是一个常数,常数对于函数的增长影响并不大,所以计算量为常数时表示为O(1),按照这种方式,即使计算量为2000,同样记为O(1),称为常数阶
2.
void bFunction(int n){
for(int i = 0;i < n;i++){ // n
int c = 2 * i;// 1
int d = 3 * i;// 2
}
}
此时函数的循环次数由未知数n来决定,循环体内计算量为2,当n是一个自然数时,函数的计算量等于(n)(2),此时时间复杂度为O(n),无论用常数2
对n进行加减乘除对于n的指数都没有影响,当n足够大时,内部的2次计算量可以忽略,所以记为O(n),称为线性阶
更粗陋的度量方法是函数体包含一层循环时记为O(n)
3.
void bFunction(int n){
for(int i = 0;i < n;i++){
for(int j = 0;j < i;j++){
}
}
}
外层循环次数为n,内层循环次数随着n的增长而增长且最大值为n-1次,那么整个函数的计算量为(n)(n-1),常数可以忽略,所以时间复杂度记为O(n^2) ,称为平方阶
粗陋的方法是有两层嵌套循环,且循环次数都随着n的增长而增长,所以是O(n^2),以此类推,但是要注意下面这种情况
4.
void bFunction(int n){
for(int i = 0;i < n;i++){
for(int j = 0;j < 3;j++){
}
}
}
此时内层循环的循环次数是固定(常数)所以不会影响计算量的数量级,时间复杂度记为O(n)
5.
void bFunction(int n){
for(int i = 3;i < n;){
i *= 3;
}
}
此时函循环次数会随着3循环体中的常数3的的变化而变化,我们可以用对数来表示,
假设循环次数为s,循环条件可表示为 s = 3^s < n;(即i本身为3,其次幂会不断的增长,但结果要小于n)
当然这里有个条件是i的初值必须和每次乘等的值相同,形成次幂的增长;
用对数表示为s = log3n,时间复杂度记为O(log3n),常数可以忽略,所以最后是O(logn)称之为对数阶
对数阶的数量级小于线性阶,因为若n的值相同,对数阶计算量必然小于线性阶
6.
void bFunction(int n){
for(int i = 0;i < n;i++){
for(int j = 0;j < n;j++){
for(int k = 0;k < n;k++){
}
}
}
}
上述算法时间复杂度为O(n3),3为常数,对应着循环的嵌套层数即次幂为常数;也可以用O(nC)表示,称为多项式阶
7.
void bFunction(int n){
int num = n;
for(int i = 0;i < n;i++){ //O(n)
num *= n;
}
for (int j = 0;j<num;j++){ //O(n)
}
}
上述函数输入的参数n将作为num的次幂,假设循环次数为s, s = 2n,那么时间复杂度为O(2n),
称之为指数阶,可记为O(C^n)
8.
void bFunction(int n)
{
for(int i=0;i<n;i++){
for(int j=0;j<n;j++){
}
}
for(int i=0;i<n;i++){
}
}
对于顺序运行的算法,总时间复杂度等于算法中最大时间复杂度,即O(n^2)
9.
void bFunction(int n)
{
if(n % 2 ==0){
for(int i=0;i<n;i++){
for(int j=0;j<n;j++){
}
}
}else{
for(int i=0;i<n;i++){
}
}
}
对于具备分支结构的算法,总时间复杂度等于算法中时间复杂度最大路径的复杂度,即O(n^2)
时间复杂度大小顺序
常数 < 对数阶 < 线性阶 < 平方阶 < 多项式阶 < 指数阶
O(1) < O(logn) < O(n) < O(n^2) < O(n^C) < O(C^n)
一般情况下,一个算法的时间复杂度是算法输入规模的函数;
通常认为,具有指数阶数量级的算法是实际不可计算的,而量级低于平方阶的算法是高效的;
空间复杂度
确定算法的空间占用量
空间复杂度是对一个算法在执行过程中临时占用存储空间大小的度量;
一个算法在执行期间所需要的存储空间包括以下部分:
- 程序代码本身所占用的空间
- 输出数据所占用的空间
- 辅助变量所占用的空间
** 估算算法空间复杂度时,一般值分析辅助变量所占用的空间;
程序代码占用空间是固定的,通常比较小
输入数据占用空间同样较小
强调:无论是时间复杂度还是空间复杂度都用大O表示,表示方法也是相同的,不需要具体到几次,只需要求出最大的数量级即可,同样忽略系数,即复杂度对于常数的加减乘除是忽略不计的;
案例:
下列是两个算法,用于对数组元素顺序进行逆转操作;
void f1(int a[],int n){
int i,temp;
for(i = 0;i < n/2-1;i++){
temp = a[i];
a[i] = a[n-1-i];
a[n-1-i] = temp;
}
}
上述算法时间复杂度为O(n);空间复杂度为O(1);
解析:该算法员工定义了两个整型的辅助变量,i和temp,辅助变量的个数与输入数据没有关系,是固定的常量C,对于拥有常数阶复杂度的算法,其复杂度用O(1)表示;
void f2(int a[],int n){
int i,b[n];
for(i = 0;i < n;i++){
b[i] = a[n-i-1];
}
for(i = 0;i < n;i++){
a[i] = b[i];
}
}
上述算法时间复杂度为O(n);空间复杂度为O(1);
解析:
对于空间复杂度,其需要的计算量是(n)(2),大O表示为O(n);
对于空间复杂度,同样是两个变量,但是数组b的空间大小是随着输入数据增长的,所以整体占用为 1 + n,大O表示为O(n);
可以发现空间复杂度相比时间复杂度更好度量,因为只需要根据定义的变量数量来计算即可,而时间复杂度会随循环语句而变化;