数据结构-时间和空间复杂度
1. 引子
在之前的文章中(递归),我们对斐波那契数列的算法进行过一次系统的分析:
package demo1;
import java.util.Scanner;
public class Test1 {
public static int fib(int n) {
if (n == 1 || n == 2) {
return 1;
}
return fib(n-1) + fib(n - 2);
}
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
int x = scanner.nextInt();
System.out.println(fib(x));
}
}
上述算法是好是坏,我们得出的结论是:用递归实现斐波那契数列在获取较大数字时效率较低,可见,衡量一个算法的好坏,需要看它的算法效率
2. 算法效率
算法效率分为两种:
- 时间效率:也称
时间复杂度
,主要用来衡量一个算法的运行速度 - 空间效率:也称
空间复杂度
,主要用来衡量一个算法所需要的额外空间(计算机的存储容量已经达到很高程度,对此我们已不需要特别关注一个算法的空间复杂度)
3. 时间复杂度
3.1 概念
算法中的基本操作的执行次数,为算法的时间复杂度
3.2 大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--) > 0) {
count++;
}
System.out.println(count);
}
Func1执行的基本操作次数:
- N = 10 F(N) = 130
- N = 100 F(N) = 10210
- N = 1000 F(N) = 1002010
实际中我们在计算时间复杂度时,我们并不一定要计算精确的执行次数,只需要获取大概的执行次数即可,这里我们使用大O的渐进表示法(大O符号(Big O notation):是用于描述函数渐进行为的数学符号
)
3.3 推导大O阶方法
如何推导大O阶方法,分为以下三个步骤:
- 用常数1取代运行时间中的所有加法常数
- 在修改后的运行次数函数中,只保留最高阶项
- 如果最高阶项存在且表示1,则去除与这个项相乘的常数,得到的结果就是大O阶。
使用大O的渐进表示法后,Func1的时间复杂度为:
- N = 10 F(N) = 100
- N = 100 F(N) = 10000
- N = 1000 F(N) = 1000000
通过上面我们会发现大O阶的渐进表示法去掉了那些对结果影响不大的项,简洁明了的表示出了执行的次数。
另外,有些算法的时间复杂度存在最好、平均和最坏情况:
- 最坏情况:任意输入规模的最大运行次数(上界)
- 平均情况:任意输入规模的期望运行次数
- 最好情况:任意输入规模的最小运行次数(下界)
例如:在一个长度为N的数组中搜索一个数据x:
- 最好情况:1次找到
- 最坏情况:N次找到
- 平均情况:N次找到
在实际中一般情况关注的是算法的最坏运行情况,所有数组中搜索数据时间复杂度为O(N)
3.4 常见时间复杂度计算举例
【实例1】
package demo3;
public class Test1 {
void func1(int N) {
int count = 0;
for (int i = 0;i < 2 * N;i++) {
count++;
}
int M = 10;
while((M--)>0) {
count++;
}
System.out.println(count);
}
}
实例1基本操作执行了2N+10次,通过推到大O阶方法可知其时间复杂度为O(N)
【实例2】
package demo3;
public class Test2 {
void func2(int M, int N) {
int count = 0;
for (int x = 0;x < M;x++) {
count++;
}
for (int y = 0;y < N;y++) {
count++;
}
System.out.println(count);
}
}
实例2基本操作执行了M+N次,存在两个未知数M和N,则其时间复杂度为O(M+N)
【实例3】
package demo3;
public class Test3 {
void func3(int N) {
int count = 0;
for (int i = 0;i < 100;i++) {
count++;
}
System.out.println(count);
}
}
实例3基本操作执行了100次,则其时间复杂度为O(1);
【实例4】
package demo3;
import com.sun.org.apache.bcel.internal.generic.SWAP;
import java.util.Arrays;
public class Test4 {
static void bubbleSort(int[] array) {
for(int end = array.length;end > 0;end--) {
boolean judge = true;
for(int i = 1;i < end;i++) {
if (array[i-1] > array[i]) {
int temp = array[i-1];
array[i-1] = array[i];
array[i] = temp;
judge = false;
}
}
if (judge) {
break;
}
}
}
public static void main(String[] args) {
int[] arr = {5,7,2,3,1};
bubbleSort(arr);
System.out.println(Arrays.toString(arr));
}
}
//执行结果
[1, 2, 3, 5, 7]
实例4基本操作最好执行了N次,最坏执行了**(N*(N-1)/2)次**,时间复杂度一般看最坏,则其时间复杂度为O(N^2)
【实例5】
package demo3;
public class Test5 {
public static int binarySearch(int[] array, int value) {
int begin = 0;
int end = array.length - 1;
while(begin <= end) {
int mid = (begin + end)/2;
if (array[mid] < value) {
begin = mid + 1;
}
else if (array[mid] > value) {
end = mid - 1;
}
else {
return mid;
}
}
return -1;
}
public static void main(String[] args) {
int[] arr = {1,3,5,7,9};
System.out.println(binarySearch(arr,5));
}
}
//执行结果
2
实例5基本操作最好执行1次,最坏log₂N次,因为二分查找每次都会排除掉一半的不适合值,则每一次值都为上一次的1/2,所以当存在N个数据时,有总个数N/(2^(砍一半的次数)y) = 1(最后剩下的一个数) ==> y = log₂N,则时间复杂度为O(log₂N)
【实例6】
long factorial(int N) {
return N < 2 ? N : factorial(N-1) * N;
}
实例6通过计算分析发现基本操作递归了N次,且递归的时间复杂度=递归次数*每次递归后的执行次数,则时间复杂度为O(N)
【实例7】
int fibonacci(int N) {
return N < 2 ? N : fibonacci(N - 1) + fibonacci(N - 2);
}
由图可以推导出该递归的次数公式为等比数列求和公式:2^0 + 2^1 + … + 2^ (n-1),则Sn = 1*(1-2^n)/1-2 = 2^n - 1,所有时间复杂度为O(2^N)
4. 空间复杂度
空间复杂度是对一个算法在运行过程中临时占用存储空间大小的量度。空间复杂度不是程序占用了多少bytes的空间,因为这个也没有太大意义,所以空间复杂度算的是变量的个数。空间复杂度计算规则跟时间复杂度类似,也使用大O渐进表示法
【实例1】
void bubbleSort(int[] array) {
for(int end = array.length;end > 0;end--) {
boolean judge = true;
for(int i = 1;i < end;i++) {
if (array[i-1] > array[i]) {
int temp = array[i-1];
array[i-1] = array[i];
array[i] = temp;
judge = false;
}
}
if (judge) {
break;
}
}
}
实例1使用了常数个额外空间,所以空间复杂度为O(1)
【实例二】
package demo4;
import java.util.Arrays;
import java.util.Scanner;
public class Test1 {
static long[] 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;
}
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
int n = scanner.nextInt();
System.out.println(Arrays.toString(fibonacci(n)));
}
}
//执行结果
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610]
实例2动态开辟了N个空间,则其空间复杂度为O(1)
【实例3】
long factorial(int N) {
return N < 2 ? N : factorial(N-1) * N;
}
实例3递归调用了N次,开辟了N个栈帧,每个栈使用了常数个空间,则其空间复杂度为O(N)