前言:对于时间复杂度和空间复杂度,我们首先要了解算法效率,这两个复杂度是用来衡量算法效率的。
让我们进入学习吧。
时间复杂度和空间复杂度:
- 一.算法效率
- 二.时间复杂度
- 1.概念
- 2.大O的渐进表示法
- 3.常见时间复杂度计算举例
- 题目1
- 题目2
- 题目3
- 题目4
- 题目5
- 题目6
- 题目7
- 三.空间复杂度
一.算法效率
算法效率分析分为两种:第一种是时间效率,第二种是空间效率。
时间效率被称为时间复杂度,而空间效率被称作空间复杂度。 时间复杂度主要衡量的是一个算法的运行速度,而空间复杂度主要衡量一个算法所需要的额外空间。
在计算机发展的早期,计算机的存储容量很小。所以对空间复杂度很是在乎。但是经过计算机行业的迅速发展,计算机的存储容量已经达到了很高的程度。所以我们如今已经不需要再特别关注一个算法的空间复杂度.
二.时间复杂度
1.概念
时间复杂度的定义:
在计算机科学中,算法的时间复杂度是一个函数,它定量描述了该算法的运行时间。一个算法执行所耗费的时间,从理论上说,是不能算出来的,只有你把你的程序放在机器上跑起来,才能知道。但是我们需要每个算法都上机测试吗?是可以都上机测试,但是这很麻烦,所以才有了时间复杂度这个分析方式。
时间复杂度的概念:
一个算法所花费的时间与其中语句的执行次数成正比例,算法中的基本操作的执行次数,为算法的时间复杂度。
也就是说,我们可以通过代码中的执行次数,大概算出其时间复杂度。
2.大O的渐进表示法
首先我们先不了解什么是大O的渐进表示法,我们先来看一些代码,算一下他们的执行次数(算法中的基本操作的执行次数)。
代码1:
void func1(int N){
int count = 0;
for (int i = 0; i < N ; i++) {
for (int j = 0; j < N ; j++) {
count++;//N*N
}
}
for (int k = 0; k < 2 * N ; k++) {
count++;//2N
}
int M = 10;
while ((M--) > 0) {
count++;//10
}
System.out.println(count);//1
}
我们来算一下:
那真的就这样子算出来了吗?对但不完全对。我们来分析一下得到的这个公式:
当N越来越大的时候,后面的位数越显得微不足道,那当N对于 100万的时候,我们可以说成,这个10加不加都无所谓了。然后2N对比起来NN也变得没那么主要了。实际中我们计算时间复杂度时,我们其实并不一定要计算精确的执行次数,而只需要大概执行次数,那么这里我们使用大O的渐进表示法。
大O符号(Big O notation):是用于描述函数渐进行为的数学符号。
推导大O阶方法:
1、用常数1取代运行时间中的所有加法常数。
2、在修改后的运行次数函数中,只保留最高阶项。
3、如果最高阶项存在且不是1,则去除与这个项相乘的常数。得到的结果就是大O阶。
我们对func1挨个来使用起来:
- 首先是第一个,用常数1取代运行时间中的所有加法常数,那么就是把加多少都去掉变成一个加1,得到就是
N*N+2*N+1
。 - 然后第二个修改说只保留最高项,也就是保留乘方数最大的,所以得到的就是
N*N
。 - 第三个是去除与这个项目相乘的常数,我们这里没有相乘的常数,比如说如果是
3*N*N
的话,第三个推导就会让他变成N*N
。但是这里已经是1了所以没变。
所以最后我们经过推导之后得到的func的时间复杂度就是N*N
。通过上面我们会发现大O的渐进表示法去掉了那些对结果影响不大的项,简洁明了的表示出了执行次数。所以:
另外有些算法的时间复杂度存在最好、平均和最坏情况:
情况 | 表达 |
最坏情况 | 任意输入规模的最大运行次数(上界) |
平均情况 | 任意输入规模的期望运行次数 |
最好情况 | 任意输入规模的最小运行次数(下界) |
例如:在一个长度为N数组中搜索一个数据x。
最好情况:1次找到 也就是恰好第一次就找到了
最坏情况:N次找到 遍历全部才找到
平均情况:N/2次找到
所以一般情况下,我们讨论的时间复杂度都是讨论最坏情况下,因为在最坏情况下,我们才有讨论的必要,所以数组中搜索数据时间复杂度为O(N)。
3.常见时间复杂度计算举例
高端的食材往往只需要最简单的烹饪方法,高端的知识往往应该刷点题去领悟,所以我们来把下面的题解析一下,就可以很好的了解一下,我们的时间复杂度计算了。
题目1
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);
}
这里的func2也很简单,就是一个for循环和一个while,计算得到2*N+10,经过三个推导大O阶的方法之后,得到的就是O(N*N)
。
题目2
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)
。
题目3
void func4(int N) {
int count = 0;
for (int k = 0; k < 100; k++) {
count++;
}
System.out.println(count);
}
这题略有偏差,就只有一个100,然后根据我们的方法,我们最后剩下的就是一个1,也就是说答案是O(1)
。
题目4
计算bubbleSort的时间复杂度?
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;
}
}
}
在这里,我们不能单单看代码,也要结合思想。这里是两个for循环,第一个是循环,循环值是length,然后第二个循环中是1开始的,所以是length-1,得到的答案就是length的平方,化为大O阶就是O(N*N)
。
注意:这里的
boolean sorted = true
是一句优化代码,它会减少循环数量,但是我们一直说的都是讨论最坏,所以我们就假设它是优化失败的来计算。
题目5
计算binarySearch
的时间复杂度?
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;
}
这里计算的是二分查找的复杂度,对于二分查找,我们应该先分析一下它的循环次数和个数的关系。这里分析元素个数和找的次数的关系是log的,所以我们最后的时间复杂度应该是O(log2n)
(这个2是以2为底的2)。
题目6
计算阶乘递归factorial
的时间复杂度?
long factorial(int N) {
return N < 2 ? N : factorial(N-1) * N;
}
对于这个递归来说,我们要看的是他的递归次数,也就是说:
递归的时间复杂度=递归的次数 * 每一次递归执行的次数
很显然这里在我们这里,每一次执行都是只是判断一次三目操作符,所以也就是说这里的时间复杂度就是递归的次数了,那么N是多少就会递归第三次,所以时间复杂度是O(N)
.
题目7
计算斐波那契递归fibonacci的时间复杂度?
int fibonacci(int N) {
return N < 2 ? N : fibonacci(N-1)+fibonacci(N-2);
}
对于斐波那契的递归的时间复杂度就真的比较复杂了,因为斐波那契的规律是后面的每一项都是前两项之和,前两项又是前前面的和来的,所以我们画图来更好理解。
递归栈帧的二叉树图解:
对于斐波那契的从第三项开始每一项都由前两项相加得,所以我们画出了这样子的图,然后得出每一层都是一个2的n次方递增下去,所以我们得到的是一个等比数列的时间复杂度,化简得到的就是O(n)
。
这就是时间复杂度。
三.空间复杂度
接下来我们学习空间复杂度。
空间复杂度是对一个算法在运行过程中临时占用存储空间大小的量度 。空间复杂度不是程序占用了多少bytes的空间,因为这个也没太大意义,所以空间复杂度算的是变量的个数。
空间复杂度计算规则基本跟时间复杂度类似,也使用大O渐进表示法。
与时间复杂度类似,那我们直接用代码来展示吧。
实例1:
// 计算bubbleSort的空间复杂度?
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;
}
}
}
完成冒泡排序的过程中,并没有说随着问题的规模越大,额外的变量在增加,在这里我们只定义了一个sorted变量,而那些比如数组的变量,是相当于是必须的。所以并不计入空间复杂度内。
也就是说,实例1使用了常数个额外空间,所以这里的空间复杂度就是
O(1)
。
实例2:
// 计算阶乘递归Factorial的时间复杂度?
long factorial(int N) {
return N < 2 ? N : factorial(N-1)*N;
}
那么对于这个斐波那契递归的空间复杂度呢,我们在上面已经了解到,当我们的N是多少的时候,我们就要有N个变量去计算,也就是我们每次都会执行一个函数创建一个变量,都是开辟内存的。
所以这个斐波那契的空间复杂度是O(N)。
实例3:
// 计算fibonacci的空间复杂度?
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越大,我们的数组就越大,所以我们的空间复杂度就是n+1。