夸张点说,如果有人因为说过一句话而被授予图灵奖、计算机科学教育杰出贡献奖、Emanual Piore奖和计算机先驱奖,您是不是很惊讶!真的吗?
是真的,此人便是尼古拉斯·沃斯(Nicklaus Wirth) ,此话就是他提出的著名公式“ 算法 + 数据结构 = 程序 ”。该公式对计算机科学的影响程度足以与经典物理学中爱因斯坦的质能方程式“E = MC^2 ”相媲美,一个公式展示出了计算机程序的本质。
当然,如果说尼古拉斯·沃斯的贡献仅限于此,那么就显得非常狭隘了。因为他又被誉为Pascal语言之父,也是Algol、W、Modula、Modula-2、Oberon和Euler等语言的发明人和主设计师,还撰写了《系统程序设计导论》、《算法 数据结构=程序》、《算法和数据结构》等多部学术著作,此处就不做赘述了。
1、何谓算法
那么,究竟什么是算法(Algorithm)呢?
从字面上理解,即用于计算的方法,我们使用该方法可以获得预期的计算结果。
较为专业且被业界广泛认可的定义:算法是模型分析的一组可行的、确定的和有穷的规则。
不论何种定义,通俗的讲,算法是针对待解决问题之解题方案的准确而完整的描述,即解题步骤,它代表着用系统的方法描述解决问题的策略机制。从计算机程序设计的角度看,它由一系列解决问题的清晰指令构成,且能够根据规范的输入,在有限时间内获得所要求的输出。
问题不同,算法可能不同
如果一个算法有缺陷,或者不适用于待解决问题,执行该算法将不会获得预期的计算结果,此问题也就不能被按要求解决。比如,我们开发一款LBS社交应用,根据上线用户的定位坐标,快速计算每个用户附近的人以作好友推荐。简单效率的做法是,把用户经纬度转换为长度后,直接采用勾股定理求c边长度即可。因为用户不会关注和某人的距离已经精确到小数点后几位,更期望看到周围有多少人,大概距离是多长。但是,同样是求距离问题,如果要计算迫击炮弹的飞行距离,我们更应该用抛物线方程式而不是勾股定理,因为炮手更关注炮弹的飞行时长和精确的落弹点位置。
问题相同,算法可能不同
另外,解决相同的问题,可以采用不同的算法,但其耗费时间、空间或计算效率可能不同。比如,求2的n次方,可以用位移运算,也可以用乘法运算,甚之您可以用加法运算。此景犹如八仙过海各显神通,那么,到底哪位神仙更厉害呢?因此,我们需要一些研判指标来衡量一个算法的优劣,如空间复杂度与时间复杂度,我们在后续章节详述。
2、算法的特征
一个典型的算法具备五个特征:有穷性、确切性、输入、输出和可行性
有穷性
算法的指令和执行的次数是有限的,执行时间是有限的。确切性
算法的每一条指令或者步骤都必须有明确的定义和描述。输入
一个算法应该有相应的输入条件,用来描述运算对象的初始状态。输出
一个算法应该有明确的输出结果。因为,没有得到预期结果的算法是无意义的。算法的执行步骤必须是可行的,且可以在有限的时间内完成。
3、算法的发展历史
在中国,最早可以追溯到公元前1世纪的《周髀算经》,它是算经的十书之一,原名《周髀》,主要阐述古代中国的盖天说和四分历法。唐朝的时候,此书被定为国子监明算科的教材之一,并改名为《周髀算经》。《周髀算经》中记载了勾股定理、开平方、等差级数问题等,其中用到了相当复杂的分数算法和开平方算法。在随后的发展中,出现了割圆术、秦九韶算法和剩余定理等一些经典算法。算法在我国古代称为“演算法”。
在西方,公元9世纪波斯数学家 al-Khwarizmi 提出了算法的概念。“算法”最初写为algorism,意思是采用阿拉伯数字的运算法则。到了18世纪,正式被命名为 algorithm 。由于汉字计算的不方便,导致我国古代算法的发展比较缓慢,而采用阿拉伯数字的西方国家则发展迅速。例如,著名的欧几里得算法,又称为辗转相除法,就是典型的算法。
在历史上,Ada Byrom被认为是第一个程序员。她在1842年编写的巴贝奇分析机上的伯努利方程的求解算法程序虽然未能执行,但是奠定了计算机算法程序设计的基础。
后来,随着电子计算机的发展,实现各种算法已经成为可能,算法在计算机程序设计领域又得到了快速发展。目前,几乎所有的程序员编程时,不论采用何种语言,都需要和算法打交道。
4、算法的分类
按照不同的应用和特性,算法被分为不同的类别。
4.1、按照应用领域分类
按照算法的应用领域,即解决的问题领域,算法可以分为基本算法、数据结构相关的算法、几何算法、图论算法、规划算法、数值分析算法、加密/解密算法、排序算法、查找算法、并行算法和数论算法等等。
4.2、按照确定性分类
按照算法结果的确定性分类,算法可以分为确定性算法和非确定性算法。
确定性算法
在有限的时间内完成计算,得到的结果是唯一的,且取决于输入值。非确定性算法
在有限的时间内完成计算,但得到的结果往往是不唯一的,即存在多值。
4.3、按照算法的思想分类
按照算法的思想分类,可以被分为穷举算法、递推算法、递归算法、贪婪算法、分治算法、动态规划算法和迭代算法和概率算法等。
5、算法相关概念
算法是比较抽象的概念,往往需要有具体的实现方法和应用场景才能体现其价值。比如,在计算机编程中的算法、数值计算中的算法等等。
另外,正是因为算法比较抽象,容易和其相关的概念产生混淆,故我们需要做一些必要的说明。
5.1、算法和公式
公式,在数学、物理学、化学、生物学等自然科学中用常量符号、函数符号和关系符号等数学符号表示几个量之间关系的式子,具有普遍性,适合于同类关系的所有问题。比如:
速度
v = s / t ,v代表速度,s代表位移,t代表时间。圆的周长
c = 2πr,c代表周长,π代表圆周率,r代表半径。
公式是一种精简的计算方法,可以认为就是一种算法。但是,算法并不一定是公式,因为算法的形式比公式更加复杂,解决的问题更加广泛。
5.2、算法和程序
算法依托于具体的实现形式。尽管一提到算法,我们就联想到计算机程序设计,但是算法并不仅限于此。比如要进行一些简单的算数运算,我们可以使用纸和笔按照一定的步骤对问题求解。也可以使用算盘珠算。甚至可以像小学生那样,使用一些小木棍卡片等等用来帮助记忆和计算。
在计算机程序设计中,每个程序的实现都要用到算法,或者说,算法在该领域的体现更加广泛。只不过有些算法比较简单,比如两数相除。有些算法比较复杂,比如APP中常见的计算距离或者最短路径问题。所以,计算机程序设计语言也是算法实现的一种形式或者辅助工具,但绝不是必要的。或者说,计算机程序为算法的实现,提供了新的高效的手段。每一个计算机程序,都是算法的具体表现形式。
另外,目前流行很多编程语言,诸如Java,C,C++,C#,Python等等,不论我们采用哪种语言来编写程序,程序员始终处于主导地位,毕竟计算机不会自我思考和自主编程。故针对某个待解决问题,需要程序员必须自备解题方案(算法),然后再根据所使用具体编程语言的语法规则实现该方案的具体步骤,并验证输入数据、跟踪和改进程序执行过程,以求得预期的输出结果。还有,针对同样的问题若采用相同的算法,不同的程序员编写的计算机程序在代码细节上也有很大的差异。
如此以来,我们发现,使用具体编程语言编写程序的过程其实就是算法实现的过程。同时,要写出能优雅解决问题的高质量程序,就必须非常熟悉具体编程语言的特性和熟练运用其语法规则,并且使用了与问题相对应的恰当的优化的算法以实现该程序。我们也发现,熟悉某种编程语言的特性以及掌握其语法规则比较容易,通常看看官方帮助文档,写写示例程序就足够了。但是,如何正确合理地运用算法并通过编程求解具体问题,这是比较难的。当然了,这也是考量一个程序员是否合格、是否专业、是否优秀的必要指标和依据。
5.3、算法和数据结构
算法往往依赖于某种数据结构,即数据结构是算法实现的基础。了解数据结构相关的更多信息,请阅读数据结构概述汇总。
通过前面的介绍,我们已经对程序、算法、数据结构和编程语言等概念有了比较深刻的认识,我们也仿照尼古拉斯·沃斯(Nicklaus Wirth),用一个公式描述他们之间的关系。如下:
数据结构 + 算法 + 编程语言 = 程序
在这里,数据结构往往表示待处理的特定的对象数据,算法是为达到预期结果而采用的处理方法,编程语言是算法的实现手段,它们正确的结合在一起便构成了真正能Run起来的程序,当然这个程序也应该能解决针对的问题。
注意,算法是针对问题的解题方案,是该方案涉及的方法和步骤的抽象描述,同一个算法,在不同的编程语言中具有不同的实现方式,这依赖于数据结构的形式,以及编程语言的特性和语法规则。
6、算法的表示法
6.1、自然语言表示
自然语言通常是指一种自然地随文化演化的语言。例如,汉语、英语、日语等都是自然语言的例子。自然语言是人类交流和思维的主要工具,是人类智慧的结晶。
我们用自然语言描述一个趣味案例的算法:
题目:
有一个农夫带三只狼和三只羊过河,只有一条船,同船可以容纳一个人和两只动物。没有人在的时候,如果狼的数量大于或等于羊的数量,那么狼就会吃掉羊。请问农夫该如何渡河,以确保羊是安全的?
自然语言描述算法一:
第一步:人带两只狼过河,自己返回;
第二步:人带一只羊过河,并带两只狼返回;
第三步:人带两只羊过河,自己返回;
第四步:人带两只狼过河,自己返回;
第五步:人带一只狼过河,完成。
自然语法描述算法二:
第一步:人带两只狼过河,自己返回;
第二步:人带一只狼过河,自己返回;
第三步:人带两只羊过河,带两只狼返回;
第四步:人带一只羊过河,自己返回;
第五步:人带两只狼过河,完成;
对于一些简单的算法,可以采用自然语言口头或者书面描述其执行过程。但是,随着需求的发展,很多算法变得比较复杂,很难用自然语言描述。同时,自然语言表述繁琐且存在歧义性,更不利于发展和交流。因此需要采用其它方法来表示算法。
其实,我国古代的算法主要使用自然语言表示,这大大阻碍了古代中国算法的发展。这也正是我国古代算法起源早,却落后于西方的原因。
6.2、伪代码表示
伪代码(Pseudocode)是另外一种算法描述方式。它不是真正的计算机程序代码,其介于自然语言和编程语言之间。因此,伪代码并不能在计算机中运行。使用伪代码的目的,仅仅是为了将算法描述成更接近计算机编程语言的形式,如Java,C,C++,C#和Python等。这样,程序员就能更加容易理解算法如何实现,然后根据具体编程语言的特性和语法规则,修改成真正能Run起来的程序。
使用伪代码的另一个原因是C语言的广泛应用,其它编程语言大都借鉴了它的语法特点。比如if表示分支和判断,for和while表示循环等等。因此,我们可以借此共性来描述算法,从而忽略编程语言之间的差异。
在使用伪代码表示算法时,既可以使用自然语言,也可以使用简化的编程语言语句,重在灵活运用阐述清楚算法,并能方便团队交流以及代码复用。
下面举一个简单的伪代码表示程序的例子:
程序开始
变量 a = 输入数据
变量 b = 输入数据
if a > b
变量 max = a
else
变量 max = b
输出变量max
程序结束
在上述伪代码中,演示了求两数最大值的伪代码。通过该案例,我们发现伪代码表示非常灵活,又高度接近编程语言语法。程序员可以以此为参考,并结合编程语言特性和语法规则,编写出真正的程序。
另外,在使用伪代码时,描述应该结构清晰、步骤简单、可读性好,这样更有利于算法的表示。否则,适得其反,晦涩难懂,那就违背了使用它的初衷了。
6.3、流程图表示
使用图形表示算法的思路是一种极好的方法,因为千言万语不如一张图。流程图在汇编语言和早期的BASIC语言环境中得到应用。
6.3.1、流程图的图元
流程图是一种用图形表示流程的方法,由一些图框和流程线组成。其中,图框表示各种操作的类型,图框中的说明文字和符号表示该操作的内容,流程线代表操作的先后次序。
6.3.2、流程图的使用
使用流程图最大的优点就是简单直观、易于理解,这也是在算法领域比较推荐使用的表示法。例如,求两个数的最大值,可以采用流程图来表示。
6.3.3、流程结构
在实际使用中,一般采用三种流程结构。即顺序结构,分支结构和循环结构。
6.3.3.1、顺序结构
顺序结构是最简单的一种流程结构,操作一个接一个地进行处理。一般来说,顺序结构适合于简单的算法。如图所示:
6.3.3.2、分支结构
分支结构也被称为条件结构,常用于根据某个判断条件来决定控制流的走向。如果条件condition成立,则执行A操作,反之,执行B操作,然后再继续执行后续操作。如图所示:
6.3.3.3、循环结构
循环结构常用于反复执行的操作,根据循环的方式,分为当型循环结构和直到型循环结构。
- 当型循环结构
当型循环结构对条件condition先进行判断,然后再执行,一般采用while语句实现。
- 直到型循环结构
直到型循环结构先执行,然后再进行条件判断,一般采用do...while语句实现。
提示:
无论采用哪种循环结构,应尽量避免死循环出现,除非我们真的需要这么做,否则这将是程序的一个逻辑Bug。
6.3.3.4、流程结构总结
一般来讲,采用上述三种结构,就可以完成所有的算法的表示。我们通过设计合理流程结构的程序,以便更好的完成算法任务,同时,也能促进团队交流和工作产出效率的提升。
6.4、N-S图表示
N-S图,也被称为盒图、CHAPIN图 或NS图(Nassi Shneiderman图),是结构化编程中的一种可视化建模。
1972年,美国学者I.Nassi 和 B.Shneiderman提出了一种在流程图中完全去掉流程线,全部算法写在一个矩形阵内,在框内还可以包含其他框的流程图形式。即由一些基本的框组成一个大的框,这种流程图又称为N-S结构流程图(以两个人的名字的头一个字母组成)。N-S图包括顺序、选择和循环三种基本结构。
NS图类似流程图,但所不同之处是NS图可以表示程序的结构。DIN66261是NS图的相关标准。
依从上到下的设计,待处理的问题会分解成一些较小的副程序,最后只有简单的叙述及控制流程结构,NS图对应了上述的思维,利用嵌套的方块来表示副程序。NS图中没有对应Goto指令的表示,和结构化编程中不使用GOTO的理念一致。NS图的抽象层次接近结构化的代码,若程序重写,NS图就需重新绘制,不过NS图在简述程序及高级设计时相当方便。
NS图几乎是流程图的同构,任何的NS图都可以转换为流程图,而大部分的流程图也可以转换为NS图。其中只有像Goto指令或是C语言中针对循环的break及continue指令无法用NS图表示。
6.4.1、结构
6.4.1.1、程序方块
程序方块表示不需再分解的基本步骤,当流程进行到一程序方块时,会进行程序方块中的动作,然后移至下一个方块。
6.4.1.1、分支方块
分支方块可分为二种:真/假分支方块和多重分支方块。
- 真/假分支方块。
对应if指令,会有二个对应的路径,根据条件是否成立,决定后续运行的程序;
- 多重分支方块
当使用类似C语言的switch指令,依表达式结果要从三个或三个以上的路径中选择一个时使用,此方块一般会有许多对应的选项和其对应的子程序。
6.4.1.1、测试循环方块
测试循环方块允许程序运行一个或一组特定程序,一直到一特定条件满足为止。测试循环方块可分为二部分:左侧长条状部分和方块上方(或下方的)的测试条件部分相连辺,测试循环方块内部的方块则是循环中可能要运行多次的程序。
测试循环方块可分为二种:先测试的循环方块及后测试的循环方块。二者的差异是条件判断次序的先后。
- 先测试的循环方块
在运行循环前会先判断特定条件是否成立,若成立,不运行循环内的程序,若不成立,则运行循环内的程序……,只要特定条件成立,就结束循环内的程序,继续运行后续的程序。由于在循环开始时就判断条件是否成立,有可能在循环内程序完全未运行过的情形下就结束循环,继续运行后续程序。
- 后测试的循环方块
会先运行一次循环内的程序,之后判断特定条件是否成立,若不成立,才运行循环内的程序……。后测试的循环方块中,循环内的程序至少会被运行一次。
7、算法性能评估
学习和研究算法的一个重要的任务就是找到合适的、效率最高的解题方案,即最好的算法。因此,我们需要对算法的性能有一个合理的评估,并采用算法复杂度这个指标来衡量算法的优劣,它包括时间复杂度和空间复杂度两个方面。
7.1、时间复杂度
时间复杂度即通常所说的算法执行所耗费的时间,时间越短,性能越好。一个算法的执行时间往往无法精确计算,通常需要在计算机上实际运行才能知晓。但是,我们也可以对代码进行评估,从而得到算法复杂度。
首先,代码执行的时间往往和执行语句的数量有关。由于每条语句的执行都需要时间,语句执行的次数越多,整个程序所耗费的时间越长。因此,总执行次数最少的程序往往执行速度最快。
其次,算法的时间复杂度还和问题的规模有关。这方面我们在专门的算法分析中详细研究讨论,此处不作赘述。
最好,需要强调的是,算法的时间复杂度若采用执行时间衡量,永远得不到有效的评估值。因为,代码在不同软硬件配置和不同性能的计算环境下,其执行时间是有很大差异的。
7.2、空间复杂度
空间复杂度是指算法程序在计算机上执行时所消耗的存储空间。主要有两个方面:
程序本身的大小。
程序在执行过程中所消耗的存储空间大小。
一般来说,程序本身的大小越小,执行过程中消耗的存储空间越少,这个程序就越好。
8、简单案例
按照业界悠久的传统,往往以“Hello World!”作为第一个示例程序。我们也以此为例,以深入理解新公式“数据结构 + 算法 + 编程语言 = 程序”对本文关键知识点的概括性总结。
同时,我们为了在屏幕上打印“Hello World!”,使用了数组和链表,它们都是线性数据结构,当然还有更多数据结构和算法可用,此处就不一一罗列了。我们更重要的目的是通过这几个简单的案例,进一步说明数据结构、算法、编程语言和程序的定义和关系。
算法是针对具体具体问题的解题方案(方法和步骤)。
比如案例一中将Hello World!
当作byte数组处理,而在案例二中却使用LinkeList链表去处理。数据结构是算法的基础,算法依赖于数据结构。
比如案例一和案例二中使用了数组和链表,虽然它们都是线性数据结构,但肯定不是相同的数据结构,而且不同数据结构,采用的处理方法也不一样。数组可以直接赋值以存入数据元素,而链表却只能用add()方法。算法的实现取决于编程语言的特性和语法规则。
本文中的案例均采用Java语言编写,但是如果我们使用C语言实现,那么肯定没有“类”这个便利的特性可用,而且C语言也没有链表数据类型可用,必须自造轮子!使用具体语言编写程序的过程即为算法实现的过程。
计算机不会自我思考和自主编程,程序是程序员编写出来的,是程序员使用编程语言的特性和语法规则,告诉计算机解题的方法和步骤。
8.1、案例一
本案例用byte[]数组实现。数组是不同编程语言中最常见的数据结构。
/**
* Hello World! <br />
* byte[]数组实现。
* @author AT阿宝哥, goldenunion@qq.com
* @version V1.0, 2020-03-02
* @see 无
* @since Algorithm
*/
public class HelloWorld {
public static void main(String[] args) {
byte[] welcome = { 72, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100, 33 };
for (byte b : welcome) {
char tempChar = (char) b;
System.out.print(tempChar);
}
}
}
8.2、案例二
本案例LinkedList链表实现。链表是一种线性表,也是常用的一种数据结构。
import java.util.LinkedList;
/**
* Hello World! <br />
* LinkedList链表实现。
* @author AT阿宝哥, goldenunion@qq.com
* @version V1.0, 2020-03-02
* @see 无
* @since Algorithm
*/
public class HelloWorld {
public static void main(String[] args) {
LinkedList<Character> welcome = new LinkedList<>();
//追加操作
welcome.add('H');
welcome.add('e');
welcome.add('l');
welcome.add('l');
welcome.add('o');
welcome.add('W');
welcome.add('o');
welcome.add('r');
welcome.add('l');
welcome.add('d');
welcome.add('!');
//插入操作
welcome.add(5 ,' ');
for (Character character : welcome) {
System.out.print( character);
}
}
}
本文将持续改进,感谢您阅读,有什么收获吗?