算 法 :
算法是解决特定问题求解步骤的描述,在计算机中表现为指令的有限序列,并且每条指令表示一个或多个操作。
2.1 开场白
各位同学大家好。
上次上完课后,有同学对我说,老师,我听了你的课,感觉数据结构没什么的,你也太夸大它的难度了。 是呀,我好像是强调了数据结构比较动脑子 ,而上次课,其实还没拿出复杂的东西来说道。不是不想,是没必要,第一次课就把你们糊弄晕,那以后还玩什么,逃课的不就更多了吗?你们看,今天来的人数和第一次差不多,而且暂时还没有睡觉的。今天我们介绍的内容在难度上就有所增加了,做好准备了吗?
2.2 数据结构与算法关系
我们这门课程叫数据结构,但很多时候我们会讲到算法,以及它们之间的关系。市场上也有不少书叫"数据结构与算法分析'这样的名字。
有人可能就要问了,那你到底是只讲数据结构呢,还是和算法一起讲?它们之间是什么关系呢? 干吗要放在一起?这问题怎么回答。打个比方吧,今天是你女友生日,你打算请女友去看爱情音乐剧,到了戏院,抬头一看一一 《梁山伯》 18 : 00 开演。 嗯,怎么会是这样? 一问才知,今天饰演祝英台的演员生病,所以梁山伯唱独角戏。 真是搞笑了,这还有什么看头。于是你们打算去看爱情电影。到了电影院 , 一看海报一一 《罗密欧》,是不是名字写错了,问了才知,原来饰演朱丽叶的演员因为嫌弃演出费用大低,中途退演了。制片方考虑到已经开拍,于是就把电影名字定为 《罗密欧》,主要讲男主角的心路旅程。哎,这电影还怎么啊?
事实上,数据结构和算法也是类似的关系。只谈数据结构 , 当然是可以,我们可以在很短的时间就把几种重要的数据结构介绍完。听完后,很可能你没什么感觉,不知道这些数据结构有何用处 。 但如果我们再把相应的算法也拿来讲一讲,你就会发现,甚至开始感慨:哦,计算机界的前辈们,的确是一些很牛很牛的人,他们使得很多看似很难解决或者没法解决的问题,变得如此美妙和神奇。
也许从这以后,慢慢地你们中的一些人会开始把你们的崇拜对象, 从帅哥美女、什么 "哥" 什么"姐"们,转移到这些大胡子或者秃顶的老头身上,那我就非常欣慰了。而且,这显然是一种成熟的表现,我期待你们中多一点这样的人,这样我们国家的软件行业,也许就有得救了。
不过话说回来,现在好多大学里,通常都是把 "算法" 分出一门课单独讲的 ,也就是说,在《数据结构》课程中,就算谈到算法,也是为了帮助理解好数据结构,并不会详细谈及算法的方方面面 。 我们的课程也是按这样的原则来展开的。
2.3 两种算法的比较
大家都已经学过一门计算机语言,不管学的是哪一种,学得好不好,好歹是可以写点小程序了。现在我要求你写一个求 1+2+3+…… + 100 结果的程序,你应该怎么写呢?
大多数人会马上写出下面的 C 语言代码(或者其他语言的代码) :
int i , sum = 0, n=100;
for (i = 1; i < = n; i++)
sum = sum + i;
printf ("%d" , sum) ;
int sum = 0, n=100;
for (int i = 1; i < = n; i++)
sum = sum +i ;
System.out.println(sum);
这是最简单的计算机程序之一,它就是一种算法,我不去解释这代码的含义了。问题在于,你的第一直觉是这样写的,但这样是不是真的很好?是不是最高效?
此时,我不得不把伟大数学家高斯的童年故事拿来说一遍,也许你们都早已经听过,但不妨再感受一下,天才当年是如何展现天分和才华的。
据说 18 世纪生于德国小村庄的高斯, 上小学的一天,课堂很乱,就像我们现在下面那些窃窃私语或者拿着手机不停摆弄的同学一样, 老师非常生气 , 后果自然也很严重。于是老师在放学时,就要求每个学生都计算 1+2+…+100 的结果,谁先算出来谁先回家。
天才当然不会被这样的问题难倒,高斯很快就得出了答案,是 5050。老师非常惊讶,因为他自己想必也是通过 1+2=3 , 3+3=6, 6+4=10,……, 4950+100=5050这样算出来的,也算了很久很久。说不定为了怕错,还算了两三遍。可眼前这个少年 , 为何可以这么快地得出结果?
高斯解释道:
所以 sum=5050
用C程序来实现如下:
int 1, sum = 0,n = 100:
sum = (1 + n) * n /2;
printf ("%d" , sum) ;
int 1, sum = 0,n = 100:
sum = (1 + n) * n /2;
System.out.println(sum);
神童就是神童,他用的方法相当于另一种求等差数列的算法,不仅仅可以用于 1加到 100 ,就是加到一千、一万、一亿(需要更改整型变量类型为长整型,否则会溢出) ,也就是瞬间之事。但如果用刚才的程序,显然计算机要循环一千、 一万、一亿次的加法运算。人脑比电脑算得快,似乎成为了现实。
2.4 算法定义
什么是算法呢?算法是描述解决问题的方法。算法 ( Algorithm) 这个单词最早出现在波斯数学家阿勒·花刺子密在公元 825 年(相当于我们中国的唐朝时期)所写的《印度数字算术》中。 如今普遍认可的对算法的定义是 :算法是解决特定问题求解步骤的描述,在计算机中表现为指令的有限序列,并且每条指令表示一个或多个操作。
刚才的例子我们也看到,对于给定的问题,是可以有多种算法来解决的。
那我就要问问你们,有没有通用的算法呀?这个问题其实很弱智,就像问有没有可以包治百病的药呀!现实世界中的问题千奇百怪,算法当然也就千变万化,没有通用的算法可以解决所有的问题。甚至解决一个小问题,很优秀的算法却不一定适合它。
算法定义中,提到了指令,指令能被人或机器等计算装置执行。它可以是计算机指令,也可以是我们平时的语言文字。
为了解决某个或某类问题,需要把指令表示成一定的操作序列,操作序列包括一组操作,每一个操作都完成特定的功能,这就是算法了 。
2.5 算法的特性
2.5.1 输入输出
输入和输出特性比较容易理解, 算法具有零个或多个输入。尽管对于绝大多数算法来说,输入参数都是必要的,但对于个别情况,如打印 "hello world ! " 这样的代码,不需要任何输入参数 , 因此算法的输入可以是零个。 算法至少有一个或多个输出, 算法是一定需要输出的,不需要输出,你用这个算法干吗?输出的形式可以是打印输出,也可以是返回一个或多个值等.
2.5.2 有穷性
有穷性:指算法在执行有限的步骤之后,自动结束而不会出现无限循环,并且每一个步骤在可接受的时间内完成。现实中经常会写出死循环的代码,这就是不满足有穷性。当然这里有穷的概念并不是纯数学意义的,而是在实际应用当中合理的、可以接受的"有边界"。你说你写一个算法,计算机需要算上个二十年,一定会结束,它在数学意义上是有穷了,可是媳妇都熬成婆了,算法的意义也不就大了。
2.5.3 确定性
确定性:算法的每一步骤都具有确定的含义,不会出现二义性 。 算法在一定条件下,只有一条执行路径,相同的输入只能有唯一的输出结果。算法的每个步骤被精确定义而无歧义。
2.5.4 可行性
可行性:算法的每一步都必须是可行的 , 也就是说,每一步都能够通过执行有限
次数完成。 可行性意味着算法可以转换为程序上机运行,并得到正确的结果。尽管在目前计算机界也存在那种没有实现的极为复杂的算法,不是说理论上不能实现 , 而是因为过于复杂,我们当前的编程方法 、 工具和大脑限制 了这个工作,不过这都是理论研究领域的问题,不属于我们现在要考虑的范围 。
2.6 算法设计的要求
刚才我们谈到了,算法不是唯一的。也就是说,同一个问题,可以有多种解决问题的算法 。 这可能让那些常年只做有标准答案题目的同学失望了, 他们多么希望存在标准答案 , 只有一个是正确的,把它背下来,需要的时候套用就可以了。不过话说回来 , 尽管算法不唯一 , 相对好的算法还是存在的。掌握好的算法,对我们解决问题很有帮助 , 否则前人的智慧我们不能利用,就都得自己从头研究了 。 那么什么才叫好的算法呢?
嗯,没错, 有同学说,好的算法,起码要是正确的,连正确都谈不上,还谈什么别的要求?
2.6.1 正确性
正确性:算法的正确性是指算法至少应该具有输入、输出和加工处理无歧义性、能正确反映问题的需求、能够得到问题的正确答案。
但是算法的"正确"通常在用法上有很大的差别,大体分为以下四个层次。
1. 算法程序没有语法错误。
2. 算法程序对于合法的输入数据能够产生满足要求的输出结果。
3. 算法程序对于非法的输入数据能够得出满足规格说明的结果。
4. 算法程序对于精心选择的,甚至刁难的测试数据都有满足要求的输出结果。
对于这四层含义,层次 1 要求最低,但是仅仅没有语法错误实在谈不上是好算法 。 这就如同仅仅解决饱, 不能算是生活幸福一样。 而层次 4 是最困难的,我们几乎不可能逐一验证所有的输入都得到正确的结果 。
因此算法的正确性在大部分情况下都不可能用程序来证明,而是用数学方法证明的。证明一个复杂算法在所有层次上都是正确的,代价非常昂贵。所以一般情况下,我们把层次 3 作为一个算法是否正确的标准。
好算法还有什么特征呢?
很好,我听到了说算法容易理解。 没错 , 就是它 。
2.6.2 可读性
可读性 : 算法设计的另一目的是为了便于阅读、 理解和交流。
可读性高有助于人们理解算法,晦涩难懂的算法往往隐含错误,不易被发现 , 并且难于调试和修改。
我在很久以前曾经看到过一个网友写的代码,他号称这程序是"用史上最少代码实现俄罗斯方块" 。 因为我自己也写过类似的小游戏程序,所以想研究一下他是如何写的 。 由于他追求的是"最少代码" 这样的致,使得他的代码真的不好理解。 也许除了计算机和他自己,绝大多数人是看不懂他的代码的。我们写代码的目的,一方面是为了让计算机执行,但还有一个重要的目的是为了便于他人阅读 , 让人理解和交流, 自己将来也可能阅读,如果可读性不好,时间长了自己都不知道写了些什么。 可读性是算法(也包括实现它的代码)好坏很重要的标志。
2.6.3 健壮性
一个好的算法还应该能对输入数据不合法的情况做合适的处理 。 比如输入的时间或者距离不应该是负数等。
健壮性:当输入数据不合法时,算法也能做出相关处理, 而不是产生异常或莫名其妙的结果。
2.6.4时间效率高和存储量低
最后,好的算法还应该具备时间效率高和存储虽低的特点。
时间效率指的是算法的执行时间 , 对于同一个问题,如果有多个算法能够解决 ,执行时间短的算法效率高,执行时间长的效率低。 存储量需求指的是算法在执行过程中需要的最大存储空间, 主要指算法程序运行时所占用的内存或外部硬盘存储空间。
设计算法应该尽量满足时间效率高和存储量低的需求。 在生活中,人们都希望花最少的钱,用最短的时间 , 办最大的事,算法也是一样的思想,最好用最少的存储空间 ,花最少的时间,办成同样的事就是好的算法。求 100 个人的高考成绩平均分 , 与求全省的所有考生的成绩平均分在占用时间和内存存储上是有非常大的差异的 ,我们自然是追求可以高效率和低存储量的算法来解决问题。
综上,好的算法,应该具有正确性 、可读性、健壮性 、高效率和低存储量的特征。