## 10.3 分治法
分治法(divide-and-conquer)是解决问题的一种常用策略,其思想是将难以处理的较大 问题分解为若干个较小的子问题,然后分别解决这些子问题,并从子问题的解构造出原问题 的解。“分”是指将原问题分解,“治”是指解决问题。
“分治”仅提及了分而治之的过程,而未提及此方法的另一个特点——递归。当我们将 大问题分解成子问题后,经常会发现子问题与大问题本质上是相同的问题,因此可以利用递 归方法来设计算法。所以,分治法常常与递归法结合起来使用。
下面我们通过排序问题来介绍分治法的应用。排序问题是指将一个数据集合中的所有数 据按从小到大的顺序(严格递增或非递减)重新排列①。计算机科学家发明了很多排序算法, 本节主要介绍利用分而治之思想设计的归并排序算法,但为了进行比较,我们先介绍没有什 么“技术含量”的选择排序算法。
选择排序
选择排序是一种朴素的排序方法,普通人都很容易想到。其思想是:先从全体 n 个数据 中找出最小值,并将该最小值排在第一个位置;然后从剩下的 n-1 个数据中再次找出最小值,这个最小值实际上是全体数据的次小值,我们将它排在第二个位置;依此类推,直至从剩下 的 2 个数据中找出最小值,排在第 n-1 个位置,而剩下的最后一个数据(全体数据中的最大 值)可以直接排在第 n 个位置。
> ① 当然也可以按从大到小的顺序(严格递减或非递增)排列,这在解决方法上并没有什么本质差别。
选择排序方法的关键步骤是找出当前剩余数据中的最小值。我们在 3.6 节中讨论过这个 问题①,并且设计了一个很好的算法:逐个检查每一个数据,并记录当前见到的最小值;当 数据检查完毕,所记录的数据就是全体数据中的最小值。下面我们利用这个求最小值的方法 来实现选择排序算法。
算法的核心部分是一个循环,每一轮循环找出剩余数据中的最小值,并将该值放到合适 位置。假设数据在列表 list 中,则第一次循环找出 list[0:n-1]中的最小值,并将该值存入 list[0] 处(原来的 list[0]数据需要挪地方,见下面介绍的实现技巧)。第二次循环从 list[1:n-1]中找 出最小值,并存入 list[1]处;依此类推,第 n-1 次循环将 list[n-2:n-1]中的最小值存入 list[n-2], 而剩下的最后一个数据自然只能存入 list[n-1]。至此,list 中存储的数据即为从小到大有序 排列的。
实现此算法时,如果没有额外的存储空间,只使用 list 本身的空间来排序,则在第一次 循环中将最小值放入 list[0]时,原先存储在其中的数据就会被覆盖。为了保留这个数据,一 个简单的技巧是将 list[0]与找到的最小值交换。即,假如最小值是 list[k],则执行
```
list[0],list[k] = list[k],list[0]
```
其他轮次的处理也是一样。为此,在循环中需要用一个变量记录最小值的位置索引。 下面的 Python 代码实现了以上设计思想,其中每轮循环找出 list[i:n-1]中的最小值(用变量 min 记录其索引位置),并放入 list[i]中。
```
>>> def selSort(list):
n = len(list)
for i in range(n-1):
min = i
for j in range(i+1,n):
if list[j] < list[min]:
min = j
list[i],list[min] = list[min],list[i]
>>> datalist = [5,2,8,3,4]
>>> selSort(datalist)
>>> print datalist
[2, 3, 4, 5, 8]
```
注意,与 3.6 中最小值算法不同的是,这里找最小值时并非记录最小值本身,而是记录最小 值的索引位置 min,即 list[min]才是当前最小值,这是为了使列表数据交换位置更方便。另 外,循环变量 i 只需从 0 取到 n-2,因为当前 n-1 个数据就位后,最后一个位置自然就是最 大值。
选择排序算法很容易设计实现,并且当数据量不大时效率也还可以,但当数据量很大时 性能很差。采用分治法可以设计一种更好的排序算法,即归并排序。
> ① 3.6 中讨论的是求最大值,但算法稍加改变即可用于求最小值。
归并排序
人们在玩扑克牌的时候,经常将手上的牌排成特定的顺序,比如按花色或按大小排序。
如果分到的牌不多,玩家一般用一只手将牌呈扇形握持,另一只手去整理排序。然而,如果 玩的是用到两三副牌的游戏,每个玩家分到的牌很多,那么玩家就会有手太小难以排序的烦 恼。这时,如果旁边坐着个观战者,玩家可以请这个观战者帮着拿一些牌,两人分别将手中 不多的牌进行排序,然后再合并两手牌以完成全部牌的排序。这就是归并排序的基本思想, 它将大任务分解成较小任务,解决了较小任务后再合并结果。下面我们详细介绍这种利用分 治法进行排序的方法。
给定一个较大的数据集合 S,先将数据平分为两部分 S1 和 S2,然后分别对 S1 和 S2 进行 排序,从而得到两个“局部有序”的序列。接下去将这两个局部有序序列合并成为“全局有 序”序列,这个过程称为归并(merge)。假设用序列 S3 存储归并结果,则具体归并方法是: 第一轮,两个局部有序的序列 S1 和 S2 分别拿出自己的局部最小值进行比较,其中更小者显 然是全局最小值,因此应放入 S3 的第一个位置。如果全局最小值来自 S1,则 S1 中原来排在 该最小值后面的数据成为新的局部最小值。第二轮,再次比较 S1 和 S2 的局部最小值,其中 更小者实际上是全局第二小的数据,因此应放入 S3 的第二个位置。第三轮以下依此类推, 不断比较 S1 和 S2 的局部最小值,并将更小者放入 S3,直至 S1(或 S2)的所有数据都已放入 S3。最后,只需将 S2(或 S1)的剩余数据按序放入 S3 的尾部,即可得到全局有序序列。图 10.5 用整理扑克牌的例子展示了这个归并排序过程。
![](https://box.kancloud.cn/2016-02-22_56cafce759b20.png)
![](https://box.kancloud.cn/2016-02-22_56cafce77abe3.png)
图 10.5 归并排序
下面是对图 10.5 所示过程的简要解释:
(a) 无序的初始扑克牌集合,牌太多导致难以一手进行排序;
(b) 一分为二,玩家和帮忙者两人各持一半牌;
(c) 两人分别对手中牌进行排序,从而得到两手局部有序的扑克牌序列;
(d) 两人比较各自手中的局部最小牌(黑桃 2 和方块 3),其中更小的黑桃 2 是全局最小 牌,将它放到存放归并结果的某个地方(比如桌子上);
(e)(f)(g) 重复(d)的做法,相继将方块 3、梅花 5 和梅花 6 放到归并结果序列中;
(h) 由于第二个序列已经没有牌了,故将第一个序列剩余的牌接在归并结果序列之后。 至此形成了全局有序序列。
通过图 10.5 的形象化演示,相信读者已经理解归并过程。现在还有一个问题:图 10.5(c) 是对图 10.5(b)的两手牌分别进行“排序”后得到的,问题是怎么排序?显然,我们又回到 了初始的“排序”问题,只不过这次的排序问题具有较小的规模:初始问题是对 6 张牌排序,
现在只需两人分别对自己的 3 张牌排序。这让我们想起了“递归”这个设计利器。是的,如果觉得 3 张牌还是太多,那么可以重复上述一分为二、局部排序、全局归并的过程。这个过程可以一直进行到只有 1 张牌的情形,这时根本无需排序,因为 1 张牌自然是局部有序的。 这样就得到了递归的奠基情形,此时无需递归,只需归并。由于满足了每次递归数据规模减 小和有奠基情形这两个条件,上述递归过程是正确的。归并排序算法的伪代码如下,其中划 线部分表现了该算法的递归结构。
算法:对 datalist 执行归并排序
输入:无序的列表 datalist
输出:有序的列表 datalist
将 datalist 一分为二:list1 和 list2
对 list1 执行归并排序
对 list2 执行归并排序
归并 list1 和 list2,结果放入 datalist
下面我们用 Python 编制一个完整的程序来实现并排序算法。程序 10.1 主要由两个函数 构成:函数 merge 用于归并两个局部有序的列表 list1 和 list2,结果放在 mergelist 中;函数 mergeSort 则利用分治法和递归实现对列表 datalist 的排序。
【程序 10.1】mergesort.py
```
def merge(list1,list2,mergelist):
i,j,k = 0,0,0
n1,n2 = len(list1),len(list2)
while i < n1 and j < n2:
if list1[i]<list2[j]:
mergelist[k] = list1[i]
i = i + 1
else:
mergelist[k] = list2[j]
j = j + 1
k = k + 1
while i < n1:
mergelist[k] = list1[i]
i = i + 1
k = k + 1
while j < n2:
mergelist[k] = list2[j]
j = j + 1
k = k + 1
def mergeSort(datalist):
n = len(datalist)
if n > 1:
m = n / 2
list1,list2 = datalist[:m],datalist[m:]
mergeSort(list1)
mergeSort(list2)
merge(list1,list2,datalist)
data = [9,2,7,6,5,3]
mergeSort(data)
print data
```
执行程序 10.1,将在屏幕上看到输出:
```
[2, 3, 5, 6, 7, 9]
```
顺便提醒读者注意:程序 10.1 中,函数 mergeSort 的形参 datalist 是列表类型,调用时 我们传递列表 data 作为实参。由于函数对列表类型的实参的修改后果是可以带出函数的①, 所以当我们将无序的 data 传给 mergeSort,等 mergeSort 执行完毕,data 就变成有序的了。
前面介绍的二分搜索算法其实也是分治法的应用,只不过将数据平分为两部分之后,只 需“治”其中一部分,另一部分可以忽略。后面的 Hanoi 塔问题也是分治法的应用。
最后小结一下分治法。解决一个问题时,经常将问题分解为较小的问题,小问题和大问 题是同类问题。解决了小问题之后,将部分解合并,形成初始问题的最终解。如果小问题完 全类似于初始问题,只是规模较小,显然可以用递归法设计算法。
- 前言
- 第 1 章 计算与计算思维
- 1.1 什么是计算?
- 1.1.1 计算机与计算
- 1.1.2 计算机语言
- 1.1.3 算法
- 1.1.4 实现
- 1.2 什么是计算思维?
- 1.2.1 计算思维的基本原则
- 1.2.2 计算思维的具体例子
- 1.2.3 日常生活中的计算思维
- 1.2.4 计算思维对其他学科的影响
- 1.3 初识 Python
- 1.3.1 Python 简介
- 1.3.2 第一个程序
- 1.3.3 程序的执行方式
- 1.3.4 Python 语言的基本成分
- 1.4 程序排错
- 1.5 练习
- 第 2 章 用数据表示现实世界
- 2.1 数据和数据类型
- 2.1.1 数据是对现实的抽象
- 2.1.1 常量与变量
- 2.1.2 数据类型
- 2.1.3 Python 的动态类型*
- 2.2 数值类型
- 2.2.1 整数类型 int
- 2.2.2 长整数类型 long
- 2.2.3 浮点数类型 float
- 2.2.4 数学库模块 math
- 2.2.5 复数类型 complex*
- 2.3 字符串类型 str
- 2.3.1 字符串类型的字面值形式
- 2.3.2 字符串类型的操作
- 2.3.3 字符的机内表示
- 2.3.4 字符串类型与其他类型的转换
- 2.3.5 字符串库 string
- 2.4 布尔类型 bool
- 2.4.1 关系运算
- 2.4.2 逻辑运算
- 2.4.3 布尔代数运算定律*
- 2.4.4 Python 中真假的表示与计算*
- 2.5 列表和元组类型
- 2.5.1 列表类型 list
- 2.5.2 元组类型 tuple
- 2.6 数据的输入和输出
- 2.6.1 数据的输入
- 2.6.2 数据的输出
- 2.6.3 格式化输出
- 2.7 编程案例:查找问题
- 2.8 练习
- 第 3 章 数据处理的流程控制
- 3.1 顺序控制结构
- 3.2 分支控制结构
- 3.2.1 单分支结构
- 3.2.2 两路分支结构
- 3.2.3 多路分支结构
- 3.3 异常处理
- 3.3.1 传统的错误检测方法
- 3.3.2 传统错误检测方法的缺点
- 3.3.3 异常处理机制
- 3.4 循环控制结构
- 3.4.1 for 循环
- 3.4.2 while 循环
- 3.4.3 循环的非正常中断
- 3.4.4 嵌套循环
- 3.5 结构化程序设计
- 3.5.1 程序开发过程
- 3.5.2 结构化程序设计的基本内容
- 3.6 编程案例:如何求 n 个数据的最大值?
- 3.6.1 几种解题策略
- 3.6.2 经验总结
- 3.7 Python 布尔表达式用作控制结构*
- 3.8 练习
- 第 4 章 模块化编程
- 4.1 模块化编程基本概念
- 4.1.1 模块化设计概述
- 4.1.2 模块化编程
- 4.1.3 编程语言对模块化编程的支持
- 4.2 Python 语言中的函数
- 4.2.1 用函数减少重复代码 首先看一个简单的用字符画一棵树的程序:
- 4.2.2 用函数改善程序结构
- 4.2.3 用函数增强程序的通用性
- 4.2.4 小结:函数的定义与调用
- 4.2.5 变量的作用域
- 4.2.6 函数的返回值
- 4.3 自顶向下设计
- 4.3.1 顶层设计
- 4.3.2 第二层设计
- 4.3.3 第三层设计
- 4.3.4 第四层设计
- 4.3.5 自底向上实现与单元测试
- 4.3.6 开发过程小结
- 4.4 Python 模块*
- 4.4.1 模块的创建和使用
- 4.4.2 Python 程序架构
- 4.4.3 标准库模块
- 4.4.4 模块的有条件执行
- 4.5 练习
- 第 5 章 图形编程
- 5.1 概述
- 5.1.1 计算可视化
- 5.1.2 图形是复杂数据
- 5.1.3 用对象表示复杂数据
- 5.2 Tkinter 图形编程
- 5.2.1 导入模块及创建根窗口
- 5.2.2 创建画布
- 5.2.3 在画布上绘图
- 5.2.4 图形的事件处理
- 5.3 编程案例
- 5.3.1 统计图表
- 5.3.2 计算机动画
- 5.4 软件的层次化设计:一个案例
- 5.4.1 层次化体系结构
- 5.4.2 案例:图形库 graphics
- 5.4.3 graphics 与面向对象
- 5.5 练习
- 第 6 章 大量数据的表示和处理
- 6.1 概述
- 6.2 有序的数据集合体
- 6.2.1 字符串
- 6.2.2 列表
- 6.2.3 元组
- 6.3 无序的数据集合体
- 6.3.1 集合
- 6.3.2 字典
- 6.4 文件
- 6.4.1 文件的基本概念
- 6.4.2 文件操作
- 6.4.3 编程案例:文本文件分析
- 6.4.4 缓冲
- 6.4.5 二进制文件与随机存取*
- 6.5 几种高级数据结构*
- 6.5.1 链表
- 6.5.2 堆栈
- 6.5.3 队列
- 6.6 练习
- 第 7 章 面向对象思想与编程
- 7.1 数据与操作:两种观点
- 7.1.1 面向过程观点
- 7.1.2 面向对象观点
- 7.1.3 类是类型概念的发展
- 7.2 面向对象编程
- 7.2.1 类的定义
- 7.2.2 对象的创建
- 7.2.3 对象方法的调用
- 7.2.4 编程实例:模拟炮弹飞行
- 7.2.5 类与模块化
- 7.2.6 对象的集合体
- 7.3 超类与子类*
- 7.3.1 继承
- 7.3.2 覆写
- 7.3.3 多态性
- 7.4 面向对象设计*
- 7.5 练习
- 第 8 章 图形用户界面
- 8.1 图形用户界面概述
- 8.1.1 程序的用户界面
- 8.1.2 图形界面的组成
- 8.1.3 事件驱动
- 8.2 GUI 编程
- 8.2.1 UI 编程概述
- 8.2.2 初识 Tkinter
- 8.2.3 常见 GUI 构件的用法
- 8.2.4 布局
- 8.2.5 对话框*
- 8.3 Tkinter 事件驱动编程
- 8.3.1 事件和事件对象
- 8.3.2 事件处理
- 8.4 模型-视图设计方法
- 8.4.1 将 GUI 应用程序封装成对象
- 8.4.2 模型与视图
- 8.4.3 编程案例:汇率换算器
- 8.5 练习
- 第 9 章 模拟与并发
- 9.1 模拟
- 9.1.1 计算机建模
- 9.1.2 随机问题的建模与模拟
- 9.1.3 编程案例:乒乓球比赛模拟
- 9.2 原型法
- 9.3 并行计算*
- 9.3.1 串行、并发与并行
- 9.3.2 进程与线程
- 9.3.3 多线程编程的应用
- 9.3.4 Python 多线程编程
- 9.3.5 小结
- 9.4 练习
- 第 10 章 算法设计和分析
- 10.1 枚举法
- 10.2 递归
- 10.3 分治法
- 10.4 贪心法
- 10.5 算法分析
- 10.5.1 算法复杂度
- 10.5.2 算法分析实例
- 10.6 不可计算的问题
- 10.7 练习
- 第 11 章 计算+X
- 11.1 计算数学
- 11.2 生物信息学
- 11.3 计算物理学
- 11.4 计算化学
- 11.5 计算经济学
- 11.6 练习
- 附录
- 1 Python 异常处理参考
- 2 Tkinter 画布方法
- 3 Tkinter 编程参考
- 3.1 构件属性值的设置
- 3.2 构件的标准属性
- 3.3 各种构件的属性
- 3.4 对话框
- 3.5 事件
- 参考文献