1. 程式人生 > >【初探】“ 希爾排序 ”—— C++程式碼實現

【初探】“ 希爾排序 ”—— C++程式碼實現

目錄

希爾排序演算法介紹

希爾排序的基本思想

希爾排序的演算法效能

時間複雜度

         直接插入排序和希爾排序的比較



希爾排序演算法介紹


● 希爾排序是希爾(Donald Shell)於1959年提出的一種排序演算法。希爾排序也是一種插入排序,它是簡單插入排序經過改進之後的一個更高效的版本,也稱為縮小增量排序,同時該演算法是衝破O(n^2)的第一批演算法之一。 希爾排序是非穩定排序演算法。

 

①希爾排序又稱縮小增量排序 ,它本質上是一個插入排序演算法。為什麼呢?

因為,對於插入排序而言,插入排序是將當前待排序的元素與前面所有的元素比較,而希爾排序是將當前元素 與前面增量位置上的元素進行比較,然後,再將該元素插入到合適位置。當一趟希爾排序完成後,處於增量位置上的元素是有序的。

 

②希爾排序演算法的效率依賴於增量的選取

假設增量序列為 h(1),h(2)...h(k),其中h(1)必須為gap=n/2,且 h(1) < h(2) <...h(k) 。 第一趟排序時在增量為 gap=n/2 的各個元素上進行比較 第二趟排序在增量為 gap = gap/2 的各個元素上進行比較

最後一趟在增量 gap=1 上進行比較。由此可以看出,每進行一趟排序,增量是一個不斷減少的過程,因此稱之為縮小增量

當增量減少到 gap=1 時,這裡完全就是插入排序了,而在此時,整個元素經過前面的幾趟排序後,已經變得基本有序了,而我們知道,對於插入排序而言,當待排序的陣列基本有序時,插入排序的效率是非常高的。
 

希爾排序的設計體現了計算機領域的“分治法”思想。在眾多排序演算法中,目前而言,希爾排序是唯一能在效率上與快速排序一較高低的演算法,目前只有這兩種排序演算法的時間複雜度突破O(n2)。值得一提的是,希爾排序與快速排序都基於“分治法”,從這裡或許可以解釋這兩種排序演算法在效率上的得天獨厚。
 

希爾排序是基於插入排序的以下兩點性質而提出改進方法的:

插入排序在對幾乎已經排好序的資料操作時, 效率高, 即可以達到線性排序的效率

但插入排序一般來說是低效的, 因為插入排序每次只能將資料移動一位

 


希爾排序的基本思想


希爾排序的基本思想是:

先將整個待排序的記錄序列分割成為若干子序列分別進行直接插入排序,待整個序列中的記錄“基本有序”時,再對全體記錄進行依次直接插入排序。

演算法描述:

1、對於n個待排序的數列,取一個小於n的整數 gap = n/2 (gap被稱為步長)  將待排序元素分成若干個組子序列,

2、所有距離為gap的倍數的記錄放在同一個組中;然後,對各組內的元素進行直接插入排序。 這一趟排序完成之後,每一個組      的元素都是有序的。

3、然後減小 gap = gap/2 的值,並重復步驟2 和 3

4、重複這樣的操作,當gap=1時,整個數列就是有序的。
 

演算法演示:

這裡寫圖片描述

 

(2)下面以數列 {80, 30, 60, 40, 20, 10, 50, 70} 為例,演示它的希爾排序過程。

第1趟:(gap=4)

這裡寫圖片描述

當gap=4時,意味著將數列分為4個組: {80,20}, {30,10}, {60,50}, {40,70}。 對應數列: {80, 30, 60, 40, 20,10, 50, 70}

對這4個組分別進行排序,排序結果: {20,80}, {10,30}, {50,60}, {40,70}。 對應數列: {20, 10, 50, 40, 80, 30, 60, 70}

 

第2趟:(gap=2)

這裡寫圖片描述

當gap=2時,意味著將數列分為2個組:{20,50,80,60}, {10,40,30,70}。 對應數列: {20, 10, 50, 40, 80, 30, 60, 70}

注意:{20,50,80,60} 實際上有兩個有序的數列 {20,80} 和 {50,60} 組成。 {10,40,30,70} 實際上有兩個有序的數列 {10,30} 和 {40,70} 組成。

對這2個組分別進行排序,排序結果:{20,50,60,80}, {10,30,40,70}。 對應數列: {20, 10, 50, 30, 60, 40, 80, 70}

第3趟:(gap=1)

這裡寫圖片描述

當gap=1時, 意味著將數列分為1個組: { 20, 10, 50, 30, 60, 40, 80, 70 }

注意:{20,10,50,30,60,40,80,70}實際上有兩個有序的數列{20,50,60,80}和 {10,30,40,70}組成。

對這1個組分別進行排序,排序結果:{ 10, 20, 30, 40, 50, 60, 70, 80 }

 

下面繼續第三個示例圖:

這裡寫圖片描述

在上面這幅圖中:

 初始時,有一個大小為 10 的無序序列。

在第一趟排序中,我們不妨設 gap1 = N / 2 = 5,即相隔距離為 5 的元素組成一組,可以分為 5 組。

接下來,按照直接插入排序的方法對每個組進行排序。

在第二趟排序中,我們把上次的 gap 縮小一半,即 gap2 = gap1 / 2 = 2 (取整數)。這樣每相隔距離為 2 的元素組成一組,可以分為 2 組。

按照直接插入排序的方法對每個組進行排序。

在第三趟排序中,再次把 gap 縮小一半,即gap3 = gap2 / 2 = 1。 這樣相隔距離為 1 的元素組成一組,即只有一組。

按照直接插入排序的方法對每個組進行排序。此時,排序已經結束。

需要注意一下的是,圖中有兩個相等數值的元素 5 和 5 。我們可以清楚的看到,在排序過程中,兩個元素位置交換了。

所以,希爾排序是不穩定的演算法。

 

下面看程式碼示例:

 

#include<iostream>
#include<cassert>
using namespace std;

class SqList
{
public:
	SqList(size_t sizeElem);
	~SqList();
	void printElem();
	void swapElem(int &a, int &b);
	void shellSord();
	void create(const size_t length);
private:
	int *m_base; //指向陣列
	int m_length;  //記錄陣列中的個數
};

SqList::SqList(size_t sizeElem)
{
	m_base = new int[sizeElem];
	assert(m_base != nullptr);
	m_length = 0;
}
SqList::~SqList()
{
	delete[] m_base;
	m_base = nullptr;
}

void SqList::create(const size_t length)
{
	m_length = length;
	cout << "請分別輸入你想排序的這" << length << "個元素,中間以回車鍵隔開:\n";
	for (size_t i = 0; i != length; ++i)
	{
		cin >> m_base[i];
	}
	cout << endl;
}
void SqList::printElem()
{
	for (size_t i = 0; i != m_length; ++i)
	{
		cout << m_base[i] << " ";
	}
	cout << endl;
}
void SqList::swapElem(int &a, int &b)
{
	int temp = a;
	a = b;
	b = temp;
}
void SqList::shellSord()
{
	for (int gap = m_length / 2; 0 < gap ; gap /= 2)  //步長 
	{
		for (int j = gap; j < m_length; ++j)  // 共gap個組,對每一組都執行  直接插入排序
		{
			if (m_base[j] < m_base[j - gap])//每個元素與自己組內的資料進行直接插入排序  
			{
				int temp = m_base[j];  // temp 記錄要插入的元素
				int k = j - gap;
				while (( 0 <= k) && (temp < m_base[k]))
				{
					m_base[k + gap] = m_base[k];
					k -= gap;
				}
				m_base[k + gap] = temp; // 把要插入的元素插進有序陣列的相應位置
			}
		}
	}

	
}

int main()
{
	{
		int sizeCapacity(0);
		cout << "輸入陣列的最大容量:";
		cin >> sizeCapacity;
		SqList mySqList(sizeCapacity);

		while (true)
		{
			{
				cout << "\n************************ 歡迎來到來到希爾排序的世界!**********************\n" << endl
					<< "輸入0,退出程式!" << endl
					<< "輸入1,進行希爾排序!" << endl
					<< "輸入2,清屏!" << endl;
			}

			cout << "************************* 請輸入你想要使用的功能的序號 **********************" << endl;
			int select(0);
			cout << "請輸入你的選擇:";
			cin >> select;
			if (!select)
			{
				cout << "程式已退出,感謝你的使用!" << endl;
				break;
			}
			switch (select)
			{
			case 1:
			{
				cout << "請輸入你想排序陣列元素的個數:";
				int arraySize(0);
				cin >> arraySize;
				assert(arraySize != 0);

				mySqList.create(arraySize);
				cout << "先輸出排序前的元素:";
				mySqList.printElem();

				mySqList.shellSord();
				cout << "再輸出排序後的元素:";
				mySqList.printElem();
				break;
			}
			case 2:
				system("cls");
				cout << "程式已清屏!可以重新輸入!" << endl;
				break;
			default:
				cout << "輸入的序號不正確,請重新輸入!" << endl;
			}
		}
	}
	system("pause");
	return 0;
}

 


希爾排序的演算法效能


希爾排序的時間複雜度與增量(即,步長gap)的選取有關。例如,當增量為1時,希爾排序退化成了直接插入排序,此時的時間複雜度為O(N²),而希爾排序的時間複雜度為O(N3/2)。

排序類別 排序方法 時間複雜度 空間複雜度 穩定性 複雜性  
插入排序 希爾排序

平均情況 O(n*log2n)

最好情況 O(N^3/2)

最壞情況 O(n2) 

O(1) 不穩定 較複雜  

注意:不過在某些序列中最好情況的複雜度可以為O(n1.3);

希爾排序是不穩定的排序演算法:

雖然一次插入排序是穩定的,不會改變相同元素的相對順序,但在不同的插入排序過程中,相同的元素可能在各自的插入排序中移動,最後其穩定性就會被打亂。

  比如序列:{ 3, 5, 10, 8, 7, 2, 8, 1, 20, 6 },gap = 2時分成兩個子序列 { 3, 10, 7, 8, 20 } 和  { 5, 8, 2, 1, 6 } ,未排序之前第二個子序列中的8在前面,現在對兩個子序列進行插入排序,得到 { 3, 7, 8, 10, 20 } 和 { 1, 2, 5, 6, 8 } ,即 { 3, 1, 7, 2, 8, 5, 10, 6, 20, 8 } ,兩個8的相對次序發生了改變。

 


時間複雜度


步長的選擇是希爾排序的重要部分。只要最終步長為1任何步長序列都可以工作。

演算法最開始以一定的步長進行排序。然後會繼續以一定步長進行排序,最終演算法以步長為1進行排序。當步長為1時,演算法變為插入排序,這就保證了資料一定會被排序。


希爾排序最初建議步長選擇為 length/2 並且對步長取半直到步長達到1。雖然這樣取可以比O(N2)類的演算法(插入排序)更好,但這樣仍然有減少平均時間和最差時間的餘地。可能希爾排序最重要的地方在於當用較小步長排序後,以前用的較大步長仍然是有序的。比如,如果一個數列以步長5進行了排序然後再以步長3進行排序,那麼該數列不僅是以步長3有序,而且是以步長5有序。如果不是這樣,那麼演算法在迭代過程中會打亂以前的順序,那就不會以如此短的時間完成排序了。

這裡寫圖片描述

已知的最好步長序列是由Sedgewick提出的(1, 5, 19, 41, 109,…),該序列的項來自這兩個算式。

這項研究也表明“比較在希爾排序中是最主要的操作,而不是交換。”用這樣步長序列的希爾排序比插入排序和堆排序都要快,甚至在小陣列中比快速排序還快,但是在涉及大量資料時希爾排序還是比快速排序慢。
 


直接插入排序和希爾排序的比較


直接插入排序是穩定的;而希爾排序是不穩定的。

直接插入排序更適合於原始記錄基本有序的集合。

希爾排序的比較次數和移動次數都要比直接插入排序少,當N越大時,效果越明顯。

在希爾排序中,增量序列gap的取法必須滿足:最後一個步長必須是 1 。

直接插入排序也適用於鏈式儲存結構;希爾排序不適用於鏈式結構。