大家好,今天让我们来学习以下数据结构的基础,时间复杂度和空间复杂度。
首先,我们知道不同的程序员写出来的代码也有高低之分,实现一个程序的思想和算法也有好坏的区别。
衡量一个算法好坏的标准可以从这个算法的运行速度和所需的内存空间两个角度来分析,那么这两个因素哪个究竟是优先考虑的呢?(通常会优先考虑时间复杂度)
时间效率被称为时间复杂度,而空间效率被称作空间复杂度。
时间复杂度主要衡量的是一个算法的运行速度,而空间复杂度主要衡量一个算法所需要的额外空间。
一.时间复杂度
**定义:算法中的基本操作的执行次数,为算法的时间复杂度。
计算时间复杂度用的是大O的渐进表示法【大O符号(Big O notation):是用于描述函数渐进行为的数学符号。】
**推导大O阶方法:
1、用常数1取代运行时间中的所有加法常数。
2、在修改后的运行次数函数中,只保留最高阶项。
3、如果最高阶项存在且不是1,则去除与这个项目相乘的常数。得到的结果就是大O阶。
总结一下就是:大O的渐进表示法去掉了那些对结果影响不大的项,简洁明了的表示出了执行次数。
比如有些时候有些算法的时间复杂度存在最好、平均和最坏情况:
最坏情况:任意输入规模的最大运行次数(上界)
平均情况:任意输入规模的期望运行次数
最好情况:任意输入规模的最小运行次数(下界)
那么这种时候我们取得就是最坏的情况!
知道了推导方法,接下来让我们用实例感受一下吧:
1.请计算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()函数里面开头是双层循环,先看最里面的j从0遍历到n-1共循环了N次,那么每循环一个j,外层循环i就会循环n次,也就是说循环了n*n次,也就是计算了n^2次,所以执行的基本操作次数n^2;接着看我们的K,因为k是小于2*n的,所以这部分代码执行的基本操作次数就是2*n;最后while(m--),因为M的值是10,所以执行的基本操作次数是10,加起来是n^2+2*n+10;
根据上面的推导大O阶方法:常数用1取代,那么m就为1;又因为保留最高阶项,所以去掉我们的2*n;因为我们的最高阶项系数是1,所以最后得到的结果是n^2+1,那么我们可以想象一下,当n足够大时,这个1还有保留的必要吗?显然,我们可以省去1,那么func1()的时间复杂度就是O(n^2);
接下来让我们进入练习吧:
2.计算func2的时间复杂度:
void func2(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);
我们可以看到,func2是由两个循环组成的,其基本语句的执行次数是M+N,又因为M和N是未知数,所以时间复杂度为O(M+N);
3.计算func3的时间复杂度:
void func3(int N) {
int count = 0;
for (int k = 0; k < 100; k++) {
count++;
}
System.out.println(count);
}
这里我们需要注意,一眼看上去,k<100,循环进行了100次,很多人会写成O(100), 但是根据我们的大O推导法,因为100是常数项,所以时间复杂度是O(1);
增加难度:4.计算的func4时间复杂度:
void func4(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;
}
}
}
注意:这里需要区分最好情况和最坏情况!我们可以知道这个函数是冒泡排序,而冒泡排序最好的情况下是执行了N次,即数组是有序的,只要遍历到每一个元素即可;那么最坏的情况就是数组是倒序的,那么第一个元素就要和每个其余的元素相比较,比较次数是n-1 第二个元素比较次数就是N-2 ……依次到最后比较一次,那么总共的比较次数就是1+2+……+(n-1)次,而我们正好可以根据等差数列求和来得到最后的次数为:(N*(N-1))/2;在根据大O阶推导法,可以得到fun4的时间复杂度就是O(n^2);
5.计算阶乘递归factorial的时间复杂度:
long factorial(int N) {
return N < 2 ? N : factorial(N-1) * N;
}
在这里基本操作执行了N次,而根据递归的时间复杂度= 递归的次数*每次递归执行的次数
可以得到时间复杂度为:O(N);
6.计算斐波那契递归fibonacci的时间复杂度:
int fibonacci(int N) {
return N < 2 ? N : fibonacci(N-1)+fibonacci(N-2);
}
求斐波那契数列第n项的值,我们知道,当我们要求第n项的值时,我们需要知道第n-1项和n-2项的值,而要求的第n-1项的值,我们需要求出n-2项的值,如此下去,我们可以发现,基本操作呈现二叉树的形式分布,而因为递归调用了N次,所以开辟了N个函数栈帧,每个栈帧使用了常数个空间,即可以理解为当我们的F(3)在执行完毕的时候会释放栈空间,这个时候在执行右边的F(2),往回计算,所以最后可以推导出fibonacci()的时间复杂度为O(2^N);
二,空间复杂度
**定义:空间复杂度和时间复杂度一样,也是一个数学表达式,是对一个算法在运行过程中临时占用存储空间大小的量度 ,而我们的空间复杂度也是用大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;
}
我们可以发现,函数参数传进来一个数组array(实际上传的是array首元素的地址),我们在函数的执行过程中并没有开辟新的空间,所以空间复杂度为O(1);
2.计算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项的值时我们动态开辟了N个空间,所以空间复杂度为 O(N);
3.计算阶乘递归factorial的空间复杂度:
long factorial(int N) {
return N < 2 ? N : factorial(N-1)*N;
}
我们可以知到因为递归调用了N次,所以开辟了N个函数栈帧,每个栈帧使用了常数个空间,即可以理解为当我们的F(3)在执行完毕的时候会释放栈空间,这个时候在执行右边的F(2),所以最终开辟的空间大小以最深的来理解,即为N,所以空间复杂度为O(N);
这里我们就学完时间复杂度和空间复杂度了,希望对大家有所帮助!