算法算法
算法是解决特定问题求解步骤的描述,在计算机中表现为指令的有限序列,并且每条指令表示一个或多个操做。编程
算法设计的要求:应该具备正确性。可读性、健壮性、高效率和低存储量的特征。小程序
你们都已经学过一门计算机语言,无论学的是哪种,学得好很差,好歹是能够写点小程序了。如今我要求你写一个求 1+2+3+...+100结果的程序,你应该怎么写呢?数组
大多数人会立刻写出下面的C语言代码(或者其余语言的代码):框架
int i, sum=0, n=100; for (int i=1; i<=n; i++){ sum += i; } print("%d",sum);
这是最简单的计算机程序之一,它就是一种算法,我不去解释着代码的含义了。问题在于,你的第一直觉是这样写的,单这样是否是真的很好?是否是最高效?函数
而高斯的解法是: 工具
用程序来实现以下: 测试
神童就是神童,他用的方法至关于另外一种求等差数列的算法,不只仅能够用于1加到100,就是加到一千、一万、一亿(须要更改整型变量类型为长整型,不然会溢出),也就是瞬间之事。单若是用刚才的程序,显然计算机要循环一千、一万、一亿次的加法运算。人脑比电脑算得快,彷佛成为了现实。操作系统
算法具备五个基本特性:输入、输出、有穷性,肯定性和可行性。设计
输入和输出特性比较容易理解,算法具备零个或多个输入。尽管对于绝大多数算法来讲,输入参数都是必要的,但对于个别状况,如打印"hello word!"这样的代码,不须要任何输入参数,所以算法的输入能够是零个。算法至少有一个或多个输出,算法是必定须要输出的,不须要输出,你用这个算法干嘛?输出的形式能够是打印输出,也能够是返回一个或多个值等。
**有穷性:指算法在执行有限的步骤以后,自动结束而不会出现无限循环,而且每个步骤在可接受的时间内完成。**现实中常常会写出死循环的代码,这就是不知足有穷性。固然这里有穷的概念并非纯数学意义的,而是在实际应用当中合理的、能够接受的"有边界"。你说你写一个算法,计算机须要算上个二十年,必定会结束,它在数学意义上是有穷了,能够媳妇都熬成婆了,算法的意义也就不大了。
**肯定性:算法的每一步骤都具备肯定的含义,不会出现二义性。**算法在必定条件下,只有一条执行路径,相同的输入只能有惟一的输出结果。算法的每一个步骤被精肯定义而无歧义。
**可行性:算法的每一步都必须是可行的,也就是说,每一步都可以经过执行有限次数完成。**可行性意味着算法能够转换为程序上机运行,并获得正确的结果。尽管目前计算机界也存在那种没有实现的极为复杂的算法,不是说理论上不能实现,而是由于过于复杂,咱们当前的编程方法、工具和大脑限制了这个工做,不过这都是理论研究领域的问题,不属于咱们如今要考虑的范围。
正确性:算法的正确性是指算法至少应该具备输入、输出和加工处理无歧义性,能正确反映问题的需求、可以获得问题的正确答案。
可是算法的"正确"一般在用法上有很大的差异,大致分为如下四个层次。
对于这四层含义,层次1要求最低,可是仅仅没有语法错误实在谈不上是好算法。这就如同仅仅解决温饱,不能算是生活幸福同样。而层次4是最困难的,咱们几乎不可能逐一验证全部的输入都获得正确的结果。
所以算法的正确性在大多数状况下都不可能用程序来证实,而是用数学方法证实的。证实一个复杂算法在全部层次上都是正确的,代价很是昂贵。因此通常状况下,咱们把层次3做为一个算法是否正确的标准。
好算法还有什么特征呢?
可读性:算法设计的另外一目的是为了便于阅读、理解和交流。
健壮性:当输入数据不合法时,算法也能作出相关处理,而不是产生异常或莫名其妙的结果。
时间效率指的是算法的执行时间,对于同一个问题,若是有多个算法可以解决,执行时间短的算法效率高,执行时间长的效率低。存储量需求指的是算法在执行过程当中须要的最大存储空间,主要指算法程序运行时所占用的内存或外部硬盘存储空间。设计算法应该尽可能知足时间效率高和存储量低的需求。在生活中,人们都但愿花最少的钱,用最短的时间,办最大的事,算法也是同样的思想,最好用最少的存储空间,花最少的时间,办成统一的事就是好的算法。
过后统计方法:这种方法主要是经过设计好的测试程序和数据,利用计算机计时器对不一样算法编制的程序的运行时间进行比较,从而肯定算法的效率的高低。
可是这种方法显然是有很大缺陷的:
基于过后统计方法有这样那样的缺陷,咱们考虑不予采纳。
事前分析估算方法:在计算机程序编制前,依据统计方法对算法进行估算。 抛开这些与计算机硬件、软件有关的因素。一个程序的运行时间,依赖于算法的好坏和问题的输入规模。所谓问题输入规模是指输入量的多少。
咱们来看看今天刚上课时举的例子,两种求和的算法: 显然,第一种算法,执行了1+(n+1)+n+1次=2n+3次;而第二种算法,是1+1+1=3次。事实上两个算法的第一条和最后一条语句是同样的,因此咱们关注的代码实际上是中间那部分,咱们把循环看做一个总体,忽略头尾循环判断的开销,那么这两个算法其实就是n次和1次的差距。算法好坏显而易见。
咱们再来延伸一下上面这个例子: 这个例子中,i从1到100,每次都要让j循环100次,而当中的x++和sum=sum+x;其实就是1+2+3+...+10000,也就是100的平方次,因此这个算法当中,循环部分的代码总体须要执行n的平方(忽略循环体头尾的开销)次。显然这个算法的执行次数对于一样的输入规模n=100,要多于前面两种算法,这个算法的执行时间随着n的增长也将远远多于前面两个。
此时你会看到,测定运行时间最可靠的方法就是计算对运行时间有消耗的基本操做的执行次数。运行时间和这个计数成正比。
咱们不关心编写程序所用的程序设计语言是什么,也不关心这些程序将跑在什么样的计算机中,咱们只关心它所实现的算法。这样,不计那些循环索引的递增和循环终止条件、变量声明、打印结果等操做,最终,在分析程序的运行时间时,最重要是把程序当作是独立于程序设计语言的算法或一系列步骤。
某个算法,随着n的增加,它会愈来愈优于另外一算法,或者愈来愈差于另外一算法。这其实就是事前估算方法的理论依据,经过算法时间复杂度来估算算法时间效率。
在进行算法分析时,语句总的执行次数T(n)是关于问题规模n的函数,进行分析T(n)随n的变化状况并肯定T(n)的数量级。算法的时间复杂度,也就是算法的时间量度,记做:T(n)=O(f(n))。它表示随问题规模n的增大,算法执行时间的增加率和f(n)的增加率相同,称做算法的渐近时间复杂度,简称为时间复杂度。其中f(n)是问题规模n的某个函数。
咱们要分析算法的复杂度,关键就是要分析循环结构的运行状况。
首先顺序结构的时间复杂度。下面这个算法,也就是刚才的第二种算法(高斯算法),为何时间复杂度不是O(3),而是O(1)。 这个算法的运行次数函数是f(n)=3.根据咱们推到大O阶的方法,第一步就是把常数项3改成1.在保留最高阶项时发现,它根本没有最高阶项,因此这个算法的时间复杂度为O(1)。
另外,咱们试想一下,若是这个算法当中的语句 sum=(1+n)*n/2有10句,即:
事实上不管n为多少,上面的两段代码就是3次和12次执行的差别。这种与问题的大小无关(n的多少),执行时间恒定的算法,咱们称之为具备O(1)的时间复杂度,又叫常数阶。
注意:无论这个常数是多少,咱们都记做O(1),而不能是O(3)、O(12)等其余任何数字,这是初学者经常犯的错误。
对于分支结构而言,不管是真,仍是假,执行的次数都是恒定的,不会随着n的变大而发生变化,因此单纯的分支结构(不包含在循环结构中),其时间复杂度也是O(1)。
线性阶的循环结构会复杂不少。要肯定某个算法的阶次,咱们经常须要肯定某个特定语句或某个语句集运行的次数。所以,咱们要分析算法的复杂度,关键就是要分析循环结构的运行状况。
下面这段代码,它的循环的时间复杂度为O(n),由于循环体中的代码须要执行n次。
下面的这段代码,时间复杂度又是多少呢? 因为每次count乘以2之后,就距离n更近了一分。也就是说,有多少个2相乘后大于n,则会退出循环。由2^x=n获得x=log2n。因此这个循环的时间复杂度为O(logn)。
下面例子是一个循环嵌套,它的内循环刚才咱们已经分析过,时间复杂度为O(n)。 而对于外层的循环,不过是内部这个时间复杂度为O(n)的语句,再循环n次。因此这段代码的时间复杂度为O(n^2)。
若是外循环的循环次数改成了m,时间复杂度就变为O(m*n)。 因此咱们能够总结得出,循环的时间复杂度等于循环体的复杂度乘以该循环运行的次数。
那么下面这个循环嵌套,它的时间复杂度是多少呢? 因为当i=0时,内循环执行了n次,当i=1时,执行了n-1次,...当i=n-1时,执行了1次。因此总的执行次数为:
用咱们推导大O阶的方法,第一条,没有加法常数不予考虑;第二条,只保留最高阶项,所以保留n^2/2;第三条,去除这个项相乘的常数,也就是去除1/2,最终这段代码的时间复杂度为O(n^2)。
从这个例子,咱们也能够获得一个经验,其实理解大O推导不算难,难的是对数列的一些相关运算,这更多的是考察你的数学知识和能力,因此想考研的朋友,要想在求算法时间复杂度这里不失分,可能须要强化你的数学,特别是数列方面的知识和解题能力。
咱们继续看例子,对于方法调用的时间复杂度又如何分析。 上面这段代码调用一个函数function。
函数体是打印这个参数。其实这很好理解,function函数的时间复杂度是O(1)。因此总体的时间复杂度为O(n)。
假如function是下面这样的: 事实上,这和刚才举的例子是同样的,只不过把嵌套内循环放到了函数中,因此最终的时间复杂度为O(n^2)。
下面这段相对复杂的语句: 它的执行次数
根据推导大O阶的方法,最终这段代码的时间复杂度也是O(n^2)。
常见的时间复杂度如表2-10-1所示。 经常使用的时间复杂度所耗费的时间从小到大依次是:
]
咱们前面已经谈到了O(1)常数阶、O(logn)对数阶、O(n)线性阶、O(n^2)平方阶等,至于O(nlogn)咱们将会在从此的课程中介绍,而像O(n^3),过大的n都会使得结果变得不现实。一样指数阶O(2^n)和阶乘O(n!)等除非是很小的n值,不然哪怕n只是100,都是噩梦般的运行时间。因此这种不切实际的算法时间复杂度,通常咱们都不去讨论它。
你早晨上班出门后忽然想起来,手机忘记带了,这年头,钥匙、钱包、手机三大件,出门哪样也不能少呀。因而回家找。打开门一看,手机就在门口玄关的台子上,原来是出门穿鞋时忘记拿了。这固然是比较好,基本没花什么时间寻找。可若是不是放在那里,你就得进去处处找,找完客厅找卧室、找完卧室找厨房、找完厨房找卫生间,就是找不到,时间一分一秒的过去,你忽然想起来,能够用家里座机打一下手机,听着手机铃声来找呀,真是笨。终于找到了,在床上枕头下面。你再去上班,迟到。见鬼,这一年的全勤奖,就由于找手机给黄了。
找东西有运气好的时候,也有怎么也找不到的状况。但在现实中,一般咱们碰到的绝大多数既不是最好的也不是最坏的,因此算下来是平均状况居多。
算法的分析也是相似,咱们查找一个有n个随机数字数组中的某个数字,最好的状况是第一个数字就是,那么算法的时间复杂度为O(1),但也有可能这个数字就在最后一个位置上待着,那么算法的时间复杂度就是O(n),这是最坏的一种状况了。
最坏状况运行时间是一种保证,那就是运行时间将不会再坏了。在应用中,这是一种最重要的需求,一般,除非特别指定,咱们提到的运行时间都是最坏状况的运行时间。
而平均运行时间也就是从几率的角度看,这个数字在每个位置的可能性是相同的,因此平均的查找时间为n/2次后发现这个目标元素。
平均运行时间是全部状况中最有意义的,由于它是指望的运行时间。也就是说,咱们运行一段程序代码时,是但愿看到平均运行时间的。可现实中,平均运行时间很难经过分析获得,通常都是经过运行必定数量的实验数据后估算出来的。
对算法的分析,一种方法是计算全部状况的平均值,这种时间复杂度的计算方法称为平均时间复杂度。另外一种方法是计算最坏状况下的时间复杂度,这种方法称为最坏时间复杂度。通常在没有特殊说明的状况下,都是指最坏时间复杂度。
咱们在写代码时,彻底能够用空间来换取时间,好比说,要判断某某年是否是闰年,你可能会花一点心思写了一个算法,并且因为是一个算法,也就意味着,每次给一个年份,都是要经过计算获得是不是闰年的结果。还有另外一个办法就是,事先创建一个有2050个元素的数组(年数略比现实多一点),而后把全部的年份按下标的数字对应,若是是闰年,此数组项的值就是1,若是不是值为0.这样,所谓的判断某一年是不是闰年,就变成了查找这个数组的某一项的值是多少的问题。此时,咱们的运算是最小化了,可是硬盘上或内存中须要存储着2050个0和1。
这是经过一笔空间上的开销来换取计算时间的小技巧。到底哪个好,其实要看你用在什么地方。
算法的空间复杂度经过计算算法所需的存储空间实现,算法空间复杂度的计算公式记为:S(n)=O(f(n)),其中,n为问题的规模,f(n)为语句关于n所占存储空间的函数。