算法分析
研究算法的目的为如何花更少的时间,占用更少的内存去完成相同的需求.我们并不能将时间和孔家量化,故有了算法的时间复杂度分析和算法的空间复杂度分析.
算法的时间复杂度分析
事后分析估算方法
在程序执行前定义一个开始的时间戳start,程序结束末尾定义一个结束的时间戳end.在程序结束是计算end-start即为程序运行所需的时间
//求解计算1到100之间整数的和
package Demo;
public class DateTimeDemo {
public static void main(String[] args) {
long start = System.currentTimeMillis();
int sum = 0;
int n = 100;
for (int i = 0; i <= n; i++) {
sum += i;
}
System.out.println("sum = " + sum);
long end = System.currentTimeMillis();
System.out.println(end - start);
}
}
事后分析估算方法必须依据算法实现编制好的测试程序,需要花费大量的时间和精力,如果测试完成后发现结果非常糟糕,那就较为耗时耗力且达不到较为好的效果.
事前分析估算法
在计算机程序编写前,依据统计方法对算法进行估算,我们会发现程序所小号的时间取决于以下因素:
算法才用的策略和方法
编译所产生的代码质量
问题的输入规模,即为解决问题所需要的问题输入规模
机器执行指令的速度
除了硬件,软件等有关因素,程序运行时间较依赖于算法的好坏和问题输入的规模.
当我们用两种不同的算法再次解决计算1到100之间整数和的问题
//方案一
package Demo;
public class Doubleway {
public static void main(String[] args) {
int sum = 0;//执行一次
int n = 100;//执行一次
for (int i = 0; i <= n; i++) {
sum += n;//核心操作执行n次
}
System.out.println("sum = " + sum);
}
}
//方案二
package Demo;
public class DoublewayDemo2 {
public static void main(String[] args) {
int sum = 0;//执行一次
int n = 100;//执行一次
sum = (n + 1) * n / 2;//执行一次
System.out.println(sum);
}
}
因此,当输入规模为n时,第一种算法执行了1+1+(n+1)+n=2n+3次;第二种算法执行了1+1+1=3次。如果我们把 第一种算法的循环体看做是一个整体,忽略结束条件的判断,那么其实这两个算法运行时间的差距就是n和1的差距
函数渐进增长
给定两个函数f(n)和g(n),如果存在一个整数N,是的对于所有的n>N,f(n)总是比g(n)大,那么我们说f(n)的增长渐进快于g(n).
我们做几个测试来了解这个概念
测试一
观察数据得出当输入规模n>2时,算法A1的渐近增长小于算法B1 的渐近增长
可以得出结论:随着输入规模的增大,算法的常数操作可以忽略不计
测试二
观察数据得出随着输入规模的增大,即使去除n^2前面的常数因子,D系列的次数要远远高于C系列。
可以得出结论:随着输入规模的增大,与最高次项相乘的常数可以忽略
测试三
通过折线图我们会看到,算法F系列随着n的增长会变得特块,算法E系列随着n的增长相比较算法F来说,变得比较慢
可以得出结论:最高次项的指数大的,随着n的增长,结果也会变得增长特别快
测试四
得出结论: 算法函数中n最高次幂越小,算法效率越高
总而言之:
在我们比较算法随着输入规模的增长时,可以有以下规则:
1.算法函数中的常数可以忽略;
2.算法函数中最高幂的常数因子可以忽略;
3.算法函数中最高幂越小,算法效率越高.
大O记法
定义:在进行算法分析时,语句总的执行次数T(n)是关于问题规模n的函数,记作:T(n)=O(f(n)).
用大写O()来体现算法时间复杂度的记法,我们称之为大O记法。一般情况下,随着输入规模n的增大,T(n)增长最 慢的算法为最优算法.
执行次数=执行时间
我们用大O表示法来表示一些求和算法的时间复杂度:
算法一:
public static void main(String[] args) {
int sum = 0;//执行1次
int n=100;//执行1次
sum = (n+1)*n/2;//执行1次
System.out.println("sum="+sum);
}
输出3次
大O记法:O(1)
算法二:
public static void main(String[] args) {
int sum = 0;//执行1次
int n=100;//执行1次
for (int i = 1; i <= n; i++) {
sum += i;//执行了n次
}
System.out.println("sum=" + sum);
}
输出n+3次
大O记法:O(n)
算法三:
public static void main(String[] args) {
int sum=0;//执行1次
int n=100;//执行1次
for (int i = 1; i <=n ; i++) {
for (int j = 1; j <=n ; j++) {
sum+=i;//执行n^2次
}
}
System.out.println("sum="+sum);
}
输出n^2+2次
大O记法:O(n^2+2)
大O记法表示算法的时间复杂度通常有一下规则:
1.用常数1取代运行时间中的所有加法常数;
2.在修改后的运行次数中,只保留高阶项;
3.如果最高阶项存在,且常数因子不为1,则去除与这个项相乘的常数;
常见的大O阶
1.线性阶O(n)
public static void main(String[] args) {
int sum = 0;
int n=100;
for (int i = 1; i <= n; i++) {
sum += i;
}
System.out.println("sum=" + sum);
}
2.平方阶O(n^2)
一般循环嵌套属于这种时间复杂度.
public static void main(String[] args) {
int sum=0,n=100;
for (int i = 1; i <=n ; i++) {
for (int j = 1; j <=n ; j++) {
sum+=i;
}
}
System.out.println(sum);
}
3.立方阶O(n^3)
一般三层循环嵌套属于这种时间复杂度
public static void main(String[] args) {
int x=0,n=100;
for (int i = 1; i <=n ; i++) {
for (int j = i; j <=n ; j++) {
for (int j = i; j <=n ; j++) {
x++;
}
}
}
System.out.println(x);
}
4.对数阶O(logn)
对数阶随着输入规模n的增大,不管底数是多少,增长趋势都是一样的,所以对于对数阶我们一般都忽略底数.
int i=1,n=100;
while(i<n){
i = i*2;
}
5.常数阶O(1)
不管n是多少,最终执行的次数都为常熟次,可以用1来代替.
public static void main(String[] args) {
int n=100;
int i=n+2;
System.out.println(i);
}
描述 | 增长的数量级 | 说明 | 举例 |
---|---|---|---|
常数级别 | 1 | 普通语句 | 将两个数相加 |
对数级别 | logN | 二分策略 | 二分查找 |
线性级别 | N | 循环 | 找出最大元素 |
线型对数级别 | NlogN | 分治思想 | 归并排序 |
平方级别 | N^2 | 双层循环 | 检查所有元素对 |
立方级别 | N^3 | 三层循环 | 检查所有三元组 |
指数级别 | 2^N | 穷举查找 | 检查所有子集 |
O(1)<O(logn)<O(n)<O(nlogn)<O(n2)<O(n3)
在函数调用过程中对于时间复杂度的分析
案例一
public static void main(String[] args) {
int n=100;
for (int i = 0; i < n; i++) {
show(i);
}
}
private static void show(int i) {
System.out.println(i);
}
main方法中有for循环,for循环又调用了show方法.由于show方法中只有一行代码,所以show方法的时间复杂度为O(1),main方法的时间复杂度为O(n).
案例二
public static void main(String[] args) {
int n=100;
for (int i = 0; i < n; i++) {
show(i);
}
}
private static void show(int i) {
for (int j = 0; j < i; i++) {
System.out.println(i);
}
}
在main方法中,有一个for循环,循环体调用了show方法,由于show方法内部也有一个for循环,所以show方法 的时间复杂度为O(n),那main方法的时间复杂度为O(n^2).
案例三
public static void main(String[] args) {
int n=100;
show(n);
for (int i = 0; i < n; i++) {
show(i);
}
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
System.out.println(j);
}
}
}
private static void show(int i) {
for (int j = 0; j < i; j++) {
System.out.println(i);
}
}
在show方法中,有一个for循环,所以show方法的时间复杂度为O(n),在main方法中,show(n)这行代码内部执行 的次数为n,第一个for循环内调用了show方法,所以其执行次数为n^2,第二个嵌套for循环内只执行了一行代码, 所以其执行次数为n2,那么main方法总执行次数为n+n2+n2=2n2+n。根据大O推导规则,去掉n保留最高阶 项,并去掉最高阶项的常数因子2,所以最终main方法的时间复杂度为O(n^2)
最坏情况
有一个存储了n个随机数字的数组,请从中查找出指定的数字。
public int search(int num){
int[] arr={11,10,8,9,7,22,23,0};
for (int i = 0; i < arr.length; i++) {
if (num==arr[i]){
return i;
}
}
return -1;
}
最好情况: 查找的第一个数字就是期望的数字,那么算法的时间复杂度为O(1)
最坏情况: 查找的最后一个数字,才是期望的数字,那么算法的时间复杂度为O(n)
平均情况: 任何数字查找的平均成本是O(n/2) 最坏情况是一种保证,在应用中,这是一种最基本的保障,即使在最坏情况下,也能够正常提供服务,所以,除非 特别指定,我们提到的运行时间都指的是最坏情况下的运行时间。