算法入门——时间与空间复杂度

大学初次接触计算机,也是初次接触算法,感觉现在光靠练习来学习还不够,和同级的人差距太大,赶进度会总是忘,就萌生了和以前一样写笔记写感想来加快记忆的方法,奈何字丑字丑字丑,而且事后难以修改,于是有了写博客来记录的决定。(其实是懒,博客提交后好像还可以随便改,也日后方便补充
—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,bfsO(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的瞬间估计你是崩溃的。
(全局变量的空间允许远大于局部变量,数组单开在外面比较好)。
没错我空间就想(会)写这么多!

总结:时间复杂度代指算法执行时间的长短,空间复杂度代指算法所需存储空间的大小。

一个博客拖了两天写完,还不确定对不对,绝了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值