在接下来的数据结构博文中,我们将会开始接触到一些算法,也就是“解决某个问题的方法”,而解决同一个问题总是会存在不同的算法,所以我们需要在不同的算法之中做出抉择,而做出抉择的根据往往就是算法耗费的时间(特殊情形下我们还需要考虑算法耗费的空间)。因此我们今天就来学习如何简单的判断算法将会耗费的时间。

  首先我们知道,同一个算法或者说同一个程序,在不同的计算机上耗费的时间是不一样的,因为不同的计算机硬件运算能力会存在不同。此外,在同一台计算机中,不同的操作,如加法和乘法要消耗的时间也会不同,整数加法和浮点数加法消耗的时间也会不同,因为同样在高级编程语言中只需要一个语句的操作,在计算机语言层面上对应的指令数量、计算机完成的速度也会不同。

  但是因为我们希望讨论的是“算法将要耗费的时间”,而不是完全具体情况下程序将要耗费的时间,所以我们计算算法将要花费的时间时,做不到或者说不应该计算出它具体的时间花费(比如xxx秒)。因此我们计算算法的时间花费时很重要的一点就是:不考虑某个具体操作要消耗的时间,我们将“一个操作”花费的时间统一记为“一个时间单位”,不论这个操作是a=b+c还是a+=b*c。

  接下来我们要明白一个东西,俗称“大O阶”。在讨论大O阶之前,我们要明白一点:我们计算算法将花费的时间时,一般都是计算其可能花费的最大时间。为什么呢?我们假设我们要解决的问题是查找或者排序,那么不论什么算法,在最良好的情况下都可以出现“一查就恰好找到”和“数据本身已经排好序”的情况,而这样作比较是不能比较出算法的“优劣”的,我们希望看到的,就是在最坏的情况下各个算法能够做出怎样的速度。所以我们计算算法消耗的时间,一般都是计算其最坏情况(也有可能计算其平均情况,因为最坏情况下速度不太良好的算法有可能平均情况下是够好的)。

  现在,我们假设这样一个算法:用顺序比较(从第一个开始逐一比较)的方法,在一个大小为n的数组中查找指定数据,若存在则返回true,否则返回false。这个算法的最坏情形显然是不存在指定数据或指定数据为数组最后一个元素,此时算法将执行n次比较操作,也就是算法将耗费n个时间单位。这样的表述当然没有问题,但我们希望能够更简洁的表达这个意思,该怎么办呢?这时候就需要大O阶出场了。

  不难发现,上述算法的最坏情形并不是“固定”的,它取决于n的大小,可以说是一个与n相关的函数,因此我们可以考虑将其记为函数形式:O(n)(基本上算法的最坏情形都不是固定的,而是与数据量有关。因为我们说过算法是处理数据的,如果不论数据多少,执行的操作都是一样多,那这部分操作是不是个算法都不好说)。即O(n)代表着算法在最坏情形下耗费的时间单位,而这个“O()”就是我们所说的大O阶(并不严谨,需继续向下看)。

  知道了我们该计算算法的最坏情形以及什么是大O阶之后,我们现在来试试计算下面这个“算法”的大O阶(关于如何计算一段程序耗费的时间在本文最后简介)

/*从键盘或文件获取n个数据,获取单个数据需要3个操作*/

/*对所有数据进行操作,某些数据的操作为3个,某些数据的操作为4个*/

/*输出或存储所有数据,单个数据需要3个操作*/
/*执行100个额外操作*/

  根据我们之前所说的,任意一个操作(除非这个操作本身是调用一个函数,且该函数花费的时间也与数据有关)均假设花费1个时间单位,上述算法累计有3*n+3*(n-x)+4*x+3*n+100个操作,简单化简的话就是9*n+x+100,由于对数据操作时存在区别对待,所以存在一个不确定的x。

  那么问题来了,我们这时候应该说该算法的大O阶为O(9*n+x+100)吗?显然是不应该的,因为如果要将这样的未知量也算上,也就意味着对于算法中的每个选择结构都得设置单独的未知量,当算法的选择结构很多时,计算算法的时间花费就会很困难。那么对于这样的“未知量”我们该如何处理?很简单,既然大O阶要代表算法的最坏情况,那我们就假设选择结构永远是最坏情况,在上述算法中也就是永远都是对数据执行4个操作而不是3个。这样一来,大O阶的计算就简单了一些,3*n+4*n+3*n+100=10n,为O(10*n+100)。

  现在我们再来看看大O阶中的常数项,乍一看,我们会认为常数项也是计算算法时间花费时必不可少的,可是如果我们仔细想想我们分析算法时间花费的根本目的,就会发现,我们在乎的其实是算法时间花费与数据量n之间的关系。就像前面说的,如果一段程序不论数据量多少都是花费一样多的时间单位,那么这段程序算不算算法都是一个问题。出于这个原因,对于大O阶中的常数项,我们总是选择直接舍弃(不论大小),因此O(10*n+100)又被我们简化为了O(10*n)

  接下来我们再来看看这段“算法”,试着计算它要花费的时间

for(int i=;i<n;++i)
for(int j=;j<n;++j)
/*执行1次操作*/

  不难计算出其耗费的时间单位为n*n即n^2,大O阶为O(n^2)。通过对比我们可以发现,不论O(a*n)的系数a有多大,当数据量n大到一定程度之后,O(n^2)总是比O(a*n)要花费更多时间,简单地说就是n^2的增长率比a*n的要大得多。出于这个原因(以及之前说过的,常数项总是不可避免或者说不是重点考虑的内容),我们对于大O阶中的常数系数也采用省略的做法。所以之前我们计算出的O(10*n)在一般情况下我们也就简记为O(n)。

  讲到这儿,想必大家已经能够猜出我们计算大O阶最重要的一点是什么了,那就是“化简”,尽可能的化简:

  1.去掉常数项

  2.去掉常数系数

  3.只保留最高次项

  因此,假设一个算法消耗时间单位为3*n^3+n^2+9*n+239,我们也将其最坏情形表示为O(n^3)。显然的,这时候的大O阶已经不能较为精确的反映出算法要消耗的时间了,只能说反映出了算法时间“所处的等级”。但是由于在数据量n足够大的时候,“低等级”(如n^2)的算法总是会比“高等级”(如n^3)的算法更快,所以大部分时候我们也只需要在意算法耗费时间“所在的等级”,这也是我们极度精简时间单位的原因。

  同样的,由于极度精简了时间单位,所以大O阶我们也不再称之为“最坏情形下算法花费的时间”了,而是称之为算法的“时间复杂度”。

  那么,常见的算法都有哪些时间复杂度呢?一般按从快到慢有这么一些:

  O(1)(表示常数时间内完成,与数据量无关),O(logN)(底数不重要,一般为2),O(N),O(N*logN),O(N^2),O(N^3)

  我们在日后有可能会见识到相应的算法。

  

  最后,是关于计算一段代码耗费的时间单位时的一般法则:

  1.循环语句

  一次循环的运行时间最多是该循环内语句的运行时间乘以迭代的次数,如

for(int i=;i<n;++i)
{
a[i]=b[i];
b[i]=b[i]+;
}

  其运行时间为2*n,时间复杂度为O(n)

  2.嵌套的循环

  首先记住,从里向外分析循环。嵌套循环总的运行时间为最内部循环语句的运行时间乘以该组所有循环大小的乘积,如

for(int i=;i<n;++i)
for(int j=;j<n;++j)
a[i]+=b[j];

  其运行时间为1*n*n,时间复杂度为O(n^2)

  3.顺序语句

  将各个语句的运行时间求和即可,如

for(int i=;i<n;++i)
a[i]=i;
for(int i=;i<n;++i)
for(int j=;j<n;++j)
b[j]+=a[i];

  其运行时间为n+1*n*n,时间复杂度为O(n^2)

  4.选择语句

  选择语句的运行时间总是不会超过最长运行时间的那种可能,如

if(Condition)
S1
else
S2

  其运行时间总是不会超过S1,S2中的最大者加上判断语句的时间

  上述做法很可能使计算出的运行时间偏高,但可以保证算法不会超过这样的“最坏情况”。

  好了,这篇博文我们就讲这些,因为根据不同的需要,我们对程序的时间复杂度计算也会有不同的要求,而且像递归这样的代码,对其进行时间复杂度分析有时候会有很大困难,所以对于算法时间复杂度的简介,到这里就差不多了。

深入浅出数据结构C语言班(11)——简要介绍算法时间复杂度的更多相关文章

  1. 深入浅出数据结构C语言版(3)——递归简论

      相信学习过C语言的读者都已经接触过递归(不论是谭浩强的C程序设计还是C Primer Plus都有递归程序),本文就是对递归的基本原则进行简要介绍.首先,我们写一个基本的递归函数作为例子: int ...

  2. 深入浅出数据结构C语言版(22)——排序决策树与桶式排序

    在(17)中我们对排序算法进行了简单的分析,并得出了两个结论: 1.只进行相邻元素交换的排序算法时间复杂度为O(N2) 2.要想时间复杂度低于O(N2),算法必须进行远距离的元素交换 而今天,我们将对 ...

  3. 深入浅出数据结构C语言版(2)——简要讨论算法的时间复杂度

    所谓算法的"时间复杂度",你可以将其理解为算法"要花费的时间量".比如说,让你用抹布(看成算法吧--)将家里完完全全打扫一遍大概要5个小时,那么你用抹布打扫家里 ...

  4. 深入浅出数据结构C语言版(5)——链表的操作

    上一次我们从什么是表一直讲到了链表该怎么实现的想法上:http://www.cnblogs.com/mm93/p/6574912.html 而这一次我们就要实现所说的承诺,即实现链表应有的操作(至于游 ...

  5. 深入浅出数据结构C语言版(8)——后缀表达式、栈与四则运算计算器

    在深入浅出数据结构(7)的末尾,我们提到了栈可以用于实现计算器,并且我们给出了存储表达式的数据结构(结构体及该结构体组成的数组),如下: //SIZE用于多个场合,如栈的大小.表达式数组的大小 #de ...

  6. 深入浅出数据结构C语言版(4)——表与链表

    在我们谈论本文具体内容之前,我们首先要说明一些事情.在现实生活中我们所说的"表"往往是二维的,比如课程表,就有行和列,成绩表也是有行和列.但是在数据结构,或者说我们本文讨论的范围内 ...

  7. 深入浅出数据结构C语言版(1)——什么是数据结构及算法

    在很多数据结构相关的书籍,尤其是中文书籍中,常常把数据结构与算法"混合"起来讲,导致很多人初学时对于"数据结构"这个词的意思把握不准,从而降低了学习兴趣和学习信 ...

  8. 深入浅出数据结构C语言版(10)——树的简介

    到目前为止,我们一直在谈论的数据结构都是"线性结构",不论是普通链表.栈还是队列,其中的每个元素(除了第一个和最后一个)都只有一个前驱(排在前面的元素)和一个后继(排在后面的元素) ...

  9. 深入浅出数据结构C语言版(12)——从二分查找到二叉树

    在很多有关数据结构和算法的书籍或文章中,作者往往是介绍完了什么是树后就直入主题的谈什么是二叉树balabala的.但我今天决定不按这个套路来.我个人觉得,一个东西或者说一种技术存在总该有一定的道理,不 ...

随机推荐

  1. C++实现密码强度测试

    最近在博客中看到许多用js写的密码强度检测,我觉得挺有意思的,所以呢我打算自己也写个来玩玩,最可悲的是我还没学js,当然这不重要,所以呢打算用C++来写一个密码强度检测,这里我来给大家说说用JS写的和 ...

  2. NodeJS 实现手机短信验证 模块阿里大于

    1,NodeJS 安装阿里大于模块 切换到项目目录使用npm 安装阿里于模块 npm i node-alidayu --save 2,aliyu官网使用淘宝账户登录 登录阿里大于 https://do ...

  3. python str转dict

    两种方法 捷径 eval(str) >>> user = "{'name' : 'jim', 'sex' : 'male', 'age': 18}" >&g ...

  4. C#继承的执行顺序

    自己对多态中构造函数.函数重载执行顺序和过程一直有些不理解,经过测试,对其中的运行顺序有了一定的了解,希望对初学者有些帮助. eg1: public class A { public A() { Co ...

  5. uwp版的音乐播放器练手

    UWP项目之音乐播放器 这个项目本来是我女朋友的一个小作业,她做不出来,结果只能是我来代劳.经过几天的时间虽然赶出来了,但是自己不是很满意,还有很多不满意的地方,因此决定在最近的一段时间内,重新完成. ...

  6. OpenCV探索之路(十七):Mat和IplImage访问每个像素的方法总结

    在opencv的编程中,遍历访问图像元素是经常遇到的操作,掌握其方法非常重要,无论是Mat类的像素访问,还是IplImage结构体的访问的方法,都必须扎实掌握,毕竟,图像处理本质上就是对像素的各种操作 ...

  7. web开发中前后端传值

    在JavaScript中,页面与页面间的传值需要注意. 比如,我们通过url向下个页面进行传一个数字时,到下个页面进行解析出来后可能是一个字符串.这样会导致一个现象.调试时,发现我要传的值的确传过来了 ...

  8. Maven转化为Dynamic Web Module

    如今Maven仍然是最常用的项目管理工具,若要将Java Web项目使用Maven进行管理,则首先需要新建Maven项目,然后将其转化为web项目. 在项目右键选择properties,然后点击左侧P ...

  9. Mac用ssh登录Ubuntu14.04

    在Ubuntu上配置ssh-server sudo apt-get install openssh-server  然后确认ssh-server是否启动  ps -e | grep ssh 如果存在s ...

  10. golang路上的小学生系列--使用reflect查找package路径

    本文同时发布在个人博客chinazt.cc 和 gitbook 今日看到了一个有趣的golang项目--kolpa(https://github.com/malisit/kolpa). 这个项目可以用 ...