时间复杂度和空间复杂度
衡量一个程序的好坏往往有许多种方法,最通常的方法还是看两个方面,一个是程序运行完成后所需要的时间的快慢,还有一个是相同时间下的程序所占用的空间大小。
通常来说,我们把一个所用时间短,所占空间小的程序,称为一个好的程序。
为什么会有时间复杂度?
对于不同的计算机而言,程序运行的时间还和计算机的硬件的性能有关,有的计算机一秒钟能进行上亿万次的运算,而有的计算机一秒钟连一亿次都不到,这就导致了相同的程序在不同的计算机上进行运行的时候,所用的时间也是不一样的。也就是说,完成相同功能的两个程序,程序一和程序二,在相同的计算机上程序一更快,但是给程序二换上一个运算更牛的计算机,那么程序二所用的时间就会比程序一所用的时间短,所以单纯的凭借程序所运行的时间的快慢也不能进行程序优良与否的判断,因此我们需要引入时间复杂度的概念,进行程序时间优化程度的判断
大O的渐进表示法
我们来观察一下下列的代码
计算一下count自增了几次
public static void count_1(int n){
int count = 0;
for (int i = 0; i < n; i++) {
for(int j = 0; j < n; j++){
count ++;
}
}//自增了n^2;
for(int i = 0; i < 2 * n; i++){
count ++;
}//自增了 2 * n;
int m = 10;
while(m != 0){
count ++;
m--;
}//自增了10次
}
假设
f(n)代表这count自增的次数,则f(n)有这样的表达式
当 n = 1 的时候,F(n) = 13
当 n = 10 的时候, F(n) = 130
当 n = 100 的时候,F(n) = 10210
当 n = 1000 的时候,F(n) = 1002010
随着n的增加,我们可以进行近似的估计,F(n) = n^2(随着n的增大,2*n + 10 对于整体的数的增长的贡献越来越小)
所以我们就说这个算法的时间复杂度就是O(n^2);
程序中的大O推导方法
1、用常数1取代运行时间中的所有的常数的所有的加法
2、在得到推到公式之后,只保留所有项中的次数最高的项
3、最高次数的项的项数进行省略就可以求出大O阶
时间复杂度的实例
<1>
public static void fun2(int n){
for(int i = 0; i < 2 * n; i++){
n ++;
}
for(int i = 0; i < 10; i++){
n *= n;
}
}
此方法的时间复杂度是O(n)
根据上述的推导大O阶的方法,首先找到最高的此项,就是2* N,去掉常数项和最高次项的系数,因此此方法的时间复杂度就是O(n);
<2>
public static void fun3(int n){
for(int i = 0; i < 10 ;i++){
n ++;
}
}
此方法的时间复杂度是O(1)
因为推导公式中只存在一个常数,因此他的时间复杂度就是O(1)
最坏时间复杂度
在我们通常的程序当中,程序并不一定会跑完全程,就比如一下的冒泡排序
public static void bubbleSort(int[] arr){
for(int i = 0; i < arr.length; i++){
boolean mark = true;
for(int j = 0; j < arr.length - i - 1; j++){
int temp = 0;
if(arr[j] > arr[j+1]){
temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
mark = false;
}
}
if(mark){
break;
}
}
}
当传入待排序的数组是完全有序的情况下,我们就不需要对他再次进行排序,这个程序也就只会进行一次内部的循环就会跳出,而不会多次的进行内部循环,这种情况我们称之为最好的情况。
但是作为一个合格且健壮的程序,我们绝对不能仅仅考虑它的最优方案的时间,我们要考虑的就是它最坏的时间复杂度,当这个程序跑完全程的情况,就是它的最坏的情况,此时的时间复杂度是O(n^2),所以我们称这个程序的时间复杂度是O(n^2)
<二分查找的时间复杂度>
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;
}
二分法进行查找,每次进行折半操作,如下图所示
假设原来有n个元素,进行x次循环,每次循环折半,由此可以列出等式
因此二分查找的时间复杂度就是
<阶乘的时间复杂度>
public static int factor(int N){
if(N == 1){
return 1;
}
return factor(N - 1) * N;
}
因为此递归的时间复杂度有着自己的计算公式
递归的深度 * 每次递归的时间复杂度 = 递归的时间复杂度
所以递归的深度是N次,每次递归的复杂度是O(1),所以阶乘递归的时间复杂度就是O(N)
<斐波那契数列的时间复杂度>
public static int fibonacci(int n){
if(n == 1 || n == 0){
return 1;
}
return fibonacci(n - 1) + fibonacci(n - 2);
}
斐波那契数列的时间复杂度同样使用递归调用的次数×每次调用的时间复杂度得到的,由于每次调用的时间复杂度是O(1),所以计算递归调用的次数即可
输入值是4,就会进行如图的次数,因此,时间复杂度是
O(2^n)
空间复杂度
空间复杂度是对一个算法在运行过程中临时占用存储空间大小的量度 。空间复杂度不是程序占用了多少bytes的空间,因为这个也没太大意义,所以空间复杂度算的是变量的个数。空间复杂度计算规则基本跟实践复杂度类似,也使用大O渐进表示法
<冒泡排序空间复杂度>
public static void bubbleSort(int[] arr){
boolean mark = true;
for(int i = 0; i < arr.length; i++){
for(int j = 0; j < arr.length - i - 1; j++){
int temp = 0;
if(arr[j] > arr[j+1]){
temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
mark = false;
}
}
if(mark){
break;
}
}
}
对于冒泡排序的空间复杂度,他是在原数组上进行的就地更改,没有开辟新的内存空间,启用一个新的数组,因此我们说它的空间复杂度就是O(1)
<斐波那契数列的空间复杂度(循环)>
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;
}
每次调用函数就会开辟新的空间,因此它的空间复杂度就是O(n)