在第13.7节,我们见到一个简单的排序算法,结果它不够高效。要排序n个项目,该算法必须遍历向量n次,而且每次遍历花的时间也是与n成比例的。因此,总时间与**n2**(这里表示n平方,下同)成比例。
本节我们会简单介绍一个更高效的算法——归并排序。要对n个项目进行排序,归并排序消耗的时间与nlogn成比例。这个数字看起来可能不会给人留下深刻印象,但是随着n增大之后,n2和nlogn的差距是巨大的。你可以自己找一些n值来试试看。
归并排序背后的基本思路是:如果有两个子牌堆,每个都是已经排序好的,那将它们合并成一个有序的牌堆是很容易的(而且很快速):
1. 形成两个子牌堆,每个牌堆大约10张纸牌,分别排序,正面朝上时最小的牌在最上面。让这两个牌堆都正面朝你。
2. 比较每个牌堆最上面的纸牌,选择小的并将它翻过来放到归并后的牌堆中。
3. 重复步骤2直到其中一个牌堆为空时为止。然后将剩下的牌加到归并后的牌堆中。
我们得到的结果就是一个有序的牌堆。该算法的伪代码看起来是这个样子的:
~~~
Deck merge (const Deck& d1, const Deck& d2) {
// 创建一个足够保存所有牌的新牌堆
Deck result (d1.cards.length() + d2.cards.length());
// 使用索引i记录在当前处理的第一个牌堆中的位置
// 使用索引j记录第二个牌堆中的位置
int i = 0;
int j = 0;
// 索引k用于遍历保存结果的牌堆
for (int k = 0; k<result.cards.length(); k++) {
// 如果d1为空,选择d2;如果d2为空,则选择d1
// 否则,比较两张纸牌
// 将选择的纸牌加入到结果牌堆中
}
return result;
}
~~~
因为两个参数是对称的,所以我选择将merge设计为非成员函数。要测试merge函数,最好的方法莫过于创建一副牌并洗牌,使用subdeck函数将牌分为两小堆,然后使用上一章的sort函数将两个子牌堆排序。之后,你就可以把这两个子牌堆传给merge函数来验证下它能否正常工作了。
如果你能让这一想法称为可行的,请尝试一下mergeSort的一个简单实现:
~~~
Deck Deck::mergeSort () const {
// 找到牌堆的中点
// 将牌堆划分为两个子牌堆
// 使用sort函数对子牌堆进行排序
// 合并两个子牌堆并返回结果
}
~~~
注意,当前对象声明为const,因为mergeSort不需要修改它。 相反,函数中创建了一个新的Deck对象并返回。
如果你能让这一版本正常工作,真正有趣的事情要开始了!mergesort的神奇之处在于,它是递归的。在对子牌堆进行排序时,为什么要调用老版本的较慢的sort函数?为什么不调用我们正在编写的这个出色的、新的mergeSort函数?
这不仅是一个好想法,为了取得我承诺的性能优势,这也是必要的。尽管为了让它能正常工作,你必须添加一个基本条件,这样才不会无限递归下去。一个简单的基本条件是,子牌堆中有没有牌或者只有1张牌。如果mergesort接受的是这样小的子牌堆,不需要修改就可以直接返回,因为这样的牌堆已经是有序的。
递归版本的mergesort看起来应该是这个样子的:
~~~
Deck Deck::mergeSort (Deck deck) const {
// 如果牌堆中只有0或1张纸牌,直接返回该牌堆
// 找到牌堆中的中点
// 将牌堆划分为两个子牌堆
// 使用mergeSort对子牌堆进行排序
// 将两个子牌堆合并到一起并返回该结果
}
~~~
像往常一样,思考递归程序有两种方法:一个是考虑清楚完整的执行流程,另一个就是通过“思路跳跃”的方式。我有意的通过构造这个例子鼓励你使用“思路跳跃”来思考问题。
当使用sort来对子牌堆进行排序的时候,你并没有感觉到不得不跟踪执行流程,对不对? 这正是因为你假设了sort函数能正常工作,因为你已经调试过这个函数。好了,要让mergeSort成为递归的,所有你需要做的就是用这个排序函数替换掉老的。没有理由读不同的程序。
实际上你必须考虑正确的基本条件,还要确认最终能到达基本条件,但除此之外,写一个递归版本应该是没什么问题的。祝你好运!
- 第1章 编程之路
- 1.1 什么是编程语言
- 1.2 什么是程序
- 1.3 什么是调试
- 1.4 形式语言与自然语言
- 1.5 第一个程序
- 1.6 术语表
- 第2章 变量和类型
- 2.1 更多的输出
- 2.2 值
- 2.3 变量
- 2.4 赋值
- 2.5 输出变量
- 2.6 关键字
- 2.7 操作符
- 2.8 操作顺序
- 2.9 操作符
- 2.10 组合
- 2.11 术语表
- 第3章 函数
- 3.1 浮点数
- 3.2 double到int的转换
- 3.3 数学函数
- 3.4 函数组合
- 3.5 添加新函数
- 3.6 定义与使用
- 3.7 多函数编程
- 3.8 参数与参数值
- 3.9 参数和变量的局部性
- 3.10 多参函数
- 3.11 有返回值的函数
- 3.12 术语表
- 第4章 条件和递归
- 4.1 取模操作符
- 4.2 条件执行
- 4.3 选择执行
- 4.4 链式条件
- 4.5 嵌套条件
- 4.6 return语句
- 4.7 递归
- 4.8 无穷递归
- 4.9 递归函数的栈图
- 4.10 术语表
- 第5章 有返回值的函数
- 5.1 返回值
- 5.2 程序开发
- 5.3 组合
- 5.4 重载
- 5.5 布尔值
- 5.6 布尔变量
- 5.7 逻辑操作符
- 5.8 布尔函数
- 5.9 从main函数返回
- 5.10 深入递归
- 5.11 思路跳跃
- 5.12 又一个例子
- 5.13 术语表
- 第6章 迭代
- 6.1 多次赋值
- 6.2 迭代
- 6.3 while语句
- 6.4 制表
- 6.5 二维表
- 6.6 封装和泛化
- 6.7 函数
- 6.8 再说封装
- 6.9 局部变量
- 6.10 再说泛化
- 6.11 术语表
- 第7章 字符串那些事儿
- 7.1 字符串的容器
- 7.2 apstring变量
- 7.3 从字符串中提取字符
- 7.4 字符串长度
- 7.5 遍历
- 7.6 一个运行时错误
- 7.7 find函数
- 7.8 我们自己的find版本
- 7.9 循环与计数
- 7.10 增量与减量操作符
- 7.11 字符串连接
- 7.12 apstring是可变的
- 7.13 apstring是可比较的
- 7.14 字符分类
- 7.15 其他apstring函数
- 7.16 术语表
- 第8章 结构体
- 8.1 复合值
- 8.2 Point对象
- 8.3 访问实例变量
- 8.4 对结构体的操作
- 8.5 作为参数的结构
- 8.6 传值调用
- 8.7 传引用调用
- 8.8 矩形
- 8.9 作为返回值的结构
- 8.10 按引用传递其他类型
- 8.11 获取用户输入
- 8.12 术语表
- 第9章 再谈结构体
- 9.1 Time结构体
- 9.2 printTime函数
- 9.3 对象函数
- 9.4 纯函数
- 9.5 const参数
- 9.6 修改函数
- 9.7 填充函数
- 9.8 哪个最佳?
- 9.9 增量开发vs高屋建瓴
- 9.10 泛化
- 9.11 算法
- 9.12 术语表
- 第10章 向量
- 10.1 元素访问
- 10.2 向量的复制
- 10.3 for循环
- 10.4 向量的长度
- 10.5 随机数
- 10.6 统计
- 10.7 随机数的向量
- 10.8 计数
- 10.9 检查其他值
- 10.10直方图
- 10.11一次遍历的方案
- 10.12随机种子
- 10.13术语表
- 第11章 成员函数
- 11.1 对象和函数
- 11.2 print
- 11.3 隐式变量访问
- 11.4 另一个例子
- 11.5 再一个例子
- 11.6 更复杂的例子
- 11.8 初始化还是构造?
- 11.7 构造函数
- 11.9 最后一个例子
- 11.10 头文件
- 11.11 术语表
- 第12章 对象的向量
- 12.1 组合
- 12.2 纸牌对象(Card)
- 12.3 printCard函数
- 12.4 equals函数
- 12.5 isGreater函数
- 12.6 纸牌的向量
- 12.7 printDeck函数
- 12.8 查找
- 12.9 二分查找
- 12.10 牌堆与子牌堆
- 12.11 术语表
- 第13章 基于向量的对象
- 13.1 枚举类型
- 13.2 switch语句
- 13.3 牌堆
- 13.4 另一个构造函数
- 13.5 Deck成员函数
- 13.6 洗牌
- 13.7 排序
- 13.8 子牌堆
- 13.9 洗牌与发牌
- 13.10 归并排序
- 13.11 术语表
- 第14章 类与不变式
- 14.1 私有数据和私有类
- 14.2 什么是类?
- 14.3 复数
- 14.4 访问函数(Accessor functions)
- 14.5 输出
- 14.6 复数相关函数(一)
- 14.7 复数相关函数(二)
- 14.8 不变式
- 14.9 先决条件
- 14.10 私有函数
- 14.11 术语表
- 第15章 文件输入/输出与apmatrix类
- 15.1 流
- 15.2 文件输入
- 15.3 文件输出
- 15.4 解析输入
- 15.5 解析数字
- 15.6 集合数据结构Set
- 15.7 apmatrix类
- 15.8 距离矩阵
- 15.9 一个更合理的距离矩阵
- 15.10 术语表