1.时间复杂度
时间复杂度本身是个函数,方法很多,这里主要使用大O渐进法
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--) { ++count; } }
这里我们就能用O(N*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--) { ++count;
这里要注意的是,如果运行次数是一个清晰的常数,不管多大,都用1表示,也就是O(1)
,对于cpu来说,你运行一次还是1一亿次,对于现在越发快速的cpu来说,都是很短的时间。
时间复杂度是一个保守的估算,有些函数的运行次数取决于实际存储,比如一个查找函数,我们运行多少次,取决于实际的存储中到底有没有存储,存储在哪个位置。这种情况下,我们就可以看到这种函数有最好(马上找到)、平均(比如数组遍历到一半)、最坏(比如数据遍历到最后),为了保守估算,我们一般用最坏的情况作为这个函数的时间复杂度。
假如最高项的系数不是1呢,那么我们就除去这个系数(常数),得到的才是我们的复杂度。
但要注意,我们算时间复杂度,是先有思想(比如冒泡、快排),然后根据思想来估算,而不是写出代码后,数循环次数。
对于递归函数,我们是进行累加,而非累乘,因为我们估算的是运算的次数,而非运算的大小
// 计算阶乘递归Fac的时间复杂度? long long Fac(size_t N) { if(0 == N) return 1; return Fac(N-1)*N; } 这样的函数,不要被*迷惑,我们计算运算次数,而非大小 如果函数里面有个长度为N的for循坏, 这里与上面有所不同,因为每次调用函数,都会进行一次循环,而又是等差数列 所以可以用等差求和来算,最后省去系数,就剩N^2
long long Fib(size_t N) { if(N < 3) return 1; return Fib(N-1) + Fib(N-2); } fib的思想,可以理解为等比数列,而这里N<3,所以最后是2^(N-2)次 而利用等比数列求和,最后的时间复杂度,通过递归累加,是2^N
2.空间复杂度
空间复杂度是对算法在运行过程中临时占用空间大小的量度,不计算byte,而是变量的个数,也用大O渐进表示法。
函数运行时所需要的栈空间在编译时就已经确定了,因此主要算的是函数在运行时运行的额外的空间来确定。
void BubbleSort(int* a, int n) { assert(a); for (size_t end = n; end > 0; --end) { int exchange = 0; for (size_t i = 1; i < end; ++i) { if (a[i-1] > a[i]) { Swap(&a[i-1], &a[i]); exchange = 1; } } if (exchange == 0) break; } } 这里看似用了很多额外的变量,但是,这都是有限的,明确的,常数个变量,那么就只是O(1)
long long* Fibonacci(size_t n) { if(n==0) return NULL; long long * fibArray = (long long *)malloc((n+1) * sizeof(long long)); fibArray[0] = 0; fibArray[1] = 1; for (int i = 2; i <= n ; ++i) { fibArray[i] = fibArray[i - 1] + fibArray [i - 2]; } return fibArray; } 这里我们要关注的动态内存部分,其他的额外变量都只是常数,可以直接忽略 动态内存会额外申请n+1大小的数组,因此,每个元素算一个变量的话,那就是n+1个变量, 由于空间复杂度,我们也是估算,所以直接O(N)即可
递归空间复杂度也是累加,但空间存在重复利用的情况
// 计算阶乘递归Fac的空间复杂度? long long Fac(size_t N) { if(N == 0) return 1; return Fac(N-1)*N; } 这里很简单,每次调用函数都会开辟一个栈空间,所以就是O(N) long long* Fibonacci(size_t n) { if(n==0) return NULL; long long * fibArray = (long long *)malloc((n+1) * sizeof(long long)); fibArray[0] = 0; fibArray[1] = 1; for (int i = 2; i <= n ; ++i) { fibArray[i] = fibArray[i - 1] + fibArray [i - 2]; } return fibArray; } 这里就不一样了,fib的思想,看起来会开辟很多空间,好像要开辟2^N个空间,但事实上, 空间是重复利用的,最多只需要N-1个空间即可,因为递归的调用,是先一撸到底,然后再去其他分支 这样被算好了的空间,就可以拿来给别的分支用