本文主要讲解关于时间复杂度与空间复杂度
😀😃😁😁😇😇
😀😃😁😁😇😇
一.前言
时间复杂度主要衡量的是一个算法的运行速度,而空间复杂度主要衡量一个算法所需要的额外空间。在计算机发展的早期,计算机的存储容量很小,所以对空间复杂度很是在乎。但是随着计算机行业的迅速发展,计算机的存储容量已经达到了很高的程度。所以我们如今已经不需要再特别关注一个算法的空间复杂度。这就是为什么我们大多时候听到的是时间复杂度,而很少听到空间复杂度的原因。
二.时间复杂度
定义
- 时间复杂度就是用来方便开发者估算出程序的运行时间
- 我们该如何估计程序运行时间呢,我们通常会估计算法的操作单元数量,来代表程序消耗的时间,
这里我们默认CPU的每个单元运行消耗的时间都是相同的。 - 假设算法的问题规模为n,那么操作单元数量便用函数f(n)来表示
- 随着数据规模n的增大,算法执行时间的增长率和f(n)的增长率相同,这称作为算法的渐近时间复杂度,简称时间复杂度,记为 O(f(n))
- 这里就要说一下这个大O,什么是大O呢,很多同学说时间复杂度的时候都知道O(n),O(n^2),但说不清什么是大O,算法导论给出的解释:大O用来表示上界的,当用它作为算法的最坏情况运行时间的上界,就是对任意数据输入的运行时间的上界。
😀😃😁😁😇😇
时间复杂度的计算规则
- 基本操作即只有常数项,认为其时间复杂度为O(1)
- 顺序结构,时间复杂度按加法进行计算
- 循环结构,时间复杂度按乘法进行计算
- 分支结构,时间复杂度取最大值
- 在没有特殊说明时,我们所分析的时间复杂度都是指最坏时间复杂度
- 判断一个算法效率时,往往只需要关注操作数量的最高次项,其他次要项和常数项可以忽略
注:递归算法的时间复杂度 = 递归的次数 * 每次递归函数中的次数。
习题
例一
int result = 100; //运行程序只执行一次 result ++ ; //执行一次 System.out.println ("Hello!"+result);
上面算法的运行的次数的函数为f(n)=3,根据推导大O阶的规则1,每次运行程序每条语句执行一次,所以这个算法的时间复杂度仍旧是O(1),我们可以称之为常数阶。
例二
void fun4(int N) { int count = 0; for (int k = 0; k < 100; k++) { ++count; } printf("%d\n", count); }
fun3的基本操作的执行了100次,通过推导大O阶方法知道,时间复杂度为O(1),也就是执行次数为常数。
例三
for(int i=0;i<n;i++){ System.out.println(result[i]); //执行一次 }
上面算法循环体中的代码执行了n次,因此时间复杂度为O(n),实际上,在for循环里面的所有时间复杂度为O(1)的语句总的时间复杂度都是O(n)。
例四
int result=1; while(result<n){ result=result*2; //时间复杂度为O(1) }
可以看出上面的代码, result=result*2; 随着result每次乘以2后,都会越来越接近n,当result大于等于n时就会退出循环(限制条件)。
如果循环的次数为T,所以2^T=n于是T=log₂n,因此得出这个算法的时间复杂度为O(logn)。
例五
//二分查找法 int BinarySearch(int* a, int n, int x) { assert(a); int begin = 0; int end = n - 1; while (begin < end) { int mid = ((end - begin) >> 1) + begin; //计算end与begin的中间值,右移1位相当于除以2 if (a[mid] < x) {begin = mid - 1;} else if(a[mid]>x){end = mid;} else {return mid;} } return -1; }
时间复杂度为:O(logN)。分析如下:
第一次查找:在长度为N的数组中查找值,取中间值进行比较
第二次查找:在长度为N/2的数组中查找值,取中间值进行比较
第三次查找:在长度为N/(2^2)的数组中查找值,取中间值进行比较
第logN次查找:在长度为N/(2^logN)的数组中查找值,即在长度为1的数组中查找,无论是否找到均跳出循环,结束查找
例六
for(int i=0;i<n;i++){ for(int j=0;j<n;j++){ System.out.println(result[i][j]); //执行一次 } }
这是一个循环嵌套的语句,很明显内层循环的时间复杂度在讲到线性阶时就已经得知是O(n),又经过了外层循环n次,那么这段算法的时间复杂度则为O(n²)。
例七
void fun(int n){ int i,j,x=0; for(i=1;i<n;i++){ for(j=n;j>=i+1;j--){ x++; } } }
例八
void fun(int n){ int i=0; while(i*i*i<=n){ i++; } }
例九
// 多个复杂度组合 for(int i=0;i<n;i++){ for(int j=0;j<n;i++){ System.out.println(result[i][j]); //执行一次 } } for(int i=0;i<n;i++){ System.out.println(result[i]); //执行一次 }
对于顺序执行的语句或者算法,总的时间复杂度等于其中最大的时间复杂度。所以对于以上的代码,时间复杂度为O(n²)。
例十
// 多个复杂度组合 if(flag){ for(int i=0;i<n;i++){ for(int j=0;j<n;i++){ System.out.println(result[i][j]); //执行一次 } } }else{ for(int i=0;i<n;i++){ System.out.println(result[i]); //执行一次 } }
对于条件判断语句,总的时间复杂度等于其中时间复杂度最大的路径的时间复杂度。所以对于以上的代码,时间复杂度为O(n²)。
三.空间复杂度
定义
既然时间复杂度不是用来计算程序具体耗时的,那么我也应该明白,空间复杂度也不是用来计算程序实际占用的空间的。
空间复杂度是对一个算法在运行过程中临时占用存储空间大小的一个量度,同样反映的是一个趋势,我们用 S(n) 来定义。
😀😃😁😁😇😇
计算方法
空间复杂度是对一个算法在运行过程中临时占用存储空间大小的量度。空间复杂度不是程序占用了多少字节的空间,因为这个也没太大意义,所以空间复杂度算的是变量的个数。空间复杂度计算规则基本跟时间复杂度类似,也使用大O渐进表示法。
习题
空间复杂度比较常用的有:O(1)、O(n)、O(n²),我们下面来看看:
空间复杂度 O(1)
如果算法执行所需要的临时空间不随着某个变量n的大小而变化,即此算法空间复杂度为一个常量,可表示为 O(1)
int i = 1; int j = 2; ++i; j++; int m = i + j;
代码中的 i、j、m 所分配的空间都不随着处理数据量变化,因此它的空间复杂度 S(n) = O(1)
空间复杂度 O(n)
int[] m = new int[n] for(i=1; i<=n; ++i) { j = i; j++; }
这段代码中,第一行new了一个数组出来,这个数据占用的大小为n,虽然有循环,但没有再分配新的空间,因此,这段代码的空间复杂度主要看第一行即可,即 S(n) = O(n).
例一
//计算冒泡排序函数的空间复杂度 void BubbleSort(int* a, int N) { assert(a); for (int i = 0; i < N; i++) { int exchange = 0; for (int j = 0; j < N - 1 - i; j++) { if (a[j]>a[j + 1]) { int tmp = a[j]; a[j] = a[j + 1]; a[j + 1] = tmp; exchange = 1; } } if (exchange == 0) break; } }
冒泡排序函数中使用了常数个额外空间(即常数个变量),所以用大O的渐进表示法表示冒泡排序函数的空间复杂度为O(1) 。
例二
//计算阶乘递归函数的空间复杂度 long long Factorial(size_t N) { return N < 2 ? N : Factorial(N - 1)*N; }
阶乘递归函数会依次调用Factorial(N),Factorial(N-1),…,Factorial(2),Factorial(1),开辟了N个空间,所以空间复杂度为O(N) 。
注:递归算法的空间复杂度通常是递归的深度(即递归多少层)。