分类目录:《算法设计与分析》总目录
算法学基础(一):算法学概述
算法学基础(二):分析算法
算法学基础(三):渐进记号
算法定义的是计算过程,该过程取某个值或值的集合作为输入并产生某个值或值的集合作为输出。这样算法就是把输入转换成输出的计算步骤的一个序列。我们也可以把算法看成是用于求解良说明的计算问题的工具。一般来说,问题陈述说明了期望的输入/输出关系。算法则描述一个特定的计算过程来实现该输入/输出关系。
例如,我们可能需要把一个数列排成非递减序。实际上,这个问题经常出现,并且为引入许多标准的设计技术和分析工具提供了足够的理由。下面是我们关于排序问题的形式定义:
输入: n n n个数的一个序列 ( a 1 , a 2 , ⋯   , a n ) (a_1, a_2, \cdots, a_n) (a1,a2,⋯,an)
输出:输入序列的一个排列 ( a 1 ′ , a 2 ′ , ⋯   , a n ′ ) (a_1', a_2', \cdots, a_n') (a1′,a2′,⋯,an′),满足 a 1 ≤ a 2 ′ ≤ ⋯ ≤ a n ′ a_1 \leq a_2' \leq \cdots \leq a_n' a1≤a2′≤⋯≤an′
例如,给定输入序列(3, 1, 4, 2, 5),排序算法将返回序列(1, 2, 3, 4, 5)作为输出。这样的输入序列称为排序问题的一个实例。一般来说,问题实例由计算该问题解所必需的(满足问题陈述中强加的各种约束的)输入组成。
因为许多程序使用排序作为一个中间步,所以排序是计算机科学中的一个基本操作。因此,已有许多好的排序算法供我们任意使用。对于给定应用,哪个算法最好依赖于以下因素:将被排序的项数、这些项已被稍微排序的程度、关于项值的可能限制、计算机的体系结构,以及将使用的存储设备的种类(主存、磁盘或者磁带)。
若对每个输入实例算法都以正确的输出停机,则称该算法是正确的,并称正确的算法解决了给定的计算问题。不正确的算法对某些输入实例可能根本不停机,也可能以不正确的回答停机。与人们期望的相反,不正确的算法只要其错误率可控有时可能是有用的,但是通常我们只关心正确的算法。算法可以用自然语言说明,也可以说明成计算机程序,甚至说明成硬件设计。唯一的要求是这个说明必须精确描述所要遵循的计算过程。
虽然算法可以解决各式各样的问题,但是算法所解决的问题有的两个共同的特征:
假设计算机是无限快的并且计算机存储器是免费的,你还有什么理由来研究算法吗?即使只是因为你还想证明你的解法会终止并以正确的答案终止,那么回答也是肯定的。如果计算机无限快,那么用于求解某个问题的任何正确的方法都行。也许你希望你的实现在好的软件工程实践的范围内(例如,你的实现应该具有良好的设计与文档),但是你最常使用的是最容易实现的方法。
当然,计算机也许是快的,但它们不是无限快。存储器也许是廉价的,但不是免费的。所以计算时间是一种有限资源,存储器中的空间也一样。你应该明智地使用这些资源,在时间或空间方面有效的算法将帮助你这样使用资源。
为求解相同问题而设计的不同算法在效率方面常常具有显著的差别。这些差别可能比由于硬件和软件造成的差别要重要得多。比如我们常用的两个用于排序的算法。第一个称为插入排序(在文章《排序算法(一):插入排序》介绍),为了排序 n n n个项,该算法所花时间大致等于 c 1 n 2 c_1n^2 c1n2,其中 c 1 c_1 c1是一个不依赖于 n n n的常数。也就是说,该算法所花时间大致与 n 2 n^2 n2成正比。第二个称为归并排序(在文章《排序算法(二):归并排序》介绍),为了排序 n n n个项,该算法所花时间大致等于 c 2 n lg n c_2n\lg{n} c2nlgn,其中 lg n \lg{n} lgn代表 log 2 n \log_2{n} log2n且 c 2 c_2 c2是另一个不依赖于 n n n的常数。与归并排序相比,插入排序通常具有一个较小的常数因子,所以 c 1 < c 2 c_1<c_2 c1<c2。我们将看到就运行时间来说,常数因子可能远没有对输入规模 n n n的依赖性重要。把插入排序的运行时间写成 c 1 n 2 c_1n^2 c1n2并把归并排序的运行时间写成 c 2 n lg n c_2n\lg{n} c2nlgn。这时就运行时间来说,插入排序有一个因子 n n n的地方归并排序有一个因子 lg n \lg{n} lgn,后者要小得多。虽然对于小的输入规模,插入排序通常比归并排序要快,但是一旦输入规模 n n n变得足够大,归并排序 lg n \lg{n} lgn对 n n n的优点将足以补偿常数因子的差别。不管 c 1 c_1 c1比 c 2 c_2 c2小多少,总会存在一个交叉点,超出这个点,归并排序更快。
作为一个具体的例子,我们让运行插入排序的一台较快的计算机(计算机A)与运行归并排序的一台较慢的计算机(计算机B)竞争。每台计算机必须排序一个具有1000万个数的数组。虽然1000万个数似乎很多,但是,如果这些数是8字节的整数,那么输入将占用大致80MB,即使一台便宜的便携式计算机的存储器也能多次装入这么多数。假设计算机A每秒执行百亿条指令,而计算机B每秒仅执行1000万条指令,结果计算机A就纯计算能力来说比计算机B快1000倍。为使差别更具戏剧性,假设世上最巧妙的程序员为计算机A用机器语言编码插入排序,并且为了排序 n n n个数,结果代码需要 2 n 2 2n^2 2n2条指令。进一步假设仅由一位水平一般的程序员使用某种带有一个低效编译器的高级语言来实现归并排序,结果代码需要 50 n lg n 50n\lg{n} 50nlgn条指令。为了排序1000万个数,可以计算得出计算机A需要20000秒,而计算机B只需要1163秒。所以,通过使用一个运行时间增长较慢的算法,即使采用一个较差的编译器,计算机B比计算机A还快17倍!当我们排序1亿个数时,归并排序的优势甚至更明显:这时插入排序需要23天多,而归并排序不超过4小时。一般来说,随着问题规模的增大,归并排序的相对优势也会增大。
上面的例子表明我们应该像计算机硬件一样把算法看成是一种技术。整个系统的性能不但依赖于选择快速的硬件而且还依赖于选择有效的算法。正如其他计算机技术正在快速推进一样,算法也在快速发展。
你也许想知道相对其他先进的计算机技术(如:先进的计算机体系结构与制造技术、易于使用、直观的图形用户界面(GUI)、面向对象的系统、集成的万维网技术、有线与无线网络的快速组网等),算法对于当代计算机是否真的那么重要?回答是肯定的。虽然某些应用在应用层不明确需要算法内容(如某些简单的基于万维网的应用),但是许多应用确实需要算法内容。例如,考虑一种基于万维网的服务,它确定如何从一个位置旅行到另一个位置。其实现依赖于快速的硬件、一个图形用户界面、广域网,还可能依赖于面向对象技术。然而,对某些操作,如寻找路线(可能使用最短路径算法)、描绘地图、插入地址,它还是需要算法。而且,即使是那些在应用层不需要算法内容的应用也高度依赖于算法。该应用依赖于快速的硬件吗?硬件设计用到算法。该应用依赖于图形用户界面吗?任何图形用户界面的设计都依赖于算法。该应用依赖于网络吗?网络中的路由高度依赖于算法。该应用采用一种不同于机器代码的语言来书写吗?那么它被某个编译器、解释器或汇编器处理过,所有这些都广泛地使用算法。
算法是当代计算机中使用的大多数技术的核心。
进一步,随着计算机能力的不断增强,我们使用计算机来求解比以前更大的问题。正如我们在上面对插入排序与归并排序的比较中所看到的,正是在较大问题规模时,算法之间效率的差别才变得特别显著。是否具有算法知识与技术的坚实基础是区分真正熟练的程序员与初学者的一个特征。使用现代计算技术,如果你对算法懂得不多,你也可以完成一些任务,但是,如果有一个好的算法背景,那么你可以做的事情就多得多。