算法的基本概念
什么是算法?
程序=数据结构+算法,数据结构研究如何把现实世界的问题信息化,将信息存进计算机。同时还要实现对数据结构的基本操作。算法研究如何处理这些信息,解决实际问题
算法的五个特性
- 有穷性。一个算法必须总在执行有穷步之后结束,且每一步都可在有穷时间内完成
- 确定性。算法中每条指令必须有确切的含义,对于相同的输入只能得出相同的输出
- 可行性。算法中描述的操作都可以通过已经实现的基本运算执行有限次来实现
- 输入。一个算法有0个或多个输入,这些输入取自于某个特定的对象的集合
- 输出。一个算法有一个或多个输出,这些输出是与输入有着某种特定关系的量
算法必须是有穷的,要求用有限的步骤解决某个特定的问题。而程序可以是无穷的
“好”算法的特性
- 正确性。算法应能够正确地解决求解问题
- 可读性。算法应具有良好的可读性,以帮助人们理解
- 健壮性。输入非法数据时,算法能适当地做出反应或进行处理,而不会莫名其妙的输出结果
- 高效率(执行速度快,时间复杂度低)与低存储量需求(不费内存,空间复杂度低)
算法可以用伪代码描述,甚至用文字描述,重要的是要“无歧义”地描述出解决问题的步骤
算法的时间复杂度
如何评估算法时间开销?
让算法先运行,事后统计运行时间的方法是不科学的。因为存在如下问题
- 和机器性能有关,如超级计算机和单片机
- 和编程语言有关,越高级的语言执行效率越低
- 和编译程序产生的机器指令质量有关
- 有些算法是不能事后再统计的,如:导弹发射算法
我们希望评估算法时能排除与算法本身无关的外界因素,且能事先估计,所以有了时间复杂度,以事前预估算法时间开销T(n)与问题规模n的关系
时间复杂度的计算
- 时间复杂度的计算满足乘法规则和加法规则
多项相加,只保留最高阶的项且系数变为1
乘法规则:
多项相乘,都保留
表达式只保留阶数高的部分,如
只关心数量级,用大O表示“同阶”,同等数量级。即:当n→∞ 时,T(n) / f(n)的极限值为不等于零的常数,则称 f(n) 是 T(n) 的同数量级函数。就做T(n) = O(f(n));称为 O(f(n)) 为算法的渐进时间复杂度(O是数量级的符号),简称时间复杂度; T(n) = 2n^3 + 3n^2 + 2n + 1则 f(n) = n^3,可知 T(n) 与 n^3 是同阶的,T(n)可记作:T(n) = O(n^3)
所以常数项系数可以省略,同时时间复杂度的计算结果用O ( . . . ) 的方式表示
可以结合洛必达法则和函数图像来证明,用“常对幂指阶”来记忆
算法案例
void loveYou(){//n为问题规模
① int i=1;
② while(i<=n){
③ i++;
④ printf("I Love You %d\n",i);
}
⑤ pritnf("I Love You Than %d\n",n);
}
语句频度:
① ——1次
② ——3001次
③④——3000次
⑤ ——1次
T(3000)=1+3001+2*300+1
时间开销与问题规模n的关系:T(n)=3n+3=O(n)
void loveYou(){//n为问题规模
int i=1;
while(i<=n){
i++;
printf("I Love You %d\n",i);
for(int j=1;j<n;j++){
printf("I am Iron Man\n");
}
}
pritnf("I Love You Than %d\n",n);
}
外层循环执行n次,内层循环共执行n^2次。 时间开销与问题规模n的关系:
void loveYou(){//n为问题规模
int i=1;
while(i<=n){
i=i*2;//每次翻倍
printf("I Love You %d\n",i);
}
}
pritnf("I Love You Than %d\n",n);
}
解释:设循环次数为x,第一次i=2,第二次i=4,第x次i=2^x,循环结束时2^x > n
//算法4—— 搜索数字型爱你
void loveYou(int flag[],int n){ //flag数组中乱序存放了1~n这些数字,n为问题规模
printf("I Am Iron Man\n");
for(int i=0;i<n;i++){//从第一个元素开始查找
if(flag[i]==n){//找到元素n
printf("I Love You %d\n",n);
break;//找到后立即跳出循环
}
}
}
该算法最好时间复杂度 T(n) = O(1)
该算法最坏时间复杂度 T(n) = O(n)
平均情况,假设元素n在任意一个位置的概率相同,为1/n ,T(n) = (1+2+3+...+n)(1/n) = (1+n)/2 =O(n)
- 顺序执行的代码只会影响最终结果的表达式的常数项,可以忽略。
- 考虑循环语句时,只需挑循环中的一个基本操作分析它的执行次数与n的关系即可
- 如果有多层嵌套循环,只需关注最深层循环的循环次数与n的关系
- 很多算法执行时间与输入的数据有关。这种时候就要考虑最好时间复杂度和最坏时间复杂度,平均时间复杂度
最坏时间复杂度:最坏情况下算法的时间复杂度
平均时间复杂度:所有输入示例等概率出现的情况下,算法的期望运行时间
最好时间复杂度:最好情况下算法的时间复杂度
算法的空间复杂度
空间复杂度的计算
基本上可以类比时间复杂度的计算
现在要运行一个程序,程序运行前会先把程序代码(这里的代码是源代码编译后生成的机器指令)放到内存中(大小固定,与问题规模无关)。接下来CPU会一行行的执行这些代码,内存中开辟空间存放局部变量和参数,数组和其他信息。
- 如果无论问题规模怎么变,算法运行所需的内存空间都是固定的常量,算法空间复杂度为S ( n ) = O ( 1 ) S(n)=O(1)S(n)=O(1)。此时可以说算法原地工作。
void test(int n){
int flag[n]; //声明一个长度为n的数组
int i;
//....此处省略无关代码
}
- 只需关注存储空间大小与问题规模相关的变量。表达式中的常数项可省略
void test(int n){
int flag[n][n]; //声明一个长度为n的数组
int i;
//....此处省略无关代码
}
- 由于只关心数量级,计算时没必要考虑不同类型变量所占用的内存空间大小上的差异
- 同样遵循加法和乘法原则,上一节用于比较数量级的不等式也适用于空间复杂度的计算
函数递归调用带来的内存开销
递归过程中每加深一层的调用都需要把这一层的局部变量,参数等在内存中开辟一块新的空间用于存储
//算法5—— 递归型爱你
void loveYou(int n){ //n为问题规模
int a,b,c; //声明一系列局部变量
//...省略无关代码
if(n > 1){
loveYou(n-1);
}
printf("I Love You %d\n",n);
}
- 绝大多数情况下空间复杂度=递归调用的深度,有些情况则要具体分析。