目录
1.复杂度
1.什么是算法(Algorithm)?
算法是用于解决特定问题的一系列的执行步骤,是解决问题的一系列清晰指令。能够对一定规范的输入,在有限时间内获得正确的输出。
2.如何评判一个算法的好坏?
2.1 事后统计法:
事后统计法:单纯从执行效率(没有考虑空间等)上考虑,比较一个问题的不同算法对同样的输入的执行处理时间。即先执行,再看效果,所以称为事后统计法。
-
比如求斐波那契数列的第n个值(从第0个开始),输入44,通过统计执行时间(事后统计法)来比较一下两个算法的好坏
发现不同的算法在相同的输入下,在执行效率上可能有着巨大的差异。
//算法1
public static int fib1(int n) {
if( n <= 1) return n;
return fib1(n -1) + fib1(n - 2);
}
//算法2
public static int fib2(int n) {
if( n<= 1) return n;
int first = 0;
int second = 1;
int tmp= 0;
for(int i=0; i<n-1; i++){
tmp = first + second;
first = second;
second = tmp;
}
return second;
}
//执行结果
【fib1(44)】
开始:21:49:40.620
701408733
结束:21:49:44.437
耗时:3.816秒
-------------------------------------
【fib2(44)】
开始:21:49:44.444
701408733
结束:21:49:44.444
耗时:0.0秒
-------------------------------------
- 但是用事后分析法或者仅仅用执行时间来决定一个算法的好坏,是不够全面的,事后分析法有其明显的缺陷:
- 执行时间严重依赖硬件以及运行时各种不确定的环境因素。
比如用不同CPU跑同一个算法的话,会得出明显不用的时间结果。也受当前的执行环境是否卡顿影响。 - 测试数据的选择难以保证公正性,不同的输入可能得到的结果(哪个效率高)不同。
可能输入是1 ~ 5时,算法1执行效率高y=10x;
输入6 ~ +∞时,算法2执行效率高y=2x - 必须编写响应的测算代码。
得到不同算法的执行时间。
2.2算法的评估维度
- 一般从以下维度来评估算法的优劣:
- 正确性,可读性,健壮性(对不合理输入的反应能力和处理能力)
时间复杂度(time complexity)
:估算程序指令的执行次数(执行时间)
假设以“;
”为界限,每个分号代表一个指令,且每个指令的执行时间是一样的。那么用一个算法执行了多少指令,来估算这个算法的执行时间。那么如果我们想要估算这个算法的执行时间,就可以考察一下这个算法执行了多少个指令。- 空间复杂度:估计所需占用的空间
比如说这个算法需要定义多少变量,需要开辟多少存储空间来解决。
- 所以一个好的算法,要在符合正确性,可读性,健壮性的基础上,时间复杂度要低(程序的指令执行次数要低),空间复杂度也要低(程序开辟的存储空间也要低)。
2.3 时间复杂度
1.时间复杂度:估算程序指令的执行次数(执行时间),假设每个指令的执行时间是一样的。那么用一个算法执行了多少指令,来估算这个算法的执行时间。
- 注意:这里估算程序指令的执行次数,只是估算。因为代码中的一个语句肯定不是一个汇编指令,一般一个代码语句会转成多个汇编指令传递到CPU执行。此处只是估算,而且是估算
规模
。
2.估算一下下面这些程序的时间复杂度(指令执行次数)
// test1:14次
public static void test1(int n) {
// 1
if (n > 10) {
System.out.println("n > 10");
} else if (n > 5) { // 2
System.out.println("n > 5");
} else {
System.out.println("n <= 5");
}
// 1 + 4 + 4 + 4
for (int i = 0; i < 4; i++) {
System.out.println("test");
}
}
//test2:1+3n
public static void test2(int n) {
// O(n)
// 1 + n + n + n
for (int i = 0; i < n; i++) {
System.out.println("test");
}
}
//test3:3n^2 + 3n + 1
public static void test3(int n) {
// 1 + n + n + n * (1 + 3n)
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
System.out.println("test");
}
}
}
//test4:1+n+n+n*(1+15+15+15) = 48n+1
public static void test4(int n) {
// 1 + 2n + n * (1 + 45)
// 1 + 2n + 46n
// 48n + 1
// O(n)
for (int i = 0; i < n; i++) {
for (int j = 0; j < 15; j++) {
System.out.println("test");
}
}
}
//test5:log2(n)
public static void test5(int n) {
// 8 = 2^3
// 16 = 2^4
// 3 = log2(8)
// 4 = log2(16)
// 执行次数 = log2(n)
// O(logn)
while ((n = n / 2) > 0) {
System.out.println("test");
}
}
3.注意下面这些程序的时间复杂度估算(指令执行次数)
//test5:while循环的条件执行一次,循环体就执行一次,主要看循环体即可:log2(n)
public static void test5(int n) {
//n能除多少次2,就能执行多少次,或者说看2的多少次幂大于等于n
//8 = 2^3:所以8能除以2,除3次,3 = log2(8)
//16 = 2^4:16能除以2,除4次,4 = log2(16)
//所以执行次数 = log2(n)
while ((n = n / 2) > 0) {
System.out.println("test");
}
}
//test6:log5(n)
public static void test6(int n) {
while ((n = n / 5) > 0) {
System.out.println("test");
}
}
//test7:1+ 3log2(n) + 3nlog2(n)
public static void test7(int n) {
//i从1开始,每次都乘以2,乘以2多少次后会超过n:1*2^x=n
//所以n=2^x,x=log2(n)次
//1+log2(n)+log2(n)+log2(n)*(1+3n)
//1+ 3log2(n) + 3nlog2(n)
for (int i = 1; i < n; i = i * 2) {
// 1+n+n+n
for (int j = 0; j < n; j++) {
System.out.println("test");
}
}
}
//test10:2n+54
public static void test10(int n) {
// 1+1+1+1+1+n+n(1)=2n+4
int a = 10;
int b = 20;
int c = a + b;
int[] array = new int[n];
for (int i = 0; i < array.length; i++) {
System.out.println(array[i] + c);
}
}
3.大O表示法(Big O)
一般用大O表示法来描述复杂度,它表示的是数据规模n
对应的复杂度:增长趋势
- 好处:大O表示法仅仅是一种粗略的分析模型,是一种估算,能帮助我们短时间内了解一个算法的效率。这样就不用像上面的比较fib算法那样,专门写一个时间工具来计算执行时间,直接分析代码即可得出大概的算法效率。
既然是看n的数据规模,那么就是看n的数量级
,所以常数,系数,低阶项都可以忽略。
- 因为在n–>+∞时,这些部分占比极小。
对数阶的细节:对数阶一般省略底数:
- 因为对数中有个公式:log2n = log29 * log9n 。等号右边的log29是一个常数,可以省略。所以在看待
n的数据规模
时,log2n,log9n是等价的,那么log2n,log9n统称为logn。
1.用大O表示法表示上面程序的时间复杂度:
test1:14次 >> O(1)
test2:1+3n >> O(n)
test3:3n^2 + 3n + 1 >> O(n^2)
test4:48n+1 >> O(n)
test5:log2(n) >> O(logn)
test6:log5(n) >> O(logn)
test7:1+ 3log2(n) + 3nlog2(n) >> O(nlogn)
test10:2n+54 >> O(n)
2.常见的复杂度
1.常见复杂度比较:
3.估算一下上面程序的空间复杂度
//test1:空间复杂度:O(1)
public static void test1(int n) {
if (n > 10) {
System.out.println("n > 10");
} else if (n > 5) { // 2
System.out.println("n > 5");
} else {
System.out.println("n <= 5");
}
// 只开辟了一个i变量
for (int i = 0; i < 4; i++) {
System.out.println("test");
}
}
//test2:空间复杂度:O(1)
public static void test2(int n) {
// 只开辟了一个i变量
for (int i = 0; i < n; i++) {
System.out.println("test");
}
}
//test3:空间复杂度:O(1)
public static void test3(int n) {
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
System.out.println("test");
}
}
}
//test4:空间复杂度:O(1)
public static void test4(int n) {
for (int i = 0; i < n; i++) {
for (int j = 0; j < 15; j++) {
System.out.println("test");
}
}
}
//test5:空间复杂度:O(1)
public static void test5(int n) {
while ((n = n / 2) > 0) {
System.out.println("test");
}
}
//test6:空间复杂度:O(1)
public static void test6(int n) {
while ((n = n / 5) > 0) {
System.out.println("test");
}
}
//test7:空间复杂度:O(1)
public static void test7(int n) {
for (int i = 1; i < n; i = i * 2) {
// 1 + 3n
for (int j = 0; j < n; j++) {
System.out.println("test");
}
}
}
//test10:空间复杂度O(n)
public static void test10(int n) {
//1+1+1+O(n)+1
int a = 10;
int b = 20;
int c = a + b;
int[] array = new int[n];//这句和n有关
for (int i = 0; i < array.length; i++) {
System.out.println(array[i] + c);
}
}
}
- 一般只要看循环体内的语句即可,循环条件:for (int i = 0; i < array.length; i++)可以不看,因为循环条件执行一次,循环语句一会执行一次,而最后的O复杂度会省略系数。所以只要关注循环体内的语句,即可分析出算法的复杂度。
- 一般我们更关注的是时间复杂度,因为硬件上的内存现在都比较大了,而且空间是可以用钱解决的。
4.多个数据规模的时间复杂度
注意多个变量,多个数据规模那么都不能省略,上例的时间复杂度:O(n + k)。
2.斐波那契数列复杂度分析
我们写了两个实现斐波那契数列的算法,下面分析一下这两个算法的复杂度
1.斐波那契数列-递归
//时间复杂度:O(2^n)
public static int fib1(int n) {
if( n <= 1) return n;
return fib1(n -1) + fib1(n - 2);
}
单独分析一下递归算法的时间复杂度:
- 估算fib1方法程序指令执行的次数,那么要看fib1方法被调用了多少次。每次fib1方法被调用,都会执行一句语句。最后分析得出递归算法的时间复杂度O(2n)
2.斐波那契数列-循环
//时间复杂度:O(n)
public static int fib2(int n) {
if (n <= 1) return n;
int first = 0;
int second = 1;
int sum = 0;
for (int i = 0; i < n-1; i++) {
sum = first + second;
first = second;
second = sum;
}
return sum;
}
/**
* 简化一些变量
* @param n
* @return
*/
public static int fib3(int n) {
if (n <= 1) return n;
int first = 0;
int second = 1;
while(n-- > 1) {
second = first + second;
first = second -first;
}
return second;
}
3.斐波那契的线性代数解法-特征方程
- 复杂度可以认为是O(1)
4.fib方法的时间复杂度分析
fib1递归算法的时间复杂度是O(2n),fib2循环算法的时间复杂度是O(n),它们的差别有多大?
- 如果有一台1GHz的普通计算机,运算速度109次每秒,当数据规模n=64时。
- O(n)大约耗时6.4 * 10 -8秒
- O(2n)大约耗时584.95年
所以有时候算法之间的差距,往往比硬件方面的差距还要大。
5.算法的优化方向
用尽量少的存储空间
用尽量少的执行步骤(执行时间)
根据情况,可以:
- 空间换时间
- 时间换空间