一、时间复杂度
1.简介
算法的时间复杂度计算的不是算法运行了多少时间,而是算法运行的大概的次数,因为运行时间时间是计算不出来的,因为不同硬件配置的计算机运行速度是不同的。
2.经典案例
2.1冒泡排序
public static void main(String[] args) {
int[] array = new int[]{10, 5, 6, 1, 2 ,3, 8, 7, 9, 4};
//int[] array = new int[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
System.out.println(Arrays.toString(array));
for (int i=0; i<array.length-1; i++){
int exchange = 0;
for (int j=0; j<array.length-1-i; j++){
if (array[j] > array[j+1]){
int temp = array[j];
array[j] = array[j+1];
array[j+1] = temp;
exchange = 1;
}
}
if (exchange == 0){
break;
}
}
System.out.println(Arrays.toString(array));
}
冒泡排序的算法时间复杂度为
O
(
N
2
)
O(N^2)
O(N2),我们先不看代码,先从冒泡排序的思想来想一下,以从大到小排序为例,每次比较都是相邻的进行比较,如果第一个大于第二个,则交换位置,然后第二个和第三个再进行比较,第一轮比较下来,比较了 n-1 次,由于第一轮比较的时候,已经将最大的数冒到了最后一位,所以第二轮比较的时候,比较的元素个数为 n-2 个(因为最大的数即最后一个元素不用比较了),同理,第三轮为 n-3, 第四轮为 n-4,一直到1。所以可以看出冒泡排序的时间复杂度为一个等差数列,即 1+2+3+4+…+(n-1),由等差数列得出
n
+
(
n
(
n
−
1
)
)
/
2
=
n
2
n+(n(n-1))/2=n^2
n+(n(n−1))/2=n2。因为时间复杂度是一个估算值,当n值很大时,各种常数项,系数,低阶项与高阶项相比,均可忽略不计,因此取值
n
2
n^2
n2,当然这是最坏的情况,因为时间复杂度就是要取最后的情况,最好的情况就是这个数组原本就有序,那么他的时间复杂度为
O
(
N
)
O(N)
O(N)。
2.2二分查找
public static void main(String[] args) {
int[] array = new int[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int findTemp = 11;
int start = 0;
int end = array.length - 1;
while (start <= end){
int middle = (start + end) >> 1;
if (findTemp == array[middle]){
System.out.println(middle);
return;
}else if (findTemp > array[middle]){
start = middle + 1;
}else {
end = middle - 1;
}
}
System.out.println(findTemp + " not find.");
}
二分查找的前提条件是要查找的数列是有序的,冒泡排序可以参考上面的示例。至于二分查找的时间复杂度,肯定不是 O ( N ) O(N) O(N),因为如果是 O ( N ) O(N) O(N),为什么不直接写一个循环查找呢,所以说二分查找的时间复杂度肯定小于 O ( N ) O(N) O(N),即 O ( l o g 2 N ) O(log_2N) O(log2N),以2为底N的对数,也就是 2 x = N 2^x=N 2x=N, x就是要查询的次数,二分查找的核心就是每次查找的范围缩小一半,举个例子,把中国人口的身份证号排个序,然后从中间找你的身份证号,如果运气好,第一次就找到了,运气不好的话,是不是要找13亿次,而如果用二分查找,只需要找31次就可以了,因为2的30次方=10亿左右,那么31次方就是20亿,远大于13亿,因此13亿次和31次进行比较,差距显而易见。但实际上这个算法不怎么样,因为使用该算法的前提是中国人的身份证号是有序的。
3.使用斐波那契计算阶乘
public static void main(String[] args) {
System.out.println(factorial(10));
}
private static int factorial(int n) {
return n < 2 ? n : factorial(n - 1) * n;
}
上述阶乘的时间复杂度为 O ( N ) O(N) O(N),因为如果计算 N,那么就要计算 1 到 N-1,如果计算 N-1,就要计算 1 到 N-2,那么递归算法的时间复杂度如何计算呢,递归算法时间复杂度=递归次数*每次递归函数的次数,以上述阶乘为例,递归次数为 N,每次递归函数的次数为 1,所以时间复杂度为 O(N), 如果内层是一个 N 次的循环,那么时间复杂度就为 O ( N 2 ) O(N^2) O(N2)。
4.求第N项斐波那契数列的值
public static void main(String[] args) {
System.out.println(fib(6));
}
private static int fib(int n) {
return n < 2 ? n : fib(n - 1) + fib(n - 2);
}
斐波那契的时间复杂度为
2
0
+
2
1
+
2
2
+
2
3
+
.
.
.
+
2
(
n
−
1
)
2^0+2^1+2^2+2^3+...+2^(n-1)
20+21+22+23+...+2(n−1)
因此第一层是
2
0
2^0
20,所以最后一层是
2
(
n
−
1
)
2^(n-1)
2(n−1),由等比数列公式可得,斐波那契时间复杂度为
2
N
−
1
=
O
(
2
N
)
2^N-1=O(2^N)
2N−1=O(2N),当n=50时,几乎已经运算不出来了,所以要进行优化,上述是计算某一位的值,可以优化为
private static long fib_one(int i) {
long a = 0;
long b = 1;
long n = 0;
if (i == 0){
return a;
}
for (int j=2; j<=i; j++){
n = a + b;
a = b;
b = n;
}
return n;
}
可以看出,这个时间复杂度仅仅为 O(N),因为每一个后一项的值等于前两项的和,因为我们只需进行位置互换及迭代即可,同理我们可以求出一定范围内所有的值
public static void main(String[] args) {
System.out.println(fib_all(50));
}
private static String fib_all(int i) {
long[] fibArray = new long[i + 1];
fibArray[0] = 0;
fibArray[1] = 1;
if (i == 0){
return Arrays.toString(fibArray);
}
for (int j=2; j<=i; j++){
fibArray[j] = fibArray[j - 1] + fibArray[j - 2];
}
return Arrays.toString(fibArray);
}
这个算法的时间复杂度也为O(N),所以可以很快的计算出前50,乃至更大位数的斐波那契数列,从 O ( 2 N ) O(2^N) O(2N),到 O ( N ) O(N) O(N),差距显而易见。
二、空间复杂度
空间复杂度:不计算空间,计算的是定义变量的大概个数。
比如说定义了一个int型的变量,占据4个字节,相对于现在几个G的内存,完全可以忽略掉,所以我们应该更加关注时间复杂度,而不是空间复杂度,如果可以用空间换时间,就去换,比如上述的斐波那契数列,我们就是通过空间去换时间,多定义了几个变量,而程序的运行速度得到了质的飞跃。