1. 程式人生 > >資料結構——排序與查詢(2)——希爾排序(C++實現)

資料結構——排序與查詢(2)——希爾排序(C++實現)

希爾排序原理

希爾排序(Shell’s Sort),也稱為“縮小增量排序”,是一種插入排序類的演算法。最簡單的插入排序,我在上一個專欄的一篇文章C++抽象程式設計——演算法分析(8)——插入排序演算法與分析有提到過,這裡就不再贅述,這裡就只介紹一些我以前沒寫過的演算法。
希爾排序是一種改進的插入排序演算法。其基本思想如下:將整個待排序列分割成若干個自序列,然後對每個子序列分別進行直接插入排序演算法。待整個序列中的每條記錄都呈現出有序狀態的時候,再對整體進行排序。(是不是有點devid and conquer演算法的味道?)。其實直接插入排序演算法在元素較小的序列中效率還是很高的,所以希爾排序就是基於這個原理來進行分割重組的。

希爾排序過程分析

假設我們需要對下面的一組資料進行希爾排序:
49 38 65 97 76 13 27 48 55 04

  1. 根據給定的增量序列,對資料進行分組(即子序列),假設一開始增量 gap = 5(每隔5個單位取一個數).那麼就分成下面的5組序列
    (49, 13),(38, 27),(65, 48),(97,55),(76, 04)
    在這一步尤其注意,這裡說的是每隔5個單位取一個數,不是每5個連續單位取一個數!!我一開始理解的時候理解錯就很棘手。比如有人會理解成第一個序列是(49 38 65 97 76),這是不對的。而且同樣注意,增量是從第二個元素開始數起的。
  2. 然後對組的元素進行組內插入排序,也就是形成這樣的一個情形:
    (13, 49),(27, 38),(48, 65),(55, 97),(04, 76)
  3. 將較小的數字往前移動,構成第一趟的排序(注意:我們剛剛的排序是在同一個子序列中進行的比較,所以這個時候組間的較小的元素不是一步步往前挪動,而是跳躍式的往前移動(即插入)),就像這樣:
    在這裡插入圖片描述
    如果這種方式不好看,我們可以換種方式來看:
    在這裡插入圖片描述
    這樣,第一趟排序的順序就出來了。
  4. 在第一趟排序的基礎上,我們取增量gaps = 3,同上操作可以得到第二趟的排序:
    在這裡插入圖片描述
  5. 因此,這個時候我們取增量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,並取消註釋。觀察出現的問題。同樣可以在裡面加一句輸出語句,把每一趟的情況輸出出來。