💐 🌸 🌷 🍀 🌹 🌻 🌺 🍁 🍃 🍂 🌿 🍄🍝 🍛 🍤
📃个人主页 :阿然成长日记 👈点击可跳转
🚩 不能则学,不知则问,耻于问人,决无长进
🍭 🍯 🍎 🍏 🍊 🍋 🍒 🍇 🍉 🍓 🍑 🍈 🍌 🍐 🍍
1.📉 时间复杂度
📌1.1 时间复杂度的概念
· 时间复杂度的定义:在计算机科学中,算法的时间复杂度是一个函数,它定量描述了该算法的运行时间。一个算法执行所耗费的时间,从理论上说,是不能算出来的,只有你把你的程序放在机器上跑起来,才能知 道。但是我们需要每个算法都上机测试吗?是可以都上机测试,但是这很麻烦,所以才有了时间复杂度这个分析方式。一个算法所花费的时间与其中语句的执行次数成正比例,算法中的基本操作的执行次数,为算法 的
时间复杂度
。
1.2 大O的渐进表示法
大
O
符号(Big O notation):是用于描述函数渐进行为的数学符号。
🔸推导大O阶方法:
1、O(1)
用常数1取代运行时间中的所有加法常数。
2、在修改后的运行次数函数中, 只保留最高阶项。
3、如果最高阶项存在且不是1,则去除与这个项目相乘的常数。得到的结果就是大O阶。
🔹常见时间复杂度:( 复杂度由小到大)
常数阶 | O(1) |
线性阶 | O(n) |
对数阶 | O(logn) |
nlogn阶 | O(nlogn) |
平方阶 | O(n^2) |
立方阶 | O(n^3) |
指数阶 | O(2^n) |
🔸大O阶的三种情况:
最坏情况:任意输入规模的最大运行次数(上界)
平均情况:任意输入规模的期望运行次数
最好情况:任意输入规模的最小运行次数(下界)
注
:我们一般都取最坏的情况作为这个算法的时间复杂度。
🏰空间复杂度
·空间复杂度也是一个数学表达式,是对一个算法在运行过程中临时占用存储空间大小的量度 。
·空间复杂度不是程序占用了多少bytes的空间,因为这个也没太大意义,所以空间复杂度算的是变量的个数。
·空间复杂度计算规则基本跟实践复杂度类似,也使用大O渐进表示法。
空间复杂度一般都为O(N);
注意
:函数运行时所需要的栈空间(存储参数、局部变量、一些寄存器信息等)在编译期间已经确定好了,因此空间复杂度主要通过函数在运行时候显式申请的额外空间来确定。
📃例题分析
1.案例(常数阶)
int fun(int n) { int i = 0;int cnt = 0; for( i; i<100;i++) { cnt++; } return cnt; }
此时时间复杂度为O(1),
这里的1不是指一次,而是常数次,该循环执行了100次,不管n多大,他都执行100次,所以是O(1)常数阶。
2.案例(线性阶)
分析下面代码复杂度
// 计算Func3的时间复杂度? 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; } printf("%d\n", count); }
这里的时间复杂度是O(M+N)
;
因为我们并不知道M和N谁大,所以我们不能舍弃任何一个。
假如:M无穷大,那么时间复杂度为O(M);反之亦然。
3.案例:(平方阶)
分析下面代码的时间复杂度
int fun(int n) { int cnt = 0; for(int i = 0;i < n;i++) { for(int j = 0; j<n; j++) { cnt++; } }//两层循环,每次循环n次,因此为n*n for(int k = 0; k<n; k++) { ++cnt; }//一层循环,循环n次 for(int l = 0;l<10;l++) { ++cnt; }//一层循环,循环10次 return cnt; }
我们列出计算时间的复杂度的表达式:n*n +n +10
。但是我们能写成O(N*N+N+10
吗?我们知道,对于时间复杂度我们不需算出精确的数字,只需要算出这个算法属于什么量级
即可,我们又如何知道它属于哪个量级呢?即,我们将字母取无穷大,例如本题中字母为n,n取无穷大
,而十对于n取无穷大后没有影响,因此10可以舍去,原表达式化为nn+n,再转化为n(n+1),由于n为无穷大,因此-1也是没有影响的,原式就变成了O(N*N)=O(N^2)
。
📌 在这里我们使用大O渐近表示法,只是一种量级的估算,而不是准确的值。
4.案例(平方阶)
分析冒泡排序的时间复杂度
void bubblesort(int* a,int n) { assert(a); for(int end = n; end>0; end--) { int exchange = 0; for(int i = 1; i<end; i++) { if(a[i-1]>a[i]) { swap(&a[i],&a[i-1]); exchange = 1; } } if(exchange==0) break; } }
在这个冒泡排序中,我们需要将无序数组转化为有序数组的一种算法,它并不是简单的双层嵌套循环,很容易想到它的循环次数是一个等差数列,第一次循环n-1次,第二次n-2次…一直到1.因此为n-1+n-2+n-3…+1 =·n*(n-1)/2,使用大O表示,去掉影响不大的项,简化为: 时间复杂度为O(N*N)。
5.案例(对数阶)
二分查找
int binary(int n, int a[], int k) { int left = 0, right = n - 1; while(l <= r) { mid = (l + r)/2; if(a[mid] == k) return mid; else if(a[mid] < k) right = mid + 1; else left = mid + 1; } return -1; }
eft和right指数组最左边和最右边的下标.每次将这个数组砍一半,求出mid中间下标. 由于是升序排列,如果中间下标代表的数大于给定的数k,那么k必定在中间下标的左边. 那么就将mid+1的值赋给right,反之则将mid+1的值赋给left,每次将数组砍一半直到找到数k为止.
也就是每次除二。2^n=N -> n=logN;
6.案例(递归调用)
斐波那契函数
long long Fac(size_t N) { if(N == 0) return 1; return Fac(N-1)*N;
推到知道,这个函数会不停的向下调用,呈金字塔型,虽然运算量非常大,但是我们在算时间复杂度时要关注代码的思想,而不是看它的次数等。 递归函数的时间复杂度是多次用的次数的累加。
空间复杂度:根据调用的次数,每次都会占用栈上的空间,所以间复杂度为O(N)
;
❗️总结:
不管是算时间复杂度,还是空间复杂度。我们都要根据代码的思想,探求程序运行的过程,来思考复杂度。不能片面的根据数量,次数的多少来算。