資料結構——排序與查詢(2)——希爾排序(C++實現)
希爾排序原理
希爾排序(Shell’s Sort),也稱為“縮小增量排序”,是一種插入排序類的演算法。最簡單的插入排序,我在上一個專欄的一篇文章C++抽象程式設計——演算法分析(8)——插入排序演算法與分析有提到過,這裡就不再贅述,這裡就只介紹一些我以前沒寫過的演算法。
希爾排序是一種改進的插入排序演算法。其基本思想如下:將整個待排序列分割成若干個自序列,然後對每個子序列分別進行直接插入排序演算法。待整個序列中的每條記錄都呈現出有序狀態的時候,再對整體進行排序。(是不是有點devid and conquer演算法的味道?)。其實直接插入排序演算法在元素較小的序列中效率還是很高的,所以希爾排序就是基於這個原理來進行分割重組的。
希爾排序過程分析
假設我們需要對下面的一組資料進行希爾排序:
49 38 65 97 76 13 27 48 55 04
- 根據給定的增量序列,對資料進行分組(即子序列),假設一開始增量 gap = 5(每隔5個單位取一個數).那麼就分成下面的5組序列
(49, 13),(38, 27),(65, 48),(97,55),(76, 04)
在這一步尤其注意,這裡說的是每隔5個單位取一個數,不是每5個連續單位取一個數!!我一開始理解的時候理解錯就很棘手。比如有人會理解成第一個序列是(49 38 65 97 76),這是不對的。而且同樣注意,增量是從第二個元素開始數起的。 - 然後對組的元素進行組內插入排序,也就是形成這樣的一個情形:
(13, 49),(27, 38),(48, 65),(55, 97),(04, 76) - 將較小的數字往前移動,構成第一趟的排序(注意:我們剛剛的排序是在同一個子序列中進行的比較,所以這個時候組間的較小的元素不是一步步往前挪動,而是跳躍式的往前移動(即插入)),就像這樣:
如果這種方式不好看,我們可以換種方式來看:
這樣,第一趟排序的順序就出來了。 - 在第一趟排序的基礎上,我們取增量gaps = 3,同上操作可以得到第二趟的排序:
- 因此,這個時候我們取增量gaps = 1,也就是這個時候退化為簡單的插入排序。這個時候序列基本有序,適合用簡單的插入演算法。所以,第三趟的排序就是:
**04 13 27 38 48 49 55 65 76 97 **
排序完成。
下面再給出一個例項:
希爾排序的不確定性
我們從上面的步驟可以看出,這個增量gap的值是我們事先給定的,不同的gap值會產生不同的排序序列,也就是說,排序的時間與gap的取值有關,當gap = 1的時候,退化為簡單的排序演算法。對於如何取這樣的gap值,是個難解的問題。常見的gap取值可以參見:Shellsort
希爾排序的實現
先貼一段虛擬碼:
# Sort an array a[0...n-1].
gaps = [701, 301, 132, 57, 23, 10, 4, 1]
# Start with the largest gap and work down to a gap of 1
foreach (gap in gaps)
{
# Do a gapped insertion sort for this gap size.
# The first gap elements a[0..gap-1] are already in gapped order
# keep adding one more element until the entire array is gap sorted
for (i = gap; i < n; i += 1)
{
# add a[i] to the elements that have been gap sorted
# save a[i] in temp and make a hole at position i
temp = a[i]
# shift earlier gap-sorted elements up until the correct location for a[i] is found
for (j = i; j >= gap and a[j - gap] > temp; j -= gap)
{
a[j] = a[j - gap]
}
# put temp (the original a[i]) in its correct location
a[j] = temp
}
}
如果理解有困難,我大致講一下,思路是這樣的,先把給定的增量放在一個數組中存放,這個時候,第一層迴圈是遍歷我們的增量,第二層迴圈是從我們增量的元素下標後面開始遍歷後面的元素,最後一層是遍歷我們的分組,並且根據情況判斷是否進行交換。我在寫的時候,特意寫了一個C++程式碼,也能成功執行,程式碼貼上:
#include <iostream>
#include <vector>
using namespace std;
/*函式原型*/
void shellsort(vector<int> & vec,vector<int> gaps);
/*主函式*/
int main() {
vector<int> vec,gaps;
cout << "請輸入待排序的序列" << endl;
for (int i = 0; i < 12; i++) {
int n;
cin >> n;
vec.push_back(n);
}
cout << "請輸入增量:" << endl;
for (int j = 0; j < 3; j++)
{
int m;
cin >> m;
gaps.push_back(m);
}
shellsort(vec, gaps);//shellsort(vec);//使用希爾排序對vector進行排序
cout << "希爾排序後:" << endl;
for (int k = 0; k < vec.size(); k++) {
cout << vec[k] << " ";
}
return 0;
}
void shellsort(vector<int> &vec, vector<int> gaps) {
int i,j,k,temp;
for (i = 0; i < gaps.size();i++) {//遍歷所給的增量gaps
//對於vec增量後的每一個元素,都執行下面的操作
for (k = gaps[i]; k < vec.size();k++){
temp = vec[k];//暫存第增量個元素
//cout << "這個時候的temp值為:" << temp;
//將此時增量後面的第k個元素作為分組的末尾元素,用插入演算法比較
for (j = k; j >= gaps[i] && vec[j - gaps[i] ]> temp; j -= gaps[i]) {
vec[j] = vec[j - gaps[i]];//將vec下標較小值部分賦值給較大的那邊
//cout << " 最裡面的vec[j]的值" << vec[j];
}
vec[j] = temp;
//cout << "外部的vec[j]= " << vec[j] << endl;
}
}
}
測試資料跟結果如下:
寫程式碼的過程
瞎扯一下,寫這段程式碼著實不是很容易。在初期,我犯了一個錯誤,就是到後面把gaps[i]當k使用了,因為有一段語句是將gaps[i]的值賦值給k。而我忽略了k是有在遞增變化的,而gaps[i]在跳出迴圈時始終是不變的。在尋找問題的過程中,我添加了程式碼(就是我註釋掉的那三句),用來追蹤迴圈中的值變化,從而找到相應的問題。這是一種常用的找迴圈錯誤的方法。有興趣的可以試試將gaps[i]改為k,並取消註釋。觀察出現的問題。同樣可以在裡面加一句輸出語句,把每一趟的情況輸出出來。