在第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 術語表