12.9 二分查找
如果牌堆中的纸牌不是按顺序排列的,那就没有比线性查找更快的查找方法了。我们必须查看每张纸牌,因为除此之外我们无法确定要找的纸牌是不是在其中。
但是查词典时,我们并不是从头到尾、一个词一个词的查。因为单词是以字母顺序排列的,所以我们可以使用类似于二分查找的算法:
- 从中间某个位置开始。
- 在这一页上选择一个单词,并用这个单词和我们要查找的单词比较。
- 如果这就是我们要找的单词,结束。
- 如果我们要找的单词在这一页上的单词之后,翻到后面某页并回到第2步。
- 如果我们要找的单词在这一页上的单词之前,翻到词典前面某页并回到第2步。
如果遇到了这种情况,某页上有两个相邻的单词,而要找的单词应该在它们之间,这时即可确定词典中没要我们要查的单词。唯一的例外就是单词印错地方了,但这就与单词按字母顺序排列的假设冲突了。
在牌堆这个例子中,如果知道纸牌是有序摆放的,我们就能写一个更快的find函数。编写二分查找最好的方法是利用递归函数,因为二分自然而然是递归的。
窍门是编写findBisect函数,它以两个索引值low和high为参数,用以确定要查找的向量的一段(包括low和high指定的元素)。
- 要在向量中查找,选择low和high之间的一个索引,我们称之为mid。将mid指定的纸牌与我们要查找的牌相比。
- 如果找到,结束。
- 如果mid处的纸牌比要找的牌大,继续在low和mid-1确定的区间中查找。
- 如果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++最终会出现运行时错误。