大学初次接触计算机,也是初次接触算法,感觉现在光靠练习来学习还不够,和同级的人差距太大,赶进度会总是忘,就萌生了和以前一样写笔记写感想来加快记忆的方法,奈何字丑字丑字丑,而且事后难以修改,于是有了写博客来记录的决定。(其实是懒,博客提交后好像还可以随便改,也日后方便补充)
—2018.12.21晚
开篇
最近总是被TLE环绕,每次都是先想出算法然后就TLE最后才分析时间复杂度,浪费了大量的时间。想想自己对时间复杂度还不是那么敏感,好多还不会计算,但网上的博客对此大都讲的不是那么的精细(或许是我太菜了),就以决定时间空间复杂度为自己笔记记录的第一章(既然时间复杂度写了,那把空间一起带上) 。
关于时间复杂度在大学好像有专门的一节课程里有涉及,这里我只记录我目前知道的,肯定会有一些或很多错误,以后我会对此文章进行一系列的修改与完善。
哦对了,TLE就是Time Limit Error,MLE同理(Memory)。
一.时间复杂度自我总结
1.关于运行时间
正常我们做练习题,如本校TSOJ、洛谷、或cf上,都会给出时间限制,也就是Time limit,它一般以1000ms,2000ms的形式出现,但我一开始做的的题目都太水了,就从来不管时间消耗,写就完事了。但后来逐渐TLE多了,就慢慢开始了对时间复杂度的理解。1000ms是什么?他是出题者给出的程序从运行到结束的限制时间,1000ms也就是1s,而在1s的时间内,计算机大致能做出100000000(1亿)次计算。我在此章所要记录的就是对程序运行完成所需的最大(最坏)的运算次数的计算方法(其实就是些要背下来的东西)。首先,先给出一个表 (摘自《挑战程序设计竞赛》):
计算次数 | 计算状态 |
---|---|
1000000(1×106) | 游刃有余 |
10000000(1×107) | 勉勉强强 |
100000000(1×108) | 很悬,仅限于循环体非常简单的情况 |
由此看来,我们每一道题的运算次数都要追求在1×106以下,这就要求我们要学会对自己的算法进行一系列的优化,以保证能顺利(大概是吧)地AC所有测试点。
2.时间复杂度的大致表示方法以及可以承受的数据规模
表示方式 | 级数 | 可承受数据 |
---|---|---|
O(1) | 常数级 | 任意 |
O(n) | 线性级 | 1×107或108 |
O(n2) (O(n3),O(n4)……) | 次方级 | 这个自己看得出来 |
O(n!) | 阶乘级 | 10 |
O(logn) | 对数级 | 任意 |
O(2x) | 指数级 | 24 |
O(nlogn) | 线性对数阶 | 1×106 |
以上就是我暂时知道的时间复杂度表现方式,但实际的计算次数肯定不是一个小符号就可以表示的,每一段程序的时间复杂度其实是一个 f(n)方程,如f(n)=n3+2 ∗ \ast ∗n2+n+1;但由于n的值相对较大时,比如说n=1000时,n3=1×109而n2=1×106,相差103个数量级,所以n2以及后面的数相比而言小到完全可以忽略不计,因此一般只取次数最高的那一项。此外,n前的常数系数通常也不计。
3.常见算法的时间复杂度
以下皆为各算法最坏时间复杂度:
算法 | 复杂度 |
---|---|
取模,没有循环的操作 | O(1) |
二分查找,快速幂,堆,二叉搜索树 | O(logn) |
一维数组遍历查找,dfs,bfs,欧拉筛 | O(n) |
dfs,bfs | O(n+m) |
二分排序,快排,埃氏筛 | O(nlogn) |
冒泡,选择排序 | O(n2) |
二维数组遍历 | O(mn) |
阶乘 | O(n!) |
这些是暂时想起来的,萌新初用,以后再补。
4.时间复杂度计算具体分析
就我现在的理解,要计算算法的时间复杂度,主要是看其中的循环,也就是for,while等。而时间复杂度高的原因,就是循环的多层嵌套。所以,计算时间复杂度,找循环并分析是关键。下面用几个例子进行实际分析:
首先,O(1),这个我之前一直没分清它和O(n)的区别,后来大致了解,没有循环的操作就是O(1),比如直接访问数组的某一元素(你要查a[10]我直接给你),取模等等(后续补充)。
然后,简单的,一个for循环从0到n-1,共n次,复杂度O(n):
for(i=0;i<n;i++)
sum=(sum+i)*2;
再来个多重的:
for(i=1;i<=n;i++)
{
ans[i-1][0]=1;
for(j=m;j>0;j--)
{
for(k=0;k<t;k++)
ans[j][k]=j*ans[j][k]+ans[j-1][k];
for(k=0;k<t;k++)
if(ans[j][k]>=10)
{
int temp=ans[j][k]/10;
ans[j][k+1]+=temp;
ans[j][k]%=10;
}
}
}
第一层看作n次,第二层m次,第三层t次,第四层3次,f(n)=3nmt,舍去常数,时间复杂度为O(mnt)。
但有时时间复杂度可以不看for循环,而是从算法的实际意义来判断:
以下欧拉筛法筛素数:
typedef long long ll;
ll prime[MAXN],num,i,j;
bool vis[MAXN];
void isprime(ll n)
{
ll i,j;
for(i=2;i<=MAXN;i++)
{
if(!vis[i]) //还没被标记,一定是素数
prime[num++]=i;//放进数组
for(j=0;(j<=num)&&(i*prime[j]<=MAXN);j++)//判断结束的条件:已存的每一个都乘过了i(即素数的倍数一定不是素数);倍数小于n,不用判断过多
{
vis[i*prime[j]]=true;//倍数都不是
if(i%prime[j]==0) break;//欧拉筛法实现的关键
}
}
}
欧拉筛法具体是怎么实现的我不讲,只看它的时间复杂度。代码看上去有两层嵌套和多个if判断,但要知道在整个算法中,n个数每个都只被筛过(访问)一次,所以n个数的运算次数即为n,时间复杂度也就是O(n)。
dfs和bfs也可以这样看,虽然做了大量选择和疯狂的自我调用,但通过回溯,所有的点都只被访问过一次;此外,在访问任何一个点时都会访问他所在的边。因此,每个边会被恰好访问两次,对于稠密图,边的条数远远大与点数,所以,访问边的时间复杂度也不可忽视。那么,bfs与dfs的时间复杂度即为点的个数加边的条数,也就是O(n+m)。
int dir[4][2]={-1,0,1,0,0,1,0,-1};
bool vis[200][200]
void dfs(int x,int y)
{
int xx,yy,i;
if(x==ex&&y==ey)
{
tot++;
return;
}
for(i=0;i<4;i++)
{
xx=x+dir[i][0];
yy=y+dir[i][1];
if(xx>0&&xx<=m&&yy>0&&yy<=n&&!vis[xx][yy]&&mape[xx][yy]==1)
{
vis[xx][yy]=1;
dfs(xx,yy);
vis[xx][yy]=0;
}
}
}
计算时间复杂度的重点是要知道每一个循环他都干了什么,做了多少次计算,或者他的实际意义是什么。当然,光看理论还是不够的,最最关键的还是要通过实际的联系操作才能掌握。
二.空间复杂度的大致介绍
与时间限制相同,空间限制也是每一道题所必有的数据。空间复杂度,即空间限制,就是算法所需的存储空间大小,而空间消耗主要来自数组的运用(反正我暂时是这么觉得的,动态什么的我了解的还不多)。我们需要从题目所给的空间限制中判断所要开数组的大小,以及是否要对现有算法做空间优化等等。因为实际操作中受限并不大(我感觉,但未来从事计算机行业学会空间优化肯定是必不可少的),所以我不做过多叙述,就我个人经验(雾),二维数组开个3000×3000,一维开个1e7差不多都够了,但具体的还得看题目要求来定,说不定哪个题目就想恶心你一下给你卡个数据,看到MLE的瞬间估计你是崩溃的。
(全局变量的空间允许远大于局部变量,数组单开在外面比较好)。
没错我空间就想(会)写这么多!
总结:时间复杂度代指算法执行时间的长短,空间复杂度代指算法所需存储空间的大小。
一个博客拖了两天写完,还不确定对不对,绝了。