时间复杂度和空间复杂度
- 如何衡量一个算法的好坏?
- 什么是时间复杂度?
- 时间复杂度为什么不使用时间来衡量而使用基本语句的运行次数来衡量?
- 时间复杂度的O渐进表示法
- 时间复杂度的:最优、平均、最差情况,为什么时间复杂度看的是最差情况?
- 如何求解:二分查找、递归求阶乘、递归斐波那契的时间复杂度?
- 什么是空间复杂度?
- 如何求空间复杂度? 普通函数&递归函数
- 分析递归斐波那契数列的:时间、空间复杂度,并对其进行优化,伪递归优化—>循环优化
- 总结常见时间复杂度
- 如何衡量一个算法的好坏?
算法:就是定义良好的计算过程,他取一个或一组的值为输入,并产生一个或一组作为输出。简单来说算法就是一系列的计算步骤,用来输入数据转化为输出结果。
算法效率:算法效率分析分为两种,第一种是时间效率,第二种是空间效率。时间效率被称为时间复杂度,主要衡量的是一个算法的运行速度,而空间复杂度主要衡量一个算饭所需要的额外空间。
2.什么是时间复杂度?
时间复杂度:什么是时间复杂度,算法中某个函数有n次基本操作重复执行,用T(n)表示,现在有某个辅助函数f(n),使得当n趋近于无穷大时,Tn)/f(n)的极限值为不等于零的常数,则称f(n)是T(n)的同数量级函数。记作T(n)=O(f(n)),称O(f(n)) 为算法的渐进时间复杂度,简称时间复杂度。也就是说:算法中基本操作的执行次数,为算法的时间复杂度。
- 时间复杂度为什么不使用时间来衡量而使用基本语句的运行次数来衡量?
一个算法执行所消耗的时间,从理论上是不能算出来的,只有把程序放在计算机上面跑起来,才能知道。而一个相同的算法在不同的机器上运行的时间可能不同,故不便于比较。而一个算法所执行需要的时间与它里面执行的基本语句的次数成正比,所以用基本语句的基本次数来衡量更为合理。
- 时间复杂度的O渐进表示法
// 请计算一下Func1基本操作执行了多少次?
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; } printf("%d\n", count); }
实际中我们计算时间复杂度时,我们其实并不一定要计算精确的执行次数,而只需要大概执行次数,那么这里我们使用大O的渐进表示法。
大O阶方法:
1、用常数1取代运行时间中的所有加法常数。
2、在修改后的运行次数函数中,只保留最高阶项。
3、如果最高阶项存在且不是1,则去除与这个项目相乘的常数。得到的结果就是大O阶。 使用大O的渐进表示法以后,Func1的时间复杂度为:
N = 10 F(N) = 100 N = 100 F(N) = 10000 N = 1000 F(N) = 1000000
通过上面我们会发现大O的渐进表示法去掉了那些对结果影响不大的项,简洁明了的表示出了执行次数。
另外有些算法的时间复杂度存在最好、平均和最坏情况:
最坏情况:任意输入规模的最大运行次数(上界)
平均情况:任意输入规模的期望运行次数
最好情况:任意输入规模的最小运行次数(下界)
- 时间复杂度的:最优、平均、最差情况,为什么时间复杂度看的是最差情况?
最坏情况:任意输入规模的最大运行次数(上界)
平均情况:任意输入规模的期望运行次数
最好情况:任意输入规模的最小运行次数(下界)
为什么时间复杂度要看最差情况呢?其实很容易理解,就像你去约会,为了不迟到,所以你要留出你最慢到达约会地点的时间,如果能早到当然最好不过,所以看最差情况的原因也就很明了,就是程序的正常运行的最差情况若已经满足,这个程序在任何情况下都可以正常运行了。
- 如何求解:二分查找、递归求阶乘、递归斐波那契的时间复杂度?
1.//二分查找
int BinarySearch(int* a, int n, int key) { assert(a); int left = 0; int right = n - 1; while (left < right) { int mid = left + ((right - left) >> 1); if (a[mid] == key) { return mid; } else if (a[mid] > key) { right = mid; } else { left = mid + 1; } } return -1; }
二分查找执行最好1次,最坏假设执行x次.
在这n个数中n/2/2直到找到key,所以就有
logn,所以该二分查找的时间复杂度为O(logn).
2//递归求阶乘long long Factorial(size_t n) { return n < 2 ? 1 : Factorial(n - 1) * n; }
基本操作递归了n次,所以时间复杂度为O(n);
单次递归中执行的次数 * 递归函数总递归数;
.
3.//递归求斐波那契long long Fibonacci(size_t n) { return n < 2 ? 1 : Fibonacci(n - 1) + Fibonacci(n - 2); }
基本操作递归了2^n,
所以时间复杂度为O(2^n);
- 什么是空间复杂度?
空间复杂度是对一个算法在运行过程中临时占用存储空间的量度。空间复杂度不是占用了多少bytes的空间,因为这个也没太大意义,所以空间复杂度算的是变量的个数。空间复杂度计算规则基本跟时间复杂度类似,也使用大O渐进表示法。
- 如何求空间复杂度? 普通函数&递归函数
计算BubbleSort的空间复杂度
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; } }
计算Fibonacci的空间复杂度
long long* Fibonacci(size_t n) { if(n==0) return NULL; long long * fibArray = new long 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 ; }
计算阶乘递归Factorial的时间复杂度
long long Factorial(size_t N) { return N < 2 ? N : Factorial(N-1)*N; }
实例1使用了常数个额外空间,所以空间复杂度为 O(1)
实例2动态开辟了N个空间,空间复杂度为 O(N)
实例3递归调用了N次,开辟了N个栈帧,每个栈帧使用了常数个空间。空间复杂度为O(N)
- 分析递归斐波那契数列的:时间、空间复杂度,并对其进行优化,伪递归优化—>循环优化
递归斐波那契数列
int Fib(int n) { return n < 3 ? 1 : (Fib(n - 1) + Fib(n - 2)); }
第n个数执行的次数为 2^ 0 + 2^ 1+…+ 2^ (n - 2)
是一个等比数列: S = [a1 + (1 - q^ n)] / (1 - q) = 2^ (n - 2) - 2;
时间复杂度: O(2^n)//优化
//1.循环优化int Fib(unsigned int n) { if (n < 3) return 1; else { int a=1, b=1; int num =0; for (int i = 3; i <= n; ++i) { num = a + b; a = b; b = num; } return num; } }
//所以时间复杂度为O(n),空间复杂度为O(1)
//2.伪递归优化long Fib(long n,long first,long second) { if (n == 1 || n == 2) return 1; if (n == 3) return first + second; if (n > 3) return Fib(n - 1, second, first + second); }
//递归调用了n 次,单次递归执行了常数次
//所以时间复杂度为O(n),空间复杂度为O(1)
- 总结常见时间复杂度
第一种
O(1)
常数复杂度, 最快的算法
取数组第1000000个元素
字典和集合的存取都是O(1)
数组的存取是O(1)
.
第二种
O(logN)
对数复杂度
假设有一个有序数组, 以二分法查找
第三种
O(n)
线性复杂度
假设有一个数组, 以遍历的方式在其中查找元素
.
第四种
O(nlogn)
求两个数组的交集, 其中一个是有序数组
A数组每一个元素都要在B数组中进行查找操作
每次查找如果使用二分法则复杂度是 logN
.
第五种
O(n2)
平方复杂度
求两个无序数组的交集