算法(Algorithm):是指用来操作数据、解决程序问题的一组方法。对于同一个问题,使用不同的算法,也许最终得到的结果是一样的,但在过程中消耗的资源和时间却会有很大的区别。
对于算法的优劣判断,有人可能会想到用程序运行时间来判断,但程序运行的时间同时还受到计算机性能的影响,故而在时间和空间两个维度上提出了复杂度概念:
-
时间维度:是指执行当前算法所消耗的时间,我们通常用「时间复杂度」来描述。
-
空间维度:是指执行当前算法需要占用多少内存空间,我们通常用「空间复杂度」来描述。
计算复杂度,使用**「 大O符号表示法 」**
时间复杂度
在 大O符号表示法中,时间复杂度的公式是: T(n) = O( f(n) ),其中f(n) 表示每行代码执行次数之和,而 O 表示正比例关系,这个公式的全称是:算法的渐进时间复杂度。
先来看一段示例:
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);
}
上面的程序,先进行了一段双循环,一共运算了i*j次,每次循环都进行了一次加加,也就是一共n^2次; 之后又进行了2*n次加加,最后进行了10次减减;
所以,上述的程序一共的运行次数是**(n2+2*n+10)**次。那它的时间复杂度就是O(n2+2*n+10)吗? 并不是,
大O符号表示法并不是用于来真实代表算法的执行时间的,它是用来表示代码执行时间的增长变化趋势的。当n无限增大时,就会发现—决定算法次数大小的是2^n;
所以,该程序最后的时间复杂度就应该是O(2^n)。
常见的时间复杂度量级有:
- 常数阶O(1)
- 对数阶O(logN)
- 线性阶O(n)
- 线性对数阶O(nlogN)
- 平方阶O(n²)
- 立方阶O(n³)
- K次方阶O(n^k)
- 指数阶(2^n)
(该图来自递归&二分查找&时间复杂度、空间复杂度和稳定性)
从上至下依次的时间复杂度越来越大,执行的效率越来越低。
空间复杂度
既然时间复杂度不是用来计算程序具体耗时的,那么我也应该明白,空间复杂度也不是用来计算程序实际占用的空间的。
特别情况:递归的时间复杂度 = 递归的次数 * 每次递归执行的次数
空间复杂度是对一个算法在运行过程中临时占用存储空间大小的一个量度,同样反映的是一个趋势,我们用 S(n) 来定义。
空间复杂度比较常用的有:O(1)、O(n)、O(n²).
例如:
int i = 1;
int j = 2;
++i;
j++;
int m = i + j;
上面这段代码中,只使用了一开始定义的i 和 j; 并没有再额外的使用别的空间,因此它的空间复杂度 S(n) = O(1)
int[] m = new int[n]
for(i=1; i<=n; ++i)
{
j = i;
j++;
}
这段代码中,第一行new了一个数组出来,这个数据占用的大小为n,这段代码的2-6行,虽然有循环,但没有再分配新的空间,因此,这段代码的空间复杂度主要看第一行即可,即 S(n) = O(n)。
实例分析
递归求阶乘
long factorial(int N) {
return N < 2 ? N : factorial(N-1) * N;
}
时间复杂度:该程序一共会递归(N-1)次,每次递归都执行一次三目运算符,所以整体次数为(N-1)*1; 故时间复杂度为O(n)
空间复杂度:你是不是看到程序并没有int、int[ ]……那样的定义新的变量,就觉得他应该是S(n) = O(1);
错!你忽略了一个关键的问题:函数调用时会在栈上开辟空间,所以这个程序每调用一次就会临时占用一份空间,故空间复杂度应为S(n) = O(n)。
递归求斐波那契
int fibonacci(int N) {
return N < 2 ? N : fibonacci(N-1)+fibonacci(N-2);
}
通过画斐波那契数列程序执行时函数栈帧调用的图,可以发现是一个完全二叉树的样子,最坏情况下:将所有空缺位位补齐,可知斐波那契数列运行的总次数:2^(n)
- 2^(n-1)
- +……+1;所以时间复杂度为O(2^n).
空间复杂度S(n) = O(n).
二分查找
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;
}
经过画图列举元素为2,4……时,各需查找的次数分析,可知:二分查找的时间复杂度为O(log2 n);
冒泡排序
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;
}
}
}
这是一段优化过后的冒泡排序代码 ,比起优化前的一定时间复杂度为O(n^2),这段代码在最好情况下可以达到O(n),
最坏情况依然为O(n^2)。
空间复杂度:由于整个程序都在原数组array上进行交换顺序,并没有临时开辟别的内存空间,所以空间复杂度为S(n) = O(n)。
学会时间、空间复杂度的计算方法,我们就可以通过估计一个程序、算法的复杂度,来判断它的优劣性,从而优化代码,比如:尽量使用次方阶以下的算法,避免大量使用指数阶,就可以大大减少算法的时间复杂度。