时间复杂度与空间复杂度
前言
一个算法的优劣主要从算法的执行时间和所需要占用的存储空间两个方面衡量,即时间复杂度和空间复杂度。 但是大多数情况下两者不能达到权衡的,常常利用空间来换取时间如hash表就是个经典的例子,牺牲空间来加快查找的效率。
一、算法的概念
算法简而言之就是用于解决特定问题的一系列的执行步骤,例如下面的两个例子:
//1计算两个整数之和
public static int sum(int a, int b){
return a + b;
}
//2计算1+2+3+...+n之和
public static int total(int n){
int result = 0;
for (int i = 0; i < n; i++) {
result += i;
}
return result;
}
1.1 斐波那契数列
1.1.1 同一个问题使用不同算法,效率可能相差非常大,比如:斐波那契数列求n项和。所谓斐波那契数列就是前面两数相加等于后面这位数例如:0 1 1 2 3 5 8 …
1.2 递归算法
/**
* 递归算法求斐波那契数列n项和
* @param n
* @return
*/
public static int fib(int n){
if (n <= 1){
return n;
}
return fib(n - 1) + fib(n - 2);
}
当入参n为64时,程序直接阻塞。控制台也无输出结果。
1.3 非递归算法
//非递归算法求斐波那契数列n项和
public static int NoFib(int n){
if (n <= 1){
return n;
}
int first = 0;
int second = 1;
for (int i = 0; i < n - 1; i++) {
int sum = first + second;
first = second;
second = sum;
}
return second;
}
非递归方法能够快速计算出第64项的和,如下图:
1.4 算法的优劣
一般从以下维度来评估算法的优劣:
- 正确性、可读性、健壮性(对不合理输入的反应能力和处理能力)
- 时间复杂度:估算程序指令的执行次数(执行时间)
- 空间复杂度:估算所需占用的存储空间
二、时间复杂度
2.1 大O表示法
一般用大O表示法来描述复杂度,它表示的是数据规模 n 对应的复杂度。
常数时间的操作:一个操作如果和数据量没有关系,每次都是固定时间内完成的操作,叫做常数操作。
时间复杂度为一个算法流程中,常数操作数量的指标。常用O来表示。具体来说,在常数操作数量的表达式中,只要高阶项,不要低阶项,也不要高阶项的系数,剩下的部分如果记为f(N),那么时间复杂度为O(f(N))。评价一个算法流程的好坏,先看时间复杂度的指标,然后再分析不同数据样本下的实际运行时间,也就是常数项时间。
2.2 常见的复杂度
三、空间复杂度
额外空间复杂度:在执行代码过程中申请的额外存储空间,比如变量,有限个变量则O(1),分析额外空间复杂度和时间复杂度类似;如使用冒泡排序对一个数组排序,期间只需要一个临时变量temp,那么该算法的额外空间复杂度为O(1)。又如归并排序,在排序过程中需要创建一个与样本数组相同大小的辅助数组,尽管在排序过后该数组被销毁,但该算法的额外空间复杂度为O(n)。
3.1 思考题
3.1.1 一个有序数组A,另一个无序数组B,请打印B中的所有不在A中的数,A数组长度为N,B数组长度为M。
思路1(遍历查找):对于数组B中的每一个数,都在A中通过遍历的方式找一下;时间复杂度分析:遍历B[0,M-1],遍历A[0,N-1],故O(M*N)
public class Test05 {
public static void main(String[] args) {
int[] arrayA = new int[]{1,2,5,6,7,8};
int[] arrayB = new int[]{11,14,5,3,6,7};
ergodic(arrayA,arrayB);
}
public static void ergodic(int[] a,int[] b){
for (int i = 0; i < b.length - 1; i++){
boolean flag = false;
for ( int j = 0; j < a.length - 1; j++){
if (b[i] == a[j]){
flag = true;
break;
}
}
if (!flag) {
System.out.print(b[i]+" ");
}
}
}
}
测试结果:
思路2(二分查找):对于数组B中的每一个数,都在A中通过二分的方式找一下。时间复杂度分析:遍历B[0,M-1],二分查找时间复杂度log2(N),故O(M*log2(N))
思路3(类似外排):先把数组B排序,然后用类似外排的方式打印所有在A中出现的数;先看第一步,排序,数组B使用二分排序,时间复杂度为O(Mlog2(M)),再看第二步,数组B排序完成之后,数组A和B就都是有序的了,我们假设一个a指针,指向数组A第一个元素,再假设一个数组b指向数组B第一个元素,然后两个指针想对应的数字进行比较,然后根据比较 大小,进行指针之间的移动。a指针移动的条件是,a指针指向的数字<b指针指向的数字时,向右移动一位。b指针移动的条件是,b指针指向的数字<=a指针指针指向的数字,如果=只向右移动,不打印,如果<,打印数字,且移动循环往复以上步骤即可,当指针a和指针b任意一个指针走到尽头,就意味着程序的中止,我们假设最坏情况,同时到最后才结束,那么他的时间复杂度为0(N+M)再加上之前排序的时间复杂度,最终复杂度为O(Mlog2(M))+O(N+M)
3.2 斐波那契数列复杂度分析
3.2.1 斐波那契数列 - 递归
public static int fib(int n){
if (n <= 1){
return n;
}
return fib(n - 1) + fib(n - 2);
}
复杂度分析:
3.2.1 斐波那契数列 - 非递归
// O(n)不开辟任何空间,只使用循环完成
public static int NoFib(int n){
if (n <= 1){
return n;
}
int first = 0;
int second = 1;
for (int i = 0; i < n - 1; i++) {
int sum = first + second;
first = second;
second = sum;
}
return second;
}
3.3 算法的优化方向
- 用尽量少的存储空间
- 用尽量少的执行步骤(执行时间)
- 根据情况:空间换时间、时间换空间