如果牌堆中的紙牌不是按順序排列的,那就沒有比線性查找更快的查找方法了。我們必須查看每張紙牌,因為除此之外我們無法確定要找的紙牌是不是在其中。
但是查詞典時,我們并不是從頭到尾、一個詞一個詞的查。因為單詞是以字母順序排列的,所以我們可以使用類似于二分查找的算法:
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 術語表