1.2 算法简介

1.2 算法简介

算法是定义良好的用来解决问题的步骤。在计算机科学领域算法是必不可少的,因为它们正是计算机完成系统操作所需要的具体步骤。好的算法就如同好的工具一样,以合理的付出完成相应的工作。使用不当的或定义不清的算法就像用台锯去切割一张纸,或者用剪刀去切夹板一样:尽管工作也许能完成但你不得不考虑完成工作的效率。和数据结构一样,使用算法也有3个原因:效率,抽象,重用性。

效率

        由于特定类型的问题经常在计算机领域出现,随着时间的推移人们已经找到了高效的方法来解决这类问题。比如,试想一下要对一本书中的索引号排序。因为排序是一项常见的任务因此对于有许多高效的算法可以完成排序任务你应该不会感到太惊讶。我们将在第12章探究一些排序算法。 

抽象

        在解决问题时,算法能够提供一定程度的抽象,因为很多看似复杂的问题都可以用已存在的算法来简化。一旦我们能用简化的眼光去看待复杂的问题,那么更为复杂的问题都可以被我们看作一个更为简单问题的抽象。例如,试想一下如何找到两个网关之间的最短路由。一旦我们意识到这个问题不过就是更具一般性的单对最短路径问题(见第16章)的变种时,我们就能够以这种泛化的方式来解决问题。 

重用性

        算法在很多不同场景下能够得到重用。因为很多著名的算法解决的问题都是由复杂的问题抽象而来的,这也是因为很多复杂的问题都能够简化为简单的问题。一些能够有效解决这类特定问题的方法使我们有可能解决更多其它的问题。

 

1.2.1 算法设计的一般方法

从广义上讲,很多算法解决问题的思路是相同的。因此,为了方便通常按照算法采用的方法和思路来给它们分类。这样给算法分类的一个原因是如果我们理解了它采用的一般思路我们常常就能够对该算法获得一些深入的了解。在解决一些没有现成算法求解,但与现有问题类似的问题时,我们从中可以得到一些启发和灵感。当然,有些算法有悖于分类原则,而另一些则是多种方法相结合的产物。这一节我们将介绍一些常用的方法。

 

1.2.1.1  随机法

随机法依赖于随机数的统计特性。一个应用随机法的例子是快速排序(见第12章)。快速排序按如下方式工作:设想要对一堆作废的支票排序,我们首先将无序的一整堆分成两部分。其中一堆里我们使所有的支票号码都小于等于我们所设定的一个中间值,另一堆里我们保证所有的支票号码都大于这个中间值。一旦我们有了这样的两堆支票后,我们再以同样的方式对这两堆支票重复刚才的划分过程,直到每一堆里都只有一张支票为止。这个时候所有的支票就都排好序了。

为了获得较高的性能,快速排序依赖于每一次我们要如何去划分支票,我们需要让划分出的两堆支票数量几乎相同。为了实现这一步,理想的方法是我们在划分支票之前首先找到支票号码的中间值。可是为了确定这个中间值需要遍历所有的支票因此我们并不打算这么做。作为替代的方法,我们随机选择一个支票号码作为划分的依据。快速排序的平均性能很不错,因为随机数的正态分布使得划分的结果是相对平衡的。

 

1.2.1.2  分治法

分治法包含3个步骤:分解,求解,合并。在分解阶段,我们将数据分解为更小,更容易管理的部分。在求解阶段,我们对每个分解出的部分进行处理。在合并阶段,我们将每部分处理的结果进行合并。一个分治法的例子是归并排序(见第12章)。

归并排序按照如下方式工作。如前所述,同样假设我们要排序一堆作废的支票。我们首先将无序的一整堆分成两半,下一步我们分别将两堆支票再各自分成两半,一直持续这个步骤直到每一堆中都只有一张支票为止。一旦所有堆中都只有一张支票时,我们将其两两合并且保证每一个合并后的新堆都是有序的。一直做两两合并直到我们重新得到一整个堆,此时所有的支票就都已经排好序了。

在所有的分治算法中都有相同的三个步骤。归并排序能以以下方式来描述,首先,在分解阶段将数据划分为两份。接下来按照递归的方式对分解出的两部分应用归并排序。最后,在合并阶段将两部分合并成一个排好序的集合。

 

1.2.1.3  动态规划

动态规划同分治法类似,都是将较大的问题分解为子问题最后再将结果合并。然而,它们处理问题的方式与子问题之间的关系有关。在分治法中每一个子问题都是独立的。为此,我们以递归(见第3章)的方式解决每一个子问题,然后将结果与其他子问题的结果合并。在动态规划中,子问题之间并不是独立的。换句话说,子问题之间可能有关联。这类问题采用动态规划法比分治法更合适。因为若用分治法来解决这类问题会多做很多不必要的工作,有些子问题会被重复计算多次。尽管动态规划法是一种重要的思想且很多算法都利用了这种思想,但本书介绍的算法中都没有使用到它。

 

1.2.1.4  贪心法

贪心法在求解问题时总能够做出在当前来看的最佳选择。换句话说,不是从整体最优上去考虑,而仅仅是在某种意义上的局部最优解。不幸的是当前的最优解长远来看却未必是最优的。因此,贪心法并不会一直产生最优结果。然而在某些方面来说,贪心法确是最佳选择。一个采用贪心法的例子是霍夫曼编码(见第14章),这是一个数据压缩算法。

霍夫曼编码中最重要的部分是构建一颗霍夫曼树(译者注:又称最优二叉树)。为了构建出一颗霍夫曼树,我们从它的叶子节点自底向上处理。我们将每个要处理的符号和它们在数据中出现的次数(频率)一起作为二叉树根节点保存。接下来,我们选择两颗拥有最小频率值的根节点作为左右子树,构造出一颗新的二叉树,且保证新的二叉树根节点的频率值为左右子树节点的频率值之和。然后我们重复这个步骤,直到我们得到唯一的一颗树,这就是最终的霍夫曼树。霍夫曼树的根节点包含了数据中符号的总数,叶子节点包含了原始的符号以及它们出现的频率。霍夫曼编码是一种贪心算法,因为每次它都会挑选出当前最合适的两棵树来合并。

 

1.2.1.5  近似法

 近似法并不计算出最优解,相反,它只计算出“足够好”的解。通常我们利用近似法去解决那些计算成本很高又因为其本身十分有价值而不愿放弃的问题。推销员问题(见第16章)是一个通常会用近似法去解决的问题。

设想一位推销员需要设计一条去往好几个城市工作的路线。推销员问题的目的是找到最短的可能路径,以便推销员能够在回到出发点前恰好每座城市都只去过一次。由于推销员问题可能存在一种最优的策略,但计算的代价很高,我们可以采用启发式的方法得到一个近似解。当最优策略行不通时,启发式是一种比最优策略稍逊一些的我们能够接受的策略。

推销员问题可以用图表的方式来描绘。我们把推销员必须前往的城市在格子中用点标记出来。然后我们按照如下启发式的方法来寻找这些点之间的最短路径。从推销员出发的位置开始只有一个点,将这个点涂黑。所有其他的点在加入路线图之前都是白色的,当它们加入时也同样涂黑。接下来,对于每一个还没有加入到路线图中的点v,计算最后一个加入到路线图中的点u和v之间的距离。通过这种方法选择出离u最近的那个点,将其涂黑并加入路线图中。重复这个过程直到所有的点都已经涂黑。最后再次将出发点加入路线图使其闭合,这样就完成了整个路线图。

 

PS:

1、此书(Mastering Algorithms with C)译稿版权归本人(Love_Lei)及好友(bigship)共同所有,未经本人同意谢绝一切转载,并不得抄袭,模仿,盗版!更请大家监督盗版之人!

2、由于本人水平有限,如对译文有任何建议和异议,欢迎大家留言指正,我们共同讨论学习!谢谢!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值