一.复杂度定义与分类
复杂度:衡量算法效率的标准.
时间效率:衡量这个算法的运行速度,也就是我们常说的时间复杂度.
空间效率:衡量这个算法所需要的额外空间,也就是我们常说的的空间复杂度.
二.时间复杂度
1.概念
时间复杂度的定义:在计算机科学中,算法的时间复杂度是一个函数,它定量描述了该算法的运行时间。一个 算法执行所耗费的时间,从理论上说,是不能算出来的,只有你把你的程序放在机器上跑起来,才能知道。但 是我们需要每个算法都上机测试吗?是可以都上机测试,但是这很麻烦,所以才有了时间复杂度这个分析方 式。一个算法所花费的时间与其中语句的执行次数成正比例,算法中的基本操作的执行次数,为算法的时间复杂度。
2. 大O的渐进表示法
一般我们用大O的渐进法来表示时间复杂度,并给出一个问题的规模,这个规模我们统称为N
下面来看一个例子:
void func1(int N) { int count = 0; for (int i = 0; i < N; i++) { for (int j = 0; j < N; j++) { count++; } } for (int k = 0; k < 2 * N; k++) { count++; } int M = 10; while ((M--) > 0) { count++; } System.out.println(count); }
分析:第一个for循环count的执行次数为N*N=N^2
第二个for循环count的执行次数为2*N=2N
第三个for循环count的执行次数为10
那么整个func1执行的次数为N^2+2N+10,原因是三个for循环是并列的,所以是相加的,如果嵌套的话就是相乘。
实际中我们计算时间复杂度时,我们其实并不一定要计算精确的执行次数,而只需要大概执行次数,那么这里我们使用大O的渐进表示法。
大O阶的推导方法(三部曲)
1、用常数1取代运行时间中的所有加法常数。
N^2+2N+10====》N^2+2N+1(拿1代替了10)
2、在修改后的运行次数函数中,只保留最高阶项。
N^2+2N+1====》N^2(去掉了2N+1)
3、如果最高阶项存在且不是1,则去除与这个项目相乘的常数。
N^2=====》N^2(去掉1)
得到的结果就是大O阶。 使用大O的渐进表示法以后,func1的时间复杂度为:
3.时间复杂度小练习
代码1
void func2(int N) { int count = 0; for (int k = 0; k < 2 * N; k++) { count++; } int M = 10; while ((M--) > 0) { count++; } System.out.println(count); }
执行总次数为2N+10,经过大O推导方法三部曲后,最终剩下N,所以时间复杂度为O(N)
代码2
void func3(int N, int M) { int count = 0; for (int k = 0; k < M; k++) { count++; } for (int k = 0; k < N; k++) { count++; } System.out.println(count); }
执行总次数为M+N,经过大O推导方法三部曲后,最终剩下M+N,所以时间复杂度为O(M+N).
代码3
void func4(int N) { int count = 0; for (int k = 0; k < 100; k++) { count++; } System.out.println(count); }
执行总次数为100,经过大O推导方法三部曲后,最终剩下1,所以时间复杂度为O(1).
代码4 冒泡查询(时间复杂度的三种情况)
void bubbleSort(int[] array) { for (int end = array.length; end > 0; end--) { boolean sorted = true; for (int i = 1; i < end; i++) { if (array[i - 1] > array[i]) { Swap(array, i - 1, i); sorted = false; } } if (sorted == true) { break; } } }
在计算冒泡排序的时间复杂度之前我们来看一个问题:
假设此时有一组数据为1,2,3,4,5,6,7........n
如果此时我想查找1这个数字,那么时间复杂度为O(1),当然这是最好情况,也就是说我第一次查查找的时候就找到了这个数字.
如果此时我想查找n这个数字,那么时间复杂度为O(n),当然这是最坏情况,直到我查找到最后才查到n这个数字
当然也有平均情况就是O(N/2),前提是这个查找的顺序是有序的,但是一般我们不去讨论平均情况,因为没有意义,
所以时间复杂度其实是可以分为三种情况:1.最好情况 2.最坏情况 3.平均情况,平时我们所说的时间复杂度指的就是最坏情况下的时间复杂度.
下面我们来看上述的冒泡排序的时间复杂度:
首先最外层的for循环最多执行N次,里层的for循环最多执行N-1次,两个for循环又是嵌套的关系,所以按照最坏情况来说,我们最终在最后一次才完成了排序,那么此时在最坏情况下的执行总次数为N*(N-1)=N^2-N,经过大O三部曲推导方法后,最终这个冒泡排序的时间复杂度为.
代码5 二分查找
int binarySearch(int[] array, int value) { int begin = 0; int end = array.length - 1; while (begin <= end) { int mid = begin + ((end - begin) / 2); if (array[mid] < value) begin = mid + 1; else if (array[mid] > value) end = mid - 1; else return mid; } return -1; }
针对二分查找(前提是在有序的情况下)这种算法,我们既然要算其时间复杂度,就要去考虑最坏的情况,下面我们来看以下几种情况:
第一种情况:
1,2 =====》针对这种情况,最坏的情况是找1或者2这两个数字,根据二分查找需要经过2次才可以找到.
第二种情况:
1,2,3,4======》针对这种情况,最坏的情况是找1或者4这两个数字,根据二分查找需要经过3次才可以找到.
第三种情况:
1,2,3,4,5,6,7,8======》针对这种情况,最坏的情况是找1或者8这两个数字,根据二分查找需要经过4次才可以找到
总结:
在第一种情况中,一共2个数字,我们找了2次(最坏情况)
在第二种情况中,一共4个数字,我们找了3次(最坏情况)
在第三种情况中,一共8个数字,我们找了4次(最坏情况)
假设个数为n,次数为b,可以看出n=2的(b-1)次方.即n=2^(b-1)
时间复杂度与我们寻找的次数有关,所以方向推理可以得出b=log(2)n+1,最后根据大O阶的推导方法可以得出我们二分查找的时间复杂度为O(log(2)n).
小结:根据现接触的时间复杂度有以下几种:
1:O(N^2) 2:O(N) 3:O(log(2)N) 4:O(1)
这几种时间复杂度按照速度来比较的话O(1)>O(log(2)N) >O(N)>O(N^2)(最常用的几个时间复杂度)
代码6 单路递归
long factorial(int N) { return N < 2 ? N : factorial(N - 1) * N; }
此处就需要我们去寻找他递归的次数,我们会发现当N=3时,递归次数为3,当N=4,递归次数为4,所以最终我们的递归次数就等于N的值,所以单路递归的时间复杂度为O(N)
代码7 多路递归
int fibonacci(int N) { return N < 2 ? N : fibonacci(N - 1) + fibonacci(N - 2); }
类似多路递归这种情况,一般来说我们都要考虑到最坏的情况,例如假设N=5,按照一般情况我们的递归总次数是12次,但是在最坏情况下是31次,是一棵满二叉树,得出规律为31=2^5-1.
正常情况:
即次数=2^N-1.那么经历过大O渐进表示法三部曲后,多路递归的时间复杂度为O(2^N).
三.空间复杂度
1.概念
空间复杂度是对一个算法在运行过程中临时占用存储空间大小的量度 。空间复杂度不是程序占用了多少bytes 的空间,因为这个也没太大意义,所以空间复杂度算的是变量的个数。空间复杂度计算规则基本跟时间复杂度类似,也使用大O渐进表示法。
2.代码示例
代码1
void bubbleSort(int[] array) { for (int end = array.length; end > 0; end--) { boolean sorted = true; for (int i = 1; i < end; i++) { if (array[i - 1] > array[i]) { Swap(array, i - 1, i); sorted = false; } } if (sorted == true) { break; } } }
定义一个boolean类型的变量来当作标志位,虽然这个临时变量定义在for循环内部,意味着每次循环都会占用一次存储空间,但是其每次循环时都会重新给这个sorted去进行赋值,那么就意味着每次也循环sorted只占用一次存储空间,所以最终的空间复杂度为O(1).
代码2
int[] fibonacci(int n) { long[] fibArray = new long[n + 1]; fibArray[0] = 0; fibArray[1] = 1; for (int i = 2; i <= n; i++) { fibArray[i] = fibArray[i - 1] + fibArray[i - 2]; } return fibArray; }
要完成这个代码就需要额外生成fibArray这个临时数组,这个数组的大小为n+1,说明其临时会占用n+1这么大的内存,所以根据大O渐进表示法计算完成后,最终这个数组的空间复杂度为O(N)。
代码3
long factorial(int N) { return N < 2 ? N : factorial(N - 1) * N; }
对于单路递归来说,每递归几次,就会开辟几次内存去存储每次return回来的值,那么每次开辟内存的时候就相当于临时占用了存储空间
也可以这么理解,每次递归调用方法的时候都会在栈上为方法开辟栈帧,当每个方法接收到return回来的值的时候就会依次退栈,这也说明了递归几次就会临时开辟几个栈帧,最终递归N次便会开辟N个栈帧,那么单路递归的空间复杂度为O(N)
可以看到单路递归的时间复杂度为O(N),空间复杂度也为O(N),所以递归是非常浪费栈帧的,递归一旦写不对就会栈溢出异常。