如果牌堆中的纸牌不是按顺序排列的,那就没有比线性查找更快的查找方法了。我们必须查看每张纸牌,因为除此之外我们无法确定要找的纸牌是不是在其中。
但是查词典时,我们并不是从头到尾、一个词一个词的查。因为单词是以字母顺序排列的,所以我们可以使用类似于二分查找的算法:
1. 从中间某个位置开始。
2. 在这一页上选择一个单词,并用这个单词和我们要查找的单词比较。
3. 如果这就是我们要找的单词,结束。
4. 如果我们要找的单词在这一页上的单词之后,翻到后面某页并回到第2步。
5. 如果我们要找的单词在这一页上的单词之前,翻到词典前面某页并回到第2步。
如果遇到了这种情况,某页上有两个相邻的单词,而要找的单词应该在它们之间,这时即可确定词典中没要我们要查的单词。唯一的例外就是单词印错地方了,但这就与单词按字母顺序排列的假设冲突了。
在牌堆这个例子中,如果知道纸牌是有序摆放的,我们就能写一个更快的find函数。编写二分查找最好的方法是利用递归函数,因为二分自然而然是递归的。
窍门是编写findBisect函数,它以两个索引值low和high为参数,用以确定要查找的向量的一段(包括low和high指定的元素)。
1. 要在向量中查找,选择low和high之间的一个索引,我们称之为mid。将mid指定的纸牌与我们要查找的牌相比。
2. 如果找到,结束。
3. 如果mid处的纸牌比要找的牌大,继续在low和mid-1确定的区间中查找。
4. 如果mid处的纸牌比要找的牌小,继续在mid+1和high确定的区间中查找。
可以怀疑第3步和第4步看起来像递归调用。我们将其转化为C++代码,看起来是这个样子的:
~~~
int findBisect (const Card& card, const apvector<Card>& deck,
int low, int high) {
int mid = (high + low) / 2;
// 如果找到了纸牌,返回其index
if (equals (deck[mid], card)) return mid;
// 否则,将纸牌与中间的纸牌比较
if (deck[mid].isGreater (card)) {
// 查找纸牌的前一半
return findBisect (card, deck, low, mid-1);
} else {
// 查找纸牌的后一半
return findBisect (card, deck, mid+1, high);
}
}
~~~
虽然这段代码已经包含了二分查找的核心,但还是缺点什么。按照当前代码,如果要找的纸牌不再牌堆中,它会一直递归下去。我们需要找到一种方法来检查这一条件并做出适当的处理(通过返回-1)。
识别出目标纸牌不在牌堆中最简单的方法是,牌堆中没有纸牌,也就是high小于low的情况。当然,牌堆中仍然有牌,我的意思是由low和high指定的段中没有纸牌。
添加了high
~~~
int findBisect (const Card& card, const apvector<Card>& deck,
int low, int high) {
cout << low << ", " << high << endl;
if (high < low) return -1;
int mid = (high + low) / 2;
if (equals (deck[mid], card)) return mid;
if (deck[mid].isGreater (card)) {
return findBisect (card, deck, low, mid-1);
} else {
return findBisect (card, deck, mid+1, high);
}
}
~~~
我在开头添加了一行输出语句,这样我们能查看递归调用序列并说服我们递归会走到基本条件。尝试下面代码
~~~
cout << findBisect (deck, deck[23], 0, 51));
~~~
其输出如下:
~~~
0, 51
0, 24
13, 24
19, 24
22, 24
I found the card at index = 23
~~~
然后,我编造了1张不在牌堆中的牌——方块15,然后试一下查找它,输出如下:
~~~
0, 51
0, 24
13, 24
13, 17
13, 14
13, 12
I found the card at index = -1
~~~
这些测试并不能证明程序的正确性,其实再多的数据也无法证明程序是正确的。但是看几个例子并检验代码,也许可以说服你自己。
递归调用的次数相当少,通常是6或7次。也就是说equals函数和isGreater函数只需要调用6或7次,而线性查找最多要调用52次。一般而言,二分查找比线性查找快的多,尤其是向量中元素数目很多时,效果更为明显。
递归程序中两个常见错误,一个是忘记了包含基本条件,另一个是递归调用永远取不到基本条件。每个错误都会导致无穷递归,这种情况下C++最终会出现运行时错误。
- 第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 术语表