算法导论(一):算法分析
算法分析
算法分析是关于计算机程序性能与资源利用的研究。
为什么需要性能
我们在用代码写程序中,有比性能更加重要的东西,例如:
- 正确性;
- 可维护性;
- 健壮性;
- 可扩展性;
- 模块化;
- 安全性;
- 用户友好;
看似这些都比性能更加重要,那为什么我们还要关注性能?
- 通常性能的好坏,决定着一个方案是可行还是不可行。例如,对于实时的需求,如果它程序不快,就代表它不可行,如果它占用过多的内存,也只能说不可行。
- 算法是一种让机器简洁思考的语言,它使实现方式更加的简洁。
总:算法就算是经济中的基础货币,是衡量的标准。例如:人们牺牲了三倍的性能,把原能用C语言写的程序改为用java来写,因为java提供面向对象的特性等好处。
排序问题
我们输入一组序列a1,a2,…直到an,然后把他们按照从小到大的顺序输出
插入排序
定义
插入排序就是找准一个key值,key值前面的都是排过序的,key值后面的都是没有排过序的,将未排序的元素依次与排序元素作比较,按照从小到大或者从大到小的顺序将未排序的元素插入排序元素中,从而组成有序的数组。
自己的思考过程
第一步:错误的代码
一组无序的数组,那么它的第一位肯定是有序的,所以我就假设这个数组的第二位就是这个key值,然后和其他元素一一做比较,得出代码:
public static int[] sort(int[] params){
for(int i=0;i<params.length;i++){
int j = i+1;
while (j<params.length&¶ms[j]<params[i]){
int temp = params[i];
params[i] = params[j];
params[j] = temp;
}
}
return params;
}
然后运行这段代码,控制台打印结果:
原数组为:[100, 97, 20, 77777, 5, 68, 37, 20]
排序后的数组为:[20, 97, 5, 68, 37, 20, 100, 77777]
第二步:无意中发现冒泡排序法
em…(○´・д・)ノ这是什么鬼?好吧,可能写错了,我继续思考一下。。。。发现了一个即为重要的问题,上段代码只是将key值和它的前一个元素(也就是有序数组的最后一位)作比较,这样是不行的,例如在进行第二次排序时,本来的正确结果应该是[20,97,100, 77777, 5, 68, 37, 20],但是由于上段代码只是将key值和它的前一个元素(也就是有序数组的最后一位)作比较,所以20只会和100交换位置,于是就变成了错误数据[97,20,100, 77777, 5, 68, 37, 20]。所以key值应该是和key值前面的所有有序元素作比较。于是得出代码:
public static int[] sort(int[] params){
for(int i=0;i<params.length;i++){
int j = i+1;
for(int a=0;a<=i;a++) {
while (j < params.length && params[j] < params[a]) {
int temp = params[a];
params[a] = params[j];
params[j] = temp;
}
}
}
return params;
}
运行出结果
原数组为:[100, 97, 20, 77777, 5, 68, 37, 20]
排序后的数组为:[5, 20, 20, 37, 68, 97, 100, 77777]
第三步:简化算法
写出程序之后,又对比了一下网上的算法,em…发现网上的代码比我的要少很多,emmmm…又思考了一下,第二步是相邻元素俩俩对比,因此是冒泡排序法。无语,算法好难,下星期再研究吧。
重新思考插入排序,应该是假设前面数组的第一位是有序的,然后从第二位至第n位都是无序的,因此应该是循环应该是从1到n,然后再把每一次的key值与前面的有序数值一一作比较。因此得到代码:
public static int[] sort(int[] params){
for(int i=1;i<params.length;i++){
int j = i;
int key = params[j];
while(j>0 && key<params[j-1]){
int temp = params[j-1];
params[j-1] = params[j];
params[j] = temp;
j--;
}
}
return params;
}
插入排序的运行时间
算法的运行时间的因素
算法的运行时间[^1]取决于多个因素,包括
1.输入本身。例如:输入的元素已经有序,那么排序的运行时间就会非常之短;如果输入的元素是逆序的,那么消耗时间就会长。
2.输入规模。例如:处理6个元素和处理60亿个元素,显然是后者消耗时间更长。通常,我们将其输入的元素参数化,将运行时间看作待排列数据规模的函数。
[^1]:运行时间一般会有一个上界,即这个程序最多再多少时间内运行,它代表了对用户的承诺。
输入时最坏情况的分析
定义
T(n)定义为输入规模为n时的最长运行时间;
T(n)也被称为期望时间,即每种输入的运行时间,乘以那种输入出现的频率。
最坏情况
最坏情况就是逆向排序,即最大值在第一个,最小值在最后一个,此时每次做插入操作都要把所有项替换一遍。我们可以假设每种原子操作都耗费常数时间,
T(n) = ∑ j = 2 n Θ ( j ) \sum_{j=2}^nΘ(j) ∑j=2nΘ(j) = Θ(n2)
这个 Θ(n2) 相当于1+2+3+…一直加到n
那么,相对于很小的n, Θ(n2) 是很小的,而相对于很大的n, Θ(n2) 则是很大的。
输入时最好情况的分析
最好情况就是只输入有序的数组一种情况,这种情况我们可以忽略。
算法的大局观
渐进分析:
1.忽略依赖于机器的常量;
2.不是去检查实际的运行时间,而是关注运行时间的增长。
渐进符号
Θ:写个公式,弃去它的低阶项,并忽略前面的常数因子。
例如,公式 3n3+90n2-5n+6046 = Θ(n3)
分析:现在有一种算法的时间T(n) = Θ(n3),而另一种算法的时间是T(n) = Θ(n3),假设他们的函数图是这样的
从上图我们可以看出总有一个n0能使得 Θ(n3)的算法运行时间小于 Θ(n2)的算法运行时间,不管一开始你给 Θ(n2)这种算法多大的计算机优势都没用,但是从工程视角来看,如果这个n0过大,大到计算机都无法计算它时,我们仍旧会采用 Θ(n2)这种算法。
注:具体的渐进符号以及解法知识将在 有趣又简单的算法导论(二):渐进符号,递归及解法中展示
归并排序
定义
第一步:如果n=1,那么排序结束
第二步:如果n!=1,那么我们做的就是递归的对a1到an/2向上取整这部分排序以及an/2向上取整到an这部分排序。
第三步:然后,我们将排好序的俩个表归并。
子程序的归并时间
首先我们研究子程序的归并时间
例如:现在有俩个排好序的表
5 6 7 8 9
0 1 2 3 4
首先将5和0对比,得出0小,因此将0写在首位,再将5和1对比,得出1小,因此将1写在第二位…以此类推,得出时间应该是 Θ(n),
总时间
T ( n ) = { Θ ( 1 ) x=1 2 T ( n / 2 ) + Θ ( n ) x>1 T(n)= \begin{cases}Θ(1) & \text{x=1}\\2T(n/2)+Θ(n) & \text{x>1} \end{cases} T(n)={Θ(1)2T(n/2)+Θ(n)x=1x>1