01-算法的复杂度(斐波那契复杂度分析)

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-------------------------------------
  • 但是用事后分析法或者仅仅用执行时间来决定一个算法的好坏,是不够全面的,事后分析法有其明显的缺陷:
  1. 执行时间严重依赖硬件以及运行时各种不确定的环境因素。
    比如用不同CPU跑同一个算法的话,会得出明显不用的时间结果。也受当前的执行环境是否卡顿影响。
  2. 测试数据的选择难以保证公正性,不同的输入可能得到的结果(哪个效率高)不同。
    可能输入是1 ~ 5时,算法1执行效率高y=10x;
    输入6 ~ +∞时,算法2执行效率高y=2x
  3. 必须编写响应的测算代码。
    得到不同算法的执行时间。

2.2算法的评估维度

  • 一般从以下维度来评估算法的优劣:
  1. 正确性,可读性,健壮性(对不合理输入的反应能力和处理能力)
  2. 时间复杂度(time complexity):估算程序指令的执行次数(执行时间)
    假设以“;”为界限,每个分号代表一个指令,且每个指令的执行时间是一样的。那么用一个算法执行了多少指令,来估算这个算法的执行时间。那么如果我们想要估算这个算法的执行时间,就可以考察一下这个算法执行了多少个指令。
  3. 空间复杂度:估计所需占用的空间
    比如说这个算法需要定义多少变量,需要开辟多少存储空间来解决。
  • 所以一个好的算法,要在符合正确性,可读性,健壮性的基础上,时间复杂度要低(程序的指令执行次数要低),空间复杂度也要低(程序开辟的存储空间也要低)。

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.算法的优化方向

用尽量少的存储空间
用尽量少的执行步骤(执行时间)
根据情况,可以:

  • 空间换时间
  • 时间换空间
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值