一,算法好坏的衡量
一个算法的好坏我们主要从两个方面来进行定义:1,时间复杂度,2,空间复杂度
时间复杂度:也就是时间效率,看的是程序运行的运行速度,这也是现代计算机主要追求的点。
空间复杂度:也就是空间效率,看的是程序在运行过程中所占用的额外空间的多少。
***
二,时间复杂度
2.1,时间复杂度的概念
在计算机科学中,时间复杂度是一个函数。对于一个程序的运行时间,我们只有当程序在电脑上跑起来我们才可能知道其运行时间是多少,并且我们不可能每个程序都需要去上机测试一次,对于时间的定量测试也是不方便记录的。所以,最终我们将算法中基本操作的执行次数作为判断标准,为算法的时间复杂度。
***
2.2,大O的渐进表示法
void func1(int N){
int count = 0;
for (int i = 0; i < N ; i++) {
for (int j = 0; j < 2N ; j++) {
count++;
}
}
for (int k = 0; k < 2 * N ; k++) {
count++;
}
int M = 10;
while ((M--) > 0) {
count++;
}
System.out.println(count);
}
`
看上面这段代码,我们可以知道,它的基本操作次数等于 2N^2 + 2N + 10。随着我们N的变化,程序的基本执行次数也是不一样的。但是对于一个程序而言,我们并不是一定需要准确的执行次数,只要有一个大概的值就足以描述我们的时间复杂度了,那这一个估计的值的确定,就需要我们的大O渐进表示法。
***
2.3,推导大O阶方法的标准
1,用1来代替我们基本操作次数中的加法常数,例如上面的10。
2,在修改后的运行次数中的表达式中,只保留最高阶项。
3,如果最高阶项的系数不是1,那么就将这个系数去除,得到最终的结果。
所以,根据以上的规则,上面的代码的执行次数我们用大O渐进法得到的最终结果就是 O(N^2)。
****
可能有些同学会想我们去掉一些项以及数之后对于结果不会有影响嘛?答案是不会的,因为我们不是要准确的得到它的基本执行次数,只是需要有一个大概的值来最为标准来衡量这个代码的时间复杂度而已,并且随着N的增大,我们去掉的都是一些很小很小的项,所以对于结果不会有一个大量级上的变化。
****
另外,对于时间复杂度,我们是存在最坏,最好,以及平均情况的。例如我们在一个长度为N的数组中去寻找一个数,最好就是一次就知道,最坏要找N次,平均是N/2次找到。但是在实际的情况中,我们说时间复杂度,说的都是最坏情况,这是一个最低的标准。
***
2.4,实例求解
示例一:
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)。
***
示例二:
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)。这里的M,N都是未知的。
***
示例三:
void func4(int N) {
int count = 0;
for (int k = 0; k < 100; k++) {
count++;
}
System.out.println(count);
}
基本执行次数100,时间复杂度O(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;
}
}
}

***
所以,推导之后,我们发现我们的基本执行次数就是1/2*(N2 - N),则时间复杂度为O(N2)。其实如果大概看执行次数的话,外层循环最坏要执行N次,内层循环最坏要执行N-1次,所以时间复杂度这样也可以得出是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;
}
`
****
二分查找,根据举的例子我们可以得出规律,然后推出次数的公式,所以时间复杂度就是O(logN(2为底))。这个时候的时间复杂度我们是根据准确的公式得到的,但是很多我们不可能一个个去推公式,所以这个时候就可以根据代码的思想来大概求解,N个数,每次分成一半,分多少次后,得到最后只剩一个数,那就是N/2^x = 1,所以最后根据这个得到的时间复杂度也是O(logN(2为底))。
***
总结,由这个题可以看出,我们在推时间复杂度的时候,不能仅仅只看代码,比如二分查找只一个循环,遍历了数组,那好像复杂度就是O(N),这样就错了,所以我们解决时间复杂度是代码结合方法的思想共同推导。另外,我们时间复杂度在求解的时候如果可以有方法大概求出执行次数,就没有必要去推导那个执行次数的准确公式了。
***
示例六:
long factorial(int N) {
return N < 2 ? N : factorial(N-1) * N;
}
`
有关递归的时间复杂度的问题:递归时间复杂度 = 递归的次数 * 每次递归的执行次数
那对于这个代码而言,每次递归进行就只有一个三目运算符的语句,所以每次都只执行一次,递归总次数我们举例后可以推出是N-1,所以时间复杂度就是O(N)。
***
示例七:
int fibonacci(int N) {
return N < 2 ? N : fibonacci(N-1)+fibonacci(N-2);
}
***
时间复杂度 :O(2^N)
***
三,空间复杂度
空间复杂度算的是我们为了实现这个算法所额外要占的空间的大小,但是这个空间的大小我们是以变量的个数来衡量的。最终的空间复杂度的结果也是用大O渐进法来求。
****
示例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;
}
}
}
变量个数三个,end,sorted,i,空间复杂度: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;
}
`
这个题的目的是求第n项的斐波那契数,所以fibarray就是额外的空间,变量的个数就是 n + 1 + 1个。所以空间复杂度就是O(n)。
***
示例三:
long factorial(int N) {
return N < 2 ? N : factorial(N-1)*N;
}
对于递归的空间复杂度的问题,为递归的总次数 * 每次递归的过程中占用的空间数,在这段代码递归求阶乘的过程中,每次递归都只有一个变量,总共是递归了N-1次,所以空间复杂度就是O(N)。
***