從之前的學習可以看到,對大型vectory要求的排序,選擇排序算法顯然不符合要求,因爲運行時間與輸入問題規模大小的平方成比例增加,對於以線性順序處理向量的元素的大多數排序算法也是如此。 所以要採用不同的方法來開發更好的排序算法。我們可以試着反過來思考。
強大的分治法(divide-and-conquer)
分治法的具體詳見
C++抽象編程——遞歸簡介(1)——遞歸範式
我們先來看看排序算法的性能爲什麼在問題規模增大後變得如此糟糕?我們之前分析過二次複雜度(即O(N^2)類)的基本特徵是,隨着問題的大小增加,運行時間增加了問題規模的兩倍(比如問題規模增加2倍,那麼運行時間要增加4倍)。 然而,反過來我們可以這樣想。 如果將二次問題的大小除以2,則可以將運行時間減少相同的四倍。 也就是說我們可以將vector除以一半,然後應用遞歸的方法繼續將問題的規模拆分,就可以成倍的減少所需的排序時間。
舉個例子,假設你有一個很大的vector需要排序。如果將vector分成兩半,然後使用選擇排序算法對這些片段進行排序,會發生什麼? 因爲選擇排序是的複雜度是二次的,每個較小的vector需要原始時間的四分之一(問題規模減少了2倍,時間就提高4倍)。 當然,你需要對這兩半分別進行排序,但是排序兩個較小vector所需的總時間仍然是排序原始vector所需的時間的一半。如果分開一個vector的兩半可以簡化整個vector排序的問題,我們將能夠大大減少排序需要的總時間。更重要的是,一旦發現如何在一個地方提高性能,就可以使用相同的算法遞歸地對每一個進行排序。
爲了確定分治法是否適用於這個排序問題,我們需要確定一個問題,即將vector分爲兩個較小的vector,然後對每個vector進行排序是否有助於解決一般問題(也就是拿一個實例來分析一下)。假設你從一個包含以下八個元素的vector開始排序:
如果將8個元素的vector劃分爲長度爲4的兩個vector,然後對每個較小的vector進行排序,就會得到下圖:
現在我們需要從這些較小的vector中取出值,並將它們以正確的順序放回到原始vector中。
合併兩個vector
從較小的排序vector重組成完整的vector比排序本身要簡單得多。這個過程我們稱爲合併(merging)。即完整排序中的第一個元素必須是v1中的第一個元素或v2中的第一個元素,以較小者爲準。回到這個例子當中,
- 我們新組成的的vector中的第一個元素是第二個vector(v2)中的第一個元素。然後將該元素添加到空的向量vec,此時我們把v2的19叉掉,表示已經取出,我們下圖的結果
- 再來一次,下一個元素只能是兩個較小向量之一中的第一個未取出的元素。比較v1中的25與v2中的30,並選擇前者:
- 重複此過程,從v1或v2中選擇較小的值,直到重構整個vector
合併排序算法
合併操作與遞歸分解相結合,產生了一種稱爲合併排序的新的排序算法,可以直接實現。 算法的基本思想可以概括如下:
- 檢查vector是否爲空或只有一個元素。如果是這樣,它肯定已經被排序。此條件用於定義遞歸的simple case。
- 將vector分成兩個較小的vector,每個vector的大小是前者的一半(意味着,不是值分成兩個vector,而是每個分開的vector還可以繼續分,重複這個過程)
- 遞歸地對每個較小的vector進行排序。
- 清除原始的vector,使其再次爲空。(用來儲存新的排序好的數字)
- 將兩個排序好的vector合併回原來的vector。
合併排序的C++代碼
合併排序思路簡單,但是實現起來並不那麼容易,下面是本人寫的C++代碼,在VS2015中編譯通過:
/*
* create by redAnt
* 2019年10月23日21:11:12
* 合併排序C++代碼
* vector爲STL自帶的C++標準類
* /
#include <iostream>
#include <vector>
using namespace std;
/*函數原型*/
void sort(vector<int> & vec);
void merge(vector<int> & vec,vector<int> & v1,vector<int> & v2);
/*主函數*/
int main(){
vector<int> vec;
for(int i = 0; i < 8; i++){
int n;
cin >> n;
vec.push_back(n); //向vec中添加數據
}
sort(vec);//執行合併排序算法
for(int k = 0; k < vec.size(); k++){
cout << vec[k] << " ";
}
return 0;
}
/*
*函數:sort
*用法:sort(vec)
*---------------
*該函數使用合併排序算法對向量的元素進行升序排序,包括以下步驟:
*1.將vector分成兩半
*2.遞歸地對每個較小的vector進行排序
*3.將兩個排序好的vector合併回原來的vector。
*/
void sort(vector<int> & vec){
int n = vec.size();
if(n <= 1) return; //當n<=1 的時候,爲simple case,直接返回
vector<int> v1,v2; //將vector分成兩半,然後分別裝進v1,v2中
for(int i = 0; i < n; i++){
if(i < n/2){
v1.push_back(vec[i]);
}else{
v2.push_back(vec[i]);
}
}
//遞歸調用
sort(v1);
sort(v2);
//清除vec ,用來裝排好序的vector
vec.clear();
//合併
merge(vec,v1,v2);
}
/*
*函數:merge
*用法:merge(vec,v1,v2);
*------------------------
*此函數將兩個排序的向量v1和v2合併到向量vec中,
*該向量vec在此操作之前應爲空。因爲輸入向量已經被排序。
*所以函數可以總是在其中一個輸入向量中選擇第一個未使用的元素來填充下一個位置。
*/
void merge(vector<int> & vec,vector<int> & v1,vector<int> & v2){
int n1 = v1.size();
int n2 = v2.size();
int p1 = 0;
int p2 = 0;
/*下面爲什麼是v1[p1++],而不是v1[p1]?自行補習i++的特性*/
while(p1 < n1 && p2 < n2){
if(v1[p1] < v2[p2]){
vec.push_back(v1[p1++]);
}else{
vec.push_back(v2[p2++]);
}
}
/*
*當上面的代碼執行完後,如果還有vector的大小爲偶數,那麼肯定有一個vector
*比較長,合併後剩下還有元素未合並進去原始的vector(至多一個元素),所以我們
*還要添加下面的判斷,找到它到底是屬於哪個vecc
*/
while(p1 < n1){
vec.push_back(v1[p1++]);
}
while(p2 < n2){
vec.push_back(v2[p2++]);
}
}
運行效果如圖:
合併排序算法的代碼可以整齊地分爲兩個函數:排序和合並。 排序代碼直接來自算法的步驟。在檢查特殊情況後,算法將原始vector分爲兩個較小的v1和v2。一旦sort代碼將所有元素複製到v1或v2中,v1,V2就已經被創建,其餘的函數會遞歸地排序這些vector,最後清除原始vector,然後調用merge來重新組合vector,從而實現合併排序。
實際上大部分的工作是通過合併函數完成的,該函數採用目標vec,以及較小的向量v1和v2。指標p1和p2標記跟蹤每一個vector的下標。 在循環的每個循環中,該函數從v1或v2選擇一個元素取較小者,並將該值添加到vec的末尾。一旦兩個較小的vector中的任何一個的元素被取盡,該函數可以簡單地從另一個vector中直接複製元素而再比較它們。實際上,因爲這些向vector中的其中一個已經在第一個while循環退出時已經耗盡,所以該函數可以將vector的其餘部分複製到vec。 其中一個vector爲空,相應的while循環將完全不執行。
這裏說一下,v1[p1++],其實我們都知道 i++返回的 i的值是自增1的。但是這個運算符返回的是自增前的值。也就是說比如 i = 2,執行
i++;
之後,就是 i = 3,但是 (i++)這個整體的值就還是 2(可以寫個程序試試)。
所以說v1[p1++]這句代碼等價於:
···
v1[p1];
p1 ++;
···
下一篇的文章我們就去分析一下這個算法的複雜度