D&C并不那么容易掌握,我将通过三个示例来介绍。首先,介绍一个直观的示例; 然后, 介绍一个代码示例,它不那么好看,

但可能更容易理解;最后,详细介绍快速排序——一种使用D&C的排序算法。

假设你是农场主,有一小块土地。

你要将这块地均匀地分成方块,且分出的方块要尽可能大。显然,下面的分法都不符合要求。

如何将一块地均匀地分成方块,并确保分出的方块是最大的呢?使用D&C策略! D&C算法是递归的。使用D&C解决问题的过程包括两个步骤。

  1. 找出基线条件,这种条件必须尽可能简单。
  2. 不断将问题分解(或者说缩小规模),直到符合基线条件。

下面就来使用D&C找出前述问题的解决方案。可你能使用的最大方块有多大呢?

首先,找出基线条件。最容易处理的情况是,一条边的长度是另一条边的整数倍。

如果一边长25 m,另一边长50 m,那么可使用的最大方块为 25 m×25 m。换言之,可以将这块地分成两个这样的方块。

现在需要找出递归条件,这正是D&C的用武之地。根据D&C的定义,每次递归调用都必须缩小问题的规模。如何缩小前述问题的规模呢?我们首先找出这块地可容纳的最大方块。

你可以从这块地中划出两个640 m×640 m的方块,同时余下一小块地。现在是顿悟时刻:何不对余下的那一小块地使用相同的算法呢?

最初要划分的土地尺寸为1680 m×640 m,而现在要划分的土地更小,为640 m×400 m。 适用于这小块地的最大方块,也是适用于整块地的最大方块。换言之,你将均匀划分1680 m×640 m土地的问题,简化成了均匀划分640 m×400 m土地的问题!

欧几里得算法

前面说“适用于这小块地的最大方块,也是适用于整块地的最大方块”,如果你觉得这一点不好理解,也不用担心。这确实不好理解,但遗憾的是,要证明这一点,需要的篇幅有点长,在本书中无法这样做,因此你只能选择相信这种说法是正确的。如果你想搞明白其中的原因,可参阅欧几里得算法。可汗学院很清楚地阐述了这种算法,网址为

https://www.khanacademy.org/computing/computer-science/ryptography/modarithmetic/a/the-euclidean-algorithm

下面再次使用同样的算法。对于640 m × 400 m的土地,可从中划出的最大方块为400 m × 400 m。

这将余下一块更小的土地,其尺寸为400 m × 240 m。

你可从这块土地中划出最大的方块,余下一块更小的土地,其尺寸为240 m × 160 m。

接下来, 从这块土地中划出最大的方块,余下一块更小的土地。

余下的这块土地满足基线条件,因为160是80的整数倍。将这块土地分成两个方块后,将不会余下任何土地!

因此,对于最初的那片土地,适用的最大方块为80 m× 80 m。

这里重申一下D&C的工作原理:

  1. 找出简单的基线条件;
  2. 确定如何缩小问题的规模,使其符合基线条件。

D&C并非可用于解决问题的算法,而是一种解决问题的思路。我们再来看一个例子。

给定一个数字数组。

你需要将这些数字相加,并返回结果。使用循环很容易完成这种任务。

def sum(arr):
    total = 0
    for x in arr:
        total += x
    return total

print sum([1, 2, 3, 4])

但如何使用递归函数来完成这种任务呢?

第一步:找出基线条件。最简单的数组什么样呢?请想想这个问题,再接着往下读。如果数组不包含任何元素或只包含一个元素,计算总和将非常容易。

因此这就是基线条件。

第二步:每次递归调用都必须离空数组更近一步。如何缩小问题的规模呢?下面是一种办法。

这与下面的版本等效。

这两个版本的结果都为12,但在第二个版本中,给函数sum传递的数组更短。换言之, 这缩小了问题的规模!

函数sum的工作原理类似于下面这样。

这个函数的运行过程如下。

别忘了, 递归记录了状态。

提 示

编写涉及数组的递归函数时,基线条件通常是数组为空或只包含一个元素。陷入困境时,请检查基线条件是不是这样的。

函数式编程一瞥

你可能想,既然使用循环可轻松地完成任务,为何还要使用递归方式呢?看看函数式编程你就明白了!诸如Haskell等函数式编程语言没有循环,因此你只能使用递归来编写这样的函数。如果你对递归有深入的认识,函数式编程语言学习起来将更容易。例如,使用Haskell时,你可能这样编写函数sum。

sum [] = 0

sum (x : xs) = x + (sum xs)

注意,这就像是你有函数的两个定义。符合基线条件时运行第一个定义,符合递归条件时运行第二个定义。也可以使用Haskell语言中的if语句来编写这个函数。

sum arr = if arr == []
    then 0
    else (head arr) + (sum (tail arr))

但前一个版本更容易理解。 Haskell大量使用了递归,因此它提供了各种方便实现递归的语法。如果你喜欢递归或想学习一门新语言,可以研究一下Haskell。

results matching ""

    No results matching ""