算法设计技巧

在一段时间的算法学习以后,我们会有一定量的算法积累。借助这些算法,我们可以解决许多现成的问题。并且我们可以看到,当一个算法给定时,具体的数据结构无需指定。为使运行时间尽可能地少,需要由编程人员来选择适当的数据结构。

但是,有时候我们不得不把注意力从算法的实现转向算法的设计,因为已有的算法和数据结构并不能完全解决我们在编程中遇到的所有问题。

因此,这里我们将集中讨论用于求解问题的五种通用类型的算法。对于许多问题,很可能这些方法中至少有一种方法是可以解决问题的。特别地,对于每种类型的算法,我们将:

  • 了解一般的处理方法
  • 考察几个例子
  • 概括地讨论时间和空间的复杂度

这五种通用类型算法分别为:

贪婪算法(greedy algorithm)

贪婪算法分阶段地工作。在每一个阶段,可以认为所做决定是好的,而不考虑将来的后果。通常,这意味着选择的是某个局部最优。这种“眼下能够拿到的就拿”的策略是这类算法名称的来源。当算法终止时,我们希望局部最优等于全局最优。如果是这样的话,那么算法就是正确的;否则,算法得到的是一个次最优解(suboptimal solution)。如果不要求绝对最佳答案,那么有时可以选择使用简单的贪婪算法生成近似的答案,而不是使用通常产生准确答案所需要的复杂算法。

贪婪算法的例子:Dijkstra算法、Prim算法和Kruskal算法。

分治算法(divide and conquer)

分治算法由两部分组成:

  1. 分(divide):递归解决较小的问题(基本情况除外)
  2. 治(conquer):然后从子问题的解构建原问题的解

传统上,在正文中至少含有两个递归调用的例程才叫作分治算法,而正文中只含有一个递归调用的例程不是分治算法。一般我们认为子问题是不相交的。

分治算法的例子:归并排序,快速排序

动态规划(dynamice programming)

任何数学递推公式都可以直接转换成递归算法,但是基本实现是编译器常常不能正确对待递归算法,结果导致低效的程序。当怀疑很可能是这种情况时,我们必须再给编译器提供一些帮助,将递归算法写成非递归算法,让后者把这些子问题的答案系统地记录在一个表内。利用这种方法的技巧叫做动态规划(dynamic programming)。

动态规划的例子:改自顶向下的归并算法为自底向上的归并算法

随机化算法(randomized algorithm)

待补充

回溯算法(backtracking)

回溯算法相当于穷举搜索的巧妙实现,但性能一般不理想。不过情况不总是如此,在某些情形下它相对于蛮力穷举搜索的工作量也有显著的节省。

回溯算的一个具体例子是在一套新房子内摆放家具的问题。存在许多尝试的可能性,但一般只有一些可能是具体要考虑的。开始什么也不摆放,然后是每件家具被摆放在室内的某个部位。如果所有的家具都已摆好而且户主很满意,那么算法终止。如果摆到某一步,该步之后的所有家具摆放都不理想,那么我们必须撤销这一步并尝试该步另外的摆放位置。当然,这也可能导致另外的撤销,等等。如果我们发现我们撤销了所有可能的第一步摆放位置,那么就不存在满意的家具摆放方法。否则,我们最终将终止在满意的位置上摆放。注意,虽然这个算法基本上是蛮力的,但是它并不直接尝试所有的可能。例如,考虑把沙发放进厨房的各种摆法是绝不会尝试的。许多其他不好的摆放方法早就取消了,因为令人讨厌的摆放的子集是知道的。在一步内删除一大组可能性的做法叫做裁剪(pruming)。

回溯算法的例子:国际象棋和西洋跳棋的对弈中计算机如何选取行棋的步骤。

小结

当面临一个问题的时候,花些时间考察一下这些方法能否适用是值得的。算法的适当选择,结合数据结构的审慎使用,常常能够迅速导致问题的高效解决。

你可能感兴趣的:(算法与数据结构)