1. 程式人生 > >排序演算法之希爾排序和堆排序

排序演算法之希爾排序和堆排序

希爾排序簡述:

希爾排序可以看作是直接插入排序的一種優化,即把一個數插入一個有序表,不過希爾排序多了一個預設的增量,把一個序列分成若干個增量大小的增量序列,然後在子序列直接進行直接插入排序,每次都使較小的數排到靠前的子序列裡面,增量不斷減小,子序列也不斷縮小,最終使整個表有序。

排序前的準備:

準備了一個預設的順序表

#define MAXSIZE 10
//順序表結構
template <class T>
struct SqList
{
	T s[MAXSIZE + 1] = { NULL, 98, 24, 55, 81, 32, 77, 48, 60, 14, 8 };          //陣列s,s[0]用作哨兵
	int length = MAXSIZE;              //s長度
	void PrintArray();
};
typedef SqList<int> SList;

//交換l中的陣列中下標為i和j的值
void Swap(SList *L, int i, int j)
{
	int temp = L->s[i];
	L->s[i] = L->s[j];
	L->s[j] = temp;
}

希爾排序實現:

這裡的length為10,增量選擇為length/3 +1=4。

增量的選擇可以是任意小於length的一個數,但最後要等於1,即能進行最後一次直接插入排序即可。

//希爾排序
//設定一個增量,來進行直接插入排序。
void ShellSort(SList *L)
{
	int i, j;
	int increment = L->length;       //增量	
	do
	{
		increment = increment / 3 + 1;      //增量
		for (i = increment+1; i <= L->length; i++)      //遍歷從第一個增量序列的後一個元素開始
		{
			if (L->s[i] < L->s[i-increment])       //增量序列後一個元素小於增量序列的第一個元素
			{
				L->s[0] = L->s[i];         //把s[0]哨兵賦值為s[i]
				for (j = i-increment; j>0 && L->s[0]<L->s[j] ;j-= increment)
				{//只迴圈一次
					L->s[j + increment] = L->s[j];         //將增量序列後一個元素賦值為增量序列的第一個元素
				}
				L->s[j+increment] = L->s[0];         //最後改變增量序列的第一個元素為哨兵值
			}
		}
	} while (increment > 1); //當增量小於等於1時終止迴圈
}

希爾排序時間複雜度:

希爾排序的時間複雜度和增量的選取有關係,並不是完全相同的。

最優情況下的時間複雜度為O(n^1.3);

最壞情況下的時間複雜度為O(n^2);

堆排序簡述:

堆排序利用了完全二叉樹的性質來進行,堆排序把一個順序表先構造成一個大頂堆,大頂堆即所有父節點比子節點大的一個完全二叉樹,構造成大頂堆後,把根節點(即當前最大值)和最後的元素交換,並繼續把長度減一的剩餘順序表構造成大頂堆,如此迴圈。

堆排序實現:

這裡碰到了一個問題是如何將順序表構造成大頂堆?

程式碼如下:

//構造成大頂堆
void HeapAdjust(SList *L,int i,int length)
{
	int temp, j;
	temp = L->s[i];                         //臨時變數賦值為當前父節點,用於後續的比較
	for (j = 2 * i; j <= length; j *= 2)    //j=2*i指向左孩子節點
	{
		if (L->s[j]<L->s[j+1] && j<length)            //如果左孩子比右孩子小,則j指向右孩子.j<length說明j不是最後節點
		{
			++j;
		}
		if (temp>L->s[j])           //如果一開始的父節點比兩個子節點都大,break
		{
			break;
		}
		L->s[i] = L->s[j];          //父節點比子節點小,把父節點賦值為兩個孩子中較大的一個
		i = j;                      //父節點指向子節點中較大的一個繼續迴圈
	}
	L->s[i] = temp;                 //把子節點較大的一個賦值為父節點
}
堆排序如下:
//堆排序
//將無序數列構造成大頂堆,把根節點和層序遍歷最後節點互換,然後把剩下元素繼續構造大頂堆
void HeapSort(SList *L)
{
	int i;
	for (i = L->length / 2; i > 0; i--)       //i=Length/2 因為完全二叉樹的父節點數量為葉子節點的一半
	{
		HeapAdjust(L, i, L->length);       //將整個序列構造成大頂堆
	}
	for (i = L->length; i > 1; i--)
	{
		Swap(L, 1, i);                  //把根節點和最後一位互換
		HeapAdjust(L, 1, i - 1);        //繼續構造大頂堆
	}
}
程式首先從完全二叉樹的父節點開始遍歷到根節點(即(length/2)個父節點),將這些父節點下面的小子樹分別構造成大頂堆,最終得到一個完全的大頂堆;

然後從尾到頭開始遍歷,把最大的數和最後節點交換,並繼續遍歷除最大節點意外剩餘節點,最終即得到一個從小到大的有序表。

堆排序時間複雜度:

構建一個大頂堆從最下層最右邊的父節點開始遍歷,進行交換,構造堆需要的時間複雜度為O(n);

而重建堆需要的時間複雜度為O(nlogn),所以整體的堆排序時間複雜度為O(nlogn)。

堆排序不適合元素數量較少的情況。