1 认识算法
【算法】是解决特定问题求解步骤的描述,在计算机中表现为指令的有限序列,并且每条指令表示一个或多个操作。
为了解决某个或某类问题,需要把指令表示成一定的操作序列,操作序列包括一组操作,每一个操作都完成特定的功能,这就是算法了。为什么要学习算法呢,有一个典中点的例子:
对于需求【求1到100之间所有整数之和】,我们可以写出两种代码:
代码A
int sum=0,i,n=100;
for(i=1;i<=n;i++)
{
sum+=i;
}
代码B
int sum=0,i=1,n=100;
sum=(i+n)*n/2;
我们知道,代码A循环需要执行100次,代码B只需要执行一次。以一个简单的例子引入,感受学习算法的重要性。
2 算法的特性
算法具有5个基本特性:输入、输出、有穷性、确定性和可行性。
2.1 输入输出
算法具有零个或多个输入,算法至少有一个或多个输出。例如算法打印 “Hello word!”,不需要输入任何参数;算法是一定要有输出的,不然没有意义。
2.2 有穷性
有穷性指算法在执行有限步骤后,自动结束而不会出现无限循环,并且每一个步骤在可接受的时间内完成。
2.3 确定性
算法的每一步骤都具有确定的含义,不会出现二义性。算法在一定条件下,只有一条执行路径,相同的输入只能有同样的输出结果。算法的每个步骤被精确定义而无歧义。
2.4 可行性
一个算法的每一步操作与要求应该是算法执行者(人或机器)可以实施的,同时在现实环境中能够做到并且能在有限的时间内完成。
3 算法设计的要求
同一个问题,可以有多个解决问题的算法,掌握好的算法对于我们解决问题很有帮助。
算法设计的要求:正确性、可读性、健壮性、时间效率高、存储量低。
3.1 正确性
能正确反映问题的需求,能得到问题的正确结果,正确考虑了问题的边界条件。
3.2 可读性
个人理解一方面是要写好注释、写好文档;一方面是算法过程逻辑清晰。
3.3 健壮性
当输入数据不合法时,算法也能做出相关处理,而不是产生异常或莫名其妙的结果。
4 算法效率的度量方法
4.1 事后统计方法
利用计算机计数器对不同算法编制的程序的运行时间进行比较,从而确定算法效率的高低。
事后统计法具有很大的缺陷:软件和机器的不同会对运行时间产生影响;把程序编制出来后再测试算法的效率,如果发现算法不好,时间就浪费了。
4.2 事前分析估算方法
【事前分析估算方法】在计算机程序编制前,依据统计方法对算法进行估算。
一个用高级程序语言编写的程序在计算机上运行时所消耗的时间取决于下列因素:
算法采用的策略、方法。编译产生的代码质量。问题的输入规模。机器执行指令的速度。
第1条是算法好坏的根本,第2条要由软件来支持,第4条主要看硬件性能。抛开这些与计算机软、硬件有关的因素,一个程序的运行时间,依赖于算法的好坏和问题的输入规模。所谓问题输入规模是指输入量的多少。我们在分析一个算法的运行时间时,重要的是把基本操作的数量与输入规模关联起来,即基本操作的数量必须表示成输入规模的函数。
5 算法时间复杂度
在进行算法分析时,语句总的执行次数T(n)是关于问题规模n的函数,进而分析T(n)随n的变化情况并确定T(n)的数量级。算法的时间复杂度,也就是算法的时间量度,记做T(n)=O(f(n))。它表示随问题规模n的增大,算法执行时间的增长率和f(n)的增长率相同,称做算法的渐进时间复杂度,简称为时间复杂度。其中f(n)是问题规模的某个函数。
这样用大写O()来体现算法时间复杂度的记法,称之为大O记法。
5.1 推导大O阶方法
用常数1取代所有加法常数。在修改后的运行次数函数中,只保留最高阶项。如果最高阶存在且不是1,则去除与这个项相乘的常数。
得到的结果就是大O阶。
简单来说,就是首先理清楚算法一共执行了多少次,在运行次数函数中寻找最高阶项并除去其他所有项,将最高阶项的系数变为常数1。因此大O阶只存在类似以下这几种:O(1)、O(n)、O(n^2)、O(n*n^2)、O(logn);而不会有以下几种:O(3)、O(2n)、O(n^2+n),它只能有一项,并且这系数为1。
下面列举几种常见大O阶例子。
5.1.1常数阶
int sum=0,n=100; /*执行一次*/
sum=(1+n)*n/2; /*执行一次*/
printf("%d",sum); /*执行一次*/
这个算法的时间复杂度是O(1),而不是O(3)哦,时间复杂度为O(1)不是说算法只用运行1次。这种与问题大小无关(n的多少),执行时间恒定的算法,称之为具有O(1)的时间复杂度,又叫常数阶。
5.1.2线性阶
int i;
for(i=0;i { /*算法时间复杂度为O(1)的程序步骤序列*/ } 这个算法,循环体必须要执行n次,时间复杂度为O(n)。 5.1.3对数阶 对数阶的介绍是从一个非常典型的算法例子推导而来的: int count=1; while (count { count=count*2; /*时间复杂度为O(1)的程序步骤序列*/ } 由于每次count乘以2后,就距离n更近了一点。也就是说,有多少个2相乘后大于n,则会退出循环。由于2^x=n得到x=log2(n),所以这个循环要执行log2(n)次,时间复杂度为O(logn)。为什么是O(logn)而不是O(log2(n))呢?因为log2(n)=logn/log2,将系数1/log2变为1,就是 O(logn)了。 5.1.4平方阶 平方阶一般出现在循环嵌套中 int i,j; for(i=0;i { for(j=0;j { /*时间复杂度为O(1)的程序步骤序列*/ } } 外层循环每执行一次,内层循环都要执行n次,外层循环一共要执行n次,那么循环嵌套一共要执行n^2次,时间复杂度为O(n^2) 如果外循环的次数变为m,时间复杂度就变成O(m*n): int i,j; for(i=0;i { for(j=0;j { /*时间复杂度为O(1)的程序步骤序列*/ } } 寻找算法的执行次数还是需要一定的数学基础的。 5.2 常见的时间复杂度 12O(1)2n+3O(n)3n^2+2n+1O(n^2)5log2(n)+20O(logn)2n+3nlog2(n)+19O(nlogn)6n^3+2n^2+3n+4O(n^3)2^nO(2^n) 常用的时间复杂度所耗费的时间从小到大依次是: O(1) 6 算法空间复杂度 算法的空间复杂度通过计算算法所需的存储空间实现,算法空间复杂度的计算公式记作:S(n)=O(f(n)),其中,n为问题规模,f(n)为语句关于n所占存储空间。 我们在写代码时,完全可以用空间来换取时间,比如说,要判断某某年是不是闰年,可以编写出一串代码,每个一个年份,通过计算得出是否是闰年的结果;也可以事先建立一个有2050个元素的数组,然后把所有的年份按下标的数字对应,如果是闰年,次数组项的值就是1,如果不是则为0,这样,所谓的判断某一年是不是闰年,就变成了查找这个数组的某一项的值是多少的问题。 课本对算法空间复杂度的介绍比较少。 7 说明 本文是笔者在学习数据结构与算法过程中所作的内容梳理,既是为了方便自己复习,也希望能够帮助到其他正在学习的朋友。笔者学习的书籍是由程杰著、清华大学出版社出版的《大话数据结构》,本文使用的语言和例子都来自该书。作为一个编程小白,笔者认为这本书对初学者非常友好,语言活泼生动,引人入胜;同时,作为一个编程小白,笔者知识水平有限,文中不免会出现部分描述错误。如果有感到困惑的地方,可能就是本文描述不当所致,请移步至其他优秀的博文中寻求解答。 好文阅读
发表评论