英文原文:http://community.topcoder.com/tc?module=Static&d1=tutorials&d2=importance_of_algorithms
导论
理解为什么研究掌握算法如此重要的第一步是准确的定义算法是什么。根据最流行的算法方面的书箱<<算法导论>>的定义"算法是设计良好的可计算的过程,它把某个值或某些值作为输入并产生某个值或某些值作为输出"。换句话说,算法是完成某个特定的设计良好的任务的路线图。所以,一段计算Fibonacci序列的代码就是一个特定算法的实现。从某种意义上说一个实现两数求和的函数也是一个算法,尽管它很简单。
一些算法,如计算Fibonacci序列,天生就存在于我们的逻辑思维与解决问题的技能中。然而,对于我们中的大多数而言,我们把复杂的算法做为基石以便在将来更有效的解决逻辑问题。事实现,当你知道人们每天在电脑上查看邮件或听音乐时就使用的多少复杂的算法时,你也许会感到惊讶。这篇文章将会介绍算法分析 的基本思路,然后列举一些这些算法在实际中的应用以说明为什么了解算法是如此的重要。
运行时分析
算法的一个重要方面是它运行的有多快。通常设计一个解决问题的算法是很容易的,但如果这个算法很慢,就要重新设计了。因为算法运行的速度取决于它运行的环境以及实现的细节,计算机科学家们倾向于把运行时间以输入的大小来表示。例如输入大小为N,运行时间与N^2成比例,刚表示为O(n^2)。这意味着如果运动一个脸有N个输入的算法,它将使用Cn^2的时间,C是不随输入大小变化的常量。
然而很多复杂算法的运行时间还会随着除输入大小以外的其它因素而变化。例如一个排序算法当输入数据有序时比输入数据无序时要运行的快。因此你常会听到最差运行时间和平均运行时间。最差运行时间是当所有可能的输入中在最不利的情况下算法运行的时间,平均运行时间是所有可能的输入下算法的运行时间。两者之中最差运行时间通常更容易找原因,因此一般作为判断一个算法的基准。判断一个算法的最差运行时间和平均运行时间往往会很棘手,因为不可能考虑到所有的输入。网上有很多资源可以帮助你评估这些值。
排序
排序是一个很好的科学家经常使用的算法的例子。排列一组元素的最简单的方式是把其中最小的元素移出来并放在首位。然后再移出第二小的放在首位的后面,依次类推。不幸的是算法的时间复杂度为O(n^2),这意味着运行时间与元素规模成平方关系。如果你不得对十亿的数据进行排序,这个算法将会执行10^18次操作。举例来看,一台一秒钟大约执行10^9次操作的PC,需要好几年才能执行完这个排序算法。
幸运的是,还有很多更高效的算法(如快速排序、堆排序、归并排序)被设计了出来,它们当中很多只需要nlogn的运行时间。这就把对十亿数据排序的操作次数降到了一个合理的值,即使一台便宜的台式机也能胜任。与10^18次操作不同,这些算法只需要100亿次操作,快了一亿倍。
最短路径
寻找从一个点到另一个点的最短路径的算法已经被研究了很多年了。应用就不说了,我们仅从在一个只有几条街和几个十字路口的城市寻找从A到B的最短路径说起。有很多不同的算法被设计出来解决这个问题,每一个都有着各自的优点和弊端。在研究这些算法之前,我们先考虑一个朴素的算法(尝试每种可能的情况)需要运行多少时间。如果这个算法考虑从A到B的每一个可能的路径(不含环路),它将会用尽我们一生的时间,即使A和B都在一个很小的城镇。这个算法的运行时间与输入大小成指数,也就是O(c^n)。即使C很小,当N适度大时,c^n也会成为一个天文数字。
一个解决这个问题的最快的算法只需要O(EVlogV)的时间,E为街的条数,V为十字路口的个数。举例来看,在一个有着10000条街和20000个十字路口的城市要找到最短路径只需要2秒的时间(每个十字路口只有两条街)。这个算法,即Dijkstra算法,相当复杂,而且还需要用到最小队列的数据结构。然而在某些应用中这个算法还是太慢了(考虑从纽约到旧金山有几百万个十字路口的情况),编程人员试图通过启发策略来做的更好些。启发策略是对问题相关性的粗略估计,由算法本身来完成计算。例如在最短路径问题中,粗略估计一个节点到目标节点的距离是很有用的。知道了这些就能开发出更快的算法(A*算法,明显的比Dijkstra算法快)并且编程人员也能设计出启发策略来估计这个值。这样做在最差情况下并不总能提升运行时间,在地真实情况下确实快了很多。
近似算法
有些情况下,一个良好设计的拥有最好的启发策略的在最好的计算机上运行的算法也会很慢。在这种情况下,在保证结果正确的前提下,做些牺牲也就很有必要了。要对于找到最短路径,一个程序可能找到的是比最短路径要长出10%的一条路径。
事实上,(这句没怎么看懂)广为人知的算法产生一个最优解仍存在的许多重要问题是:对大多数目的不是足够的慢。这些问题被称为NP,代表不确定的多项式。当一个问题被称为NP完全的或是NP困难的,也就意味着没有人知道解决此类问题的有效方式。更进一步,如果一个人设计出了一个解决NP完全问题的有效算法,这个算法也会适用于所有NP完全的问题。
一个NP困难的例子是著名的旅行商问题。一个销售人员想要走访N个城市,他知道从一个城市到另一个城市要用多久,问题是“他要怎样才能最快的访问所有的城市?”。因为解决这个问题的最快的算法太慢了-并且很多人认为这种情况会一直存在-编程人员努力寻找一个足够快的算法来给出一个好的但不是最优的解决方案。
随机算法
然而解决一些问题的另一个方法是在某种程度上随机化算法。这样做并不会算法在最差情况下的性能,但在平均情况下常会表现的很好。快速排序算法是使用随机算法的好例子。在最坏情况下,快速排序需要O(n^2)的时间来对n个元素的集合排序。如果随机化被引入到算法中,实际遇到最差情况的概率会逐渐的减小,在平均情况下,快速排序的运行时间为O(nlogn)。即使在最差的情况下,也能保证O(nlogn)的运行时间,但在平均情况下会慢些。尽管算法的渐近时间复杂度都为O(nlogn),快速排序却有一个更小的常数因子,它需要cnlogn次操作,其它算法约为2cnlogn次操作。
另一个使用随机数寻找集成中中位数的算法需要O(n)的平均时间。这比先对元素排序再取其中位数有了很大提升,那样做会用O(nlogn)的时间。更进一步,使用O(n)时间查找一个集成中位数的确定算法(非随机)确实存在,而随机算法却异常简单,并且比确定算法要快。
中位数算法的基本思想是从集合中随机选取一个数,并计算集合中有多少个数比这个数小。比如集合中有N个元素,其中有k个元素小于等于随机选中的数。如果k小于N/2,那我们就知道有N/2-k个数比我们随机选取的数大,所以我们放弃那k个小于等于随机选取数的数。现在我们需要找到第N/2-k小的数,而不是中位数。算法也是同样的,我们简单的随机选取另一个数,并且重复上面的步骤。
压缩
算法解决的另一个经典情况是数据压缩。这类算法没有一个特定的期望的输出,但有其它标准可以优化。在数据压缩中,算法(如LZW)试图让数据占用尽量少的空间,并且可以不愿成最初的形式。在某些性况下,这类算法会像其它算法寻样使用相同的技术产生一个好的但次优的解。例如JPG、MP3用某种方法使得压缩后的结果质量降低了,但产生了更小的文件。Mp3文件压缩后并不会保存原有文件的所有特性,但它会试图保留足够的细节,在保证质量的同时让文件尽可能的小。JPG文件的压缩同样遵循这样的原则,只是在细节上会有很不同,因为这是图而不是音频。
了解算法的重要性
做为一个计算机科学家,了解每一类算法是很重要的,这样就能正确的使用它们。如果你正致力于软件的一个重要模块,你很可能想估计它运行的有多快。如果不了解运行时分析估算就会很不精确。更进一步,你需要了解相关算法的具体细节,以便预测是否存在某种情况使用软件不能快速的工作,或者会产生不可预期的结果。
当然有时你会遇到一些未知的问题。在这种情况下,你就需要设计新的算法或是改进旧的算法。你对算法了解的越多,找到解决问题的好方法的机率就越大。很多时候一个新的问题可以很容易的转换成老问题,但是这样做时你需要对老问题有足够的理解。
举个例子,考虑下交换机在网络中的应用。一个交换机会与N条网线相接,并接收网线中的数据。交换机需要在第一时间分析网络包,并把它们转送到正确的网口。像电脑一样,交换机也是以离散有时钟计算的,网络包会在离散时间发送出去而不是连接的发出去。在一个快速的交换机中,我们想要在一个离散间隔发送出去尽可能多的包以便它们不会堆积和丢失。我们要设计的算法的目的是要在每一个离散间隔内尽可能多的发送出包,并且最早到达的包会最先被发送出去。在这个例子中一个叫做"稳定匹配"的算法就非常适合我们的问题,尽管最初这种关系看上去并不那么明显。只有对之前存在的算法有足够的掌握和了解才能发现这种关系。
更多现实中的例子
现实中要解决的问题需要更多高深的算法。基本上你在电脑上做的每一件事件在某种程度上都依赖于别人努力设计出的算法。即使在现代电脑上的一个最简单的应用没有算法在背后提供内存分配和数据载入也不可能运行。
有很多复杂算法的应用,但是接下来我将在讨论两个在TopCode中用到的算法。第一个是最大流,第二个是动态规划,这是一个可以快速解决看上去不可能解决的问题的方法。
最大流
序列比较
结论