代码改变世界

The Importance of Algorithms[翻译]

2007-02-20 23:08  老博客哈  阅读(1606)  评论(6编辑  收藏  举报

                                                               The Importance of Algorithms
                【原文见: http://www.topcoder.com/tc?module=Static&d1=tutorials&d2=importance_of_algorithms
                                                                                                       作者:      By lbackstrom
                                                                                                                          Topcoder Member
                                                                                                        翻译:      农夫三拳@seu
                                                                                                                          (drizzlecrj@gmail.com)

Introduction
理解为什么学习算法知识如此重要的第一步是要了解我们所说的算法是什么含义。根据《算法导论》(第二版 由 Thomas H. Cormen,

Charles E. Leiserson, Ronald L. Rivest, Clifford Stein著), “一个算法是一个定义好的计算过程,它使用某些值或者
一系列的值作为输入并产生某些值或者一系列的值作为输出”。换句话说,算法就像一个用来帮助完成一个给定的任务的路线图。由此,计算Fibonacci序列的一小段代码是一个具体算法的实现。甚至一个用来求两数之和的简单函数在某种意义上来说也是一个算法,尽管很简单。

一些算法,像计算Fibonacci序列一样,是具有启发性的并且可能已经潜移默化到我们的逻辑思维和解决问题的能力当中。尽管如此,对于我们中的大多数人,学习好复杂的算法可以为日后更加高效的解决逻辑问题建立模块。事实上,你可能很惊讶人们在日常生活中查看e-mail和在电脑上听音乐当中使用了多少复杂的算法。这篇文章将讨论与算法分析相关的基本问题, 并且会举一些事例来说明为什么学习算法很重要。

Runtime Analysis
算法中最重要的部分是它的速度。 通常我们都很够很容易想到解决一个问题的算法,但是如果这个算法太慢,我们又要重新开始思考。由于算法的准确速度与算法的运行环境及实现相关, 计算机学者通常使用与输入相关的运行时分析。例如,如果这个输入包含N个整数,
一个算法可能有一个N^2相关的运行时,用O(N^2)来表示。这个意味着如果你在你的机子上运行这样一个输入大小为N的实现,它将会花费C*N^2秒, 这里C是一个常数并且不会随输入的大小而改变。

尽管如此, 许多复杂算法的运行时间除了输入规模以外还有一些因素。例如,一个排序算法也许会非常快,当给定的整数中已经部分排好序而不是无序。此外,你经常会听到最坏运行时,或者平均运行时。最坏运行时是在最阴险的数据作为输入时,它将花费多长时间来运行这个程序。平均运行时指的是对于所有可能的数据的算法运行时间。这两者当中,最坏运行时通常很具有说服力,因此经常被用来作为一个评判给定算法的基准。 对于一个给定的算法,决定最坏运行时和平均运行时通常很诡异,因为不可能让它运行所有的输入。 网上有很多的资源能够帮助你估计这些值。

   算法的近似完成时间, N = 100    

    O(Log(N)) 10-7 seconds 
    O(N) 
10-6 seconds 
    O(N
*Log(N)) 10-5 seconds 
    O(N2) 
10-4 seconds 
    O(N6) 
3 minutes 
    O(2N) 
1014 years. 
    O(N
!10142 years. 

Sorting
排序是计算机学者经常使用的算法之一。最简单的方法来排序一组项是首先从改组中取出最小的项,然后将它放到第一个。然后取出次小的那个,然后依次往后放。不幸的是,这个算法是O(N^2), 这就意味着这个算法所需要的时间与项的个数成平方比例关系。 如果你要排序100万个数,这个算法将执行约10^18次操作。进一步看, 一个PC每秒能够执行10^9多一点点的操作,那么算下来要花费许多年才来完成排序100万个数。

幸运的是, 有许多已经设计好的更好的算法(例如:快速排序, 堆排序, 归并排序), 他们中大多数都是O(N*Log(N))的运行时。这个将使得上述排序100万个数的操作数降到一个普通电脑就能够执行的地步。与100万个数需要平方操作(10^18)相比, 这些算法仅需要1千万次操作(10^10), 将近10亿倍快。

Shortest Path
用来查找一个点和另外一个点之间的最短路径的算法已经研究许多年了。应用方面,举个简单例子,我们想要在一个有一些街道和十字路口的城市中寻找点A到点B的最短路径。有相当一部分算法可以用来解决这种问题,所有的算法都有自己的优缺点。在我们深入研究这个问题之前,让我们考虑一下原始的算法-尝试每一个可以想到的路径-会花费多长时间。如果这个算法考虑A和B之间所有的可能路径(不考虑环), 在我们有生之年恐怕也不会结束,即使A和B都在一个小镇上。这个算法运行时对于输入大小是指数级的,也就是说对于C,他是O(C^N)。甚至对一些C,当N一定大时, C^N将会变为天文数字。

解决这个问题的最快的算法之一运行时是O(E*V*Log(V)),这里E代表路线的数量,V是交叉点的数量。这样来看,这个算法将花费大约2秒来查找一个有10000个交叉点和20000条路线的城市中的最短路径(通常大约每一个交叉点两个路线)。这个算法就是著名的Dijkstra算法,它相当复杂,需要使用一个叫做优先级队列的数据结构。在一些应用中,在这个算法相当慢时(考虑查找New York和San Francisco之间的最短路径--在美国有数百万的交叉点),程序员考虑使用启发式算法来做的更好。一个启发式算法是和这个问题相关的近似解法并且通常由算法本身来进行计算。 在最短路径问题中,比方说, 知道一个点和目的地之间的近似距离很有用。这样将允许使用更快的算法(例如A*,一个有时能够远远快于Dijkstra算法的算法)因此程序员使用启发式来得到近似值。这样做并不能提高程序在最坏情况下的运行时,但是它的确使得算法在现实世界中的应用变得更快。

Approximate algorithms
有时,即使最高级的算法,使用最高级的启发式,在最快的电脑上运行,它也很慢。在这种情况下,牺牲一点正确性势在必行。与试图去得到最短路径,一个程序员可能更加喜欢找到一个比最短路径最多多10%的路径。

事实上,有相当一部分的重要问题,那些产生最优化结果的著名算法都是非常慢的。这些问题的一个著名集合被称作NP--代表无法决定的多项式算法(non-deterministic polynomial )(不要担心它们是什么意思)。当一个问题被称作NP完全问题或者NP-hard问题,这意味着没有人知道一个解决他们的最佳办法。更进一步来说,如果有人却是想到了一个NP-hard问题的高效算法,那么这个算法将会对所有的NP-hard算法适用。

NP-hard问题一个很好的例子就是著名的旅行商问题。一个商人想要访问N个城市并且他知道从一个城市到其他城市所要花费的时间。问题是“他怎样才能最快的访问所有的城市?”由于现知的解决这个问题最快的算法太慢了--大多数人认为这个永远不变--程序员们还在寻找最快的能够给出“好”解而不是最优解的算法。

Random Algorithms
另外一个处理一些问题的方法是用某种方式进行随机。尽管这样做并不能改善算法在最坏情况下的性能, 但是它通常能够得到在平均情况下的好的算法。快速排序就是一个随机化使用的算法例子。在最坏的情况下,快速排序对一组项排序的时间复杂度是O(N^2),这里的N是项的个数。如果随机化过程被合并到算法中,那么最坏情况产生的机会将会变得非常小, 因此在平均情况下,快速排序有一个O(N*Log(N))的运行时。其他有一些算法能也够保证算法即使在最坏情况下也具有O(N*Log(N))的时间复杂度,但是,他们通常在平均情况下
比较慢。尽管算法都具有一个N*Log(N)比例的运行时,快速排序有一个较小的常数因子-也就是需要C*N*Log(N)次操作,而其他的算法需要更多的像2*C*N*Log(N)次的操作。

另外一个算法是在平均情况O(N)的复杂度下,使用随机数查找一组数的中位数。这个要比使用O(N*Log(N))的算法对数进行排序,然后取出中间数要有着巨大的改进。更进一步的,与具有O(N)复杂度的非随机算法相比, 随机算法显得异常简单并且要比非随机算法更加快。

中位数的基本思想是从该组数中随机选取一个数,然后数一下这组数中有多少个数小于它。架设现在有N个数, 其中有K个数小于或者等于我们随机挑选的数。如果K小于N的一半,那么我们就知道了中位数是第(N / 2 - K)个比我们挑选的数大的数,接着舍去这K个比我们挑选的随机数小于或者等于的数。现知, 我们需要找到第(N/2 - K)个最小的数而不是中间的数。我们接下去继续随机挑选一个数,
然后重复上述操作。

Compression
算法处理的另外一个方面是数据压缩。这种类型的算法没有一个期望的输出(像排序算法一样), 取而代之的是去优化一些其他的标准。在数据压缩领域,算法(例如,LZW)试图将数据用尽可能少的字节进行存储并且可以被解压成原始样式。在某些情况下, 这种类型的算法将会使用其他算法的技巧来达到很好的输出,但是是潜在的是子最优的。比如JPG和MP3压缩, 两者压缩数据都使得结果的质量比原来的要低,但是他们创建的文件比原来的要小。MP3压缩不能够保有原声的每一个细节, 但是它试图保有足够多的细节来保证足够
的质量,同时又能确保我们知道的并且喜欢的文件大小。JPG图片遵从了上面一样的道理,但是细节却是大相径庭因为这里的目标是图像而不是音频压缩。

The Importance of Knowing Algorithms
作为一个计算机学者, 为了正确的使用这些算法,知道它们是很重要的。如果你正在从事软件中一个重要的模块,那么你很可能需要估计它运行的有多快。这样的估计在不知道运行时分析的情况下是不准确的。更进一步的,你需要理解算法所包含的细节,这样你就能够预见在一些特殊情况下软件不能很快的工作或者不能产生可以接受的情况。

当然, 碰到一个以前没有研究过的算法是家常便饭的事情。在这种情况下,你需要自己想一个新的算法或者老法新用,你知道的算法越多,更好解决问题的可能性就越大。在很多情况下, 一个新问题可以很容易的被简化成一个老问题,但是前面是你需要对这个老问题有一定的了解。

举个例子, 让我们考虑网络中使用的交换机。一个交换机有着N个电缆插入其中, 并且接收者从这些电缆中传输的包。这个交换机首先要分析包,然后将他们在正确的电缆上发出。和一个电脑一样,一个交换机也是由一个时钟运行着的-包是在离散的时间段中被发出,而不是连续不断的。在一个很快的交换机中,我想要在一个时间段里尽可能多的发出包而不让它们堆积并且丢掉。我们所设计的这个算法的目标是在一个时间段里尽可能多的发出包并且将它们发出达到先来的包先发出的效果。在这种情况下, 事实证明“stable matching”算法和我们的问题是相对应的,尽管咋一看两者似乎没有关系。只有预先存在的算法知识和理解才能发现这样的关系。

More Real-world Examples
其他现实世界中的问题需要更多高级的算法。几乎你用电脑完成的每一件事都或多或少依赖于一些人思考出来的算法。甚至是最简单的现代电脑上的管理内存和从驱动器中加载数据的应用在没有使用的算法情况下也是不可能的。有许多个复杂算法的应用例子,这里我想从以前的TopCoder问题中讨论两个使用相同技巧的问题。第一个是最大流问题,第二个和动态规划有关, 一个通常能够以惊人的速度解决似乎不可能的问题的技术。

Maximum Flow
最大流问题就是在某种网络中以最好的方式将一些东西从一个地方运到另外一个地方。更具体一点的说, 这个问题的起因和1950年苏维埃共和国的铁路线路有关。美国想要知道苏维埃共和国通过铁路线路能够多快从东欧的国家得到补给。除此之外,美国还想知道哪个线路能够最容易的被破坏来切断到苏维埃的救援。事实证明这两个问题密切相关, 并且解决最大流问题同时也就绝了计算最经济的切断苏维埃救援的最小割问题。

第一个找到最大流的高效算法是由两位计算机科学家, Ford和Fulkerson构思的。 这个算法后来被称为了Ford-Fulkerson算法,并且成为了计算机科学界最著名的算法之一。在过去的50年里,出线了许多使Ford-Fulkerson算法更快的改进方案,然后一些很复杂。

由于这个问题首次亮相, 许多额外的应用也随之被发现。这个算法和网络有着显然的联系,在网络上需要从一个点到另外一个点尽可能多的获取数据是很重奥的。 它也进入了许多企业中并且称为了调配研究的重要部分之一。例如,如果你有N个雇员和N份工作需要完成,但是并不是每个雇员可以做任何工作,最大流算法能够计算出怎么样分配这N个雇员的工作来使得没见工作得以完成,证明这样是可能的。SRM200中的Graduation就是TopCoder问题中的一个使用最大流的问题。

Sequence comparison

许多程序员在他们整个职业生涯中也没有实现过一个使用动态规划的算法。尽管如此,动态规划产生了许多个重要的算法。大多数程序员可能使用过的一个算法,或许他们不知道叫什么,找出两个序列的不同之处。更加具体一点,计算最少的插入,删除,修改操作来使得序列A变为序列B。

例如,让我们考虑两个序列, "AABAA"和"AAAB"。想要将第一个序列变为第二个序列,最简单的方式是删除中间的B,然后把最后的A换成B。这个算法有很多的应用,包括啊DNA问题和抄袭检查。尽管如此 最长用的是许多哦程序员使用的比较两个版本的同样一份源代码。如果这个序列的元素是文件中的行,那么这个算法能够告诉程序员哪些行被移除了,哪些被添加了,哪些由一个版本被修改成另外一个。

没有动态规划,我们可能考虑 a--你猜--指数级的变换由一个序列变成另外一个。尽管如此,动态规划使得这个算法在O(N*M)的运行时内完成,这里的N和M是两个序列中的元素的个数。

Conclusion
人们研究的算法因碰到的问题的不同而不同。尽管如此, 你要解决的问题和你以前解决的很相似的机会还是很多的。通过设计一个大范围的算法,你将能够从中挑选出合适的并且正确的使用它。更进一步的,解决TopCoder比赛中的问题可以帮助你联系你的这方面的能力。许多问题,尽管看起来不现实,同样需要我们在现实生活中每天遇到的算法知识。