排序算法之直接插入排序、二分插入排序和希爾排序


下列所有排序都默認是升序,從小到大。

插入排序

有一個已經有序的數據序列,要求在這個已經排好的數據序列中插入一個數,但要求插入後此數據序列仍然有序,這個時候就要用到一種新的排序方法——插入排序法,插入排序的基本操作就是將一個數據插入到已經排好序的有序數據中,從而得到一個新的、個數加一的有序數據,算法適用於少量數據的排序時間複雜度爲O(n^2)。是穩定的排序方法。插入算法把要排序的數組分成兩部分:第一部分包含了這個數組的所有元素,但將最後一個元素除外(讓數組多一個空間纔有插入的位置),而第二部分就只包含這一個元素(即待插入元素)。在第一部分排序完成後,再將這個最後元素插入到已排好序的第一部分中。

直接插入排序


插入排序的過程


插入排序就像有的人打撲克一樣,每次摸起來一張牌後,在手中已經排序好的撲克中插入當前摸起的這張牌。問題的關鍵點就是:如何在已經排好序的序列中,找到當前元素該插入的位置


當序列中只有一個元素時,不需要排序,因爲一個元素已經有序。


現在來考慮有多個元素的過程:




因爲一個元素已經有序,所以需要從第二個元素開始找插入位置,即需要有一層循環從第二個元素遍歷到最後一個元素。

然後來思考在循環內該怎麼做?

首先我們應該知道,在一個已經就緒的序列中插入一個元素,肯定會導致插入位置之後的所有元素都會向後移動一位, 要牢記這一點。

循環內:
我們可以先把當前把插入的元素先保存起來,然後分別將剛纔保存起來的元素與有序序列中的每一個元素比較,那麼比較到什麼時候才停下來?
a. 出現一個比保存的元素小的元素(saveNum < array[ i ] );
b.以序隊列已經比較完畢(i >=0 )。
因爲以序隊列的範圍是從 0 .. 當前插入元素位置 -1 , 所以我們需要一個循環來完成比較和搬遷元素。
當循環條件不滿足時,即找到插入位置。直接插入即可。

參考代碼:

void InsertSort(int* array, int size)
{
	if (array == NULL || size <= 0)
		return;

	// 從數組第二個元素開始處理,因爲一個元素已經有序
	for (int i = 1; i < size; i++)
	{
		int temp = array[i];
		int end = i - 1;
		
		// 插入array[i] 到已序序列中 array[0.. i-1]
		while (end >= 0 && temp < array[end])
		{
			array[end + 1] = array[end];
			end--;
		}

		// 插入 array[i]
		array[end + 1] = temp;
	}
}

// 打印數組
void PrintfArray(string info , int* array, int size)
{
	cout << info << ":";
	for (int i = 0; i < size - 1; ++i)
		cout << array[i] << " ";
	cout << endl;
}

int main()
{
	int array[] = {5, 9, 1, 6, 2, 4, 7, 8, 0 , 2, 0};
	int size = sizeof(array) / sizeof(array[0]);
	PrintfArray("排序前", array, size);
	InsertSort(array, size);
	PrintfArray("排序後", array, size);
	return 0;
}


輸出:
排序前:5 9 1 6 2 4 7 8 0 2
排序後:0 0 1 2 2 4 5 6 7 8


二分插入排序


我們知道,插入排序最重要的就是在已序序列中找到插入位置,在上面的代碼中,我們每次找位置,都需要一個一個挨着遍歷元素,如果插入位置在第以序序列的頭部(從後往前比較),那麼時間複雜度將是線性的,最壞情況O(N)。

細心的同仁們已經發現在以序序列,那麼是不是可以把二分查找的思想搬遷過來,可以提高找位置的效率呢?     文章:二分查找遞歸和循環實現

當然是可以的。

思路主要是利用在有序序列查找元素複雜度爲對數級別,從而優化了找元素的時間。但是搬移元素的時間是必不可少的。

參考代碼:
void InsertSort_Binary(int * array, int size)
{
	if (array == NULL || size <= 0)
		return;

	for (int i = 1; i < size; ++i)
	{
		
		int temp = array[i];

		int left = 0;     // 有序序列的左邊界
		int right = i - 1;// 有序序列的右邊界

		while (left <= right)
		{
			int mid = (left + right) / 2;
			// 注意這裏的 等號,爲了保證算法的穩定性(相同關鍵字排序前後位置不會變)
			// 所以也需要向後移動
			if (array[mid] <= temp)
				left = mid + 1;
			else if (array[mid] > temp)
				right = mid - 1;
			
		}

		// 搬移數據, 上面循環結束後,left的位置就是插入位置
		// 需要將從left 到 當前插入元素的前一個位置都搬移一個位置
		for (int j = i - 1; j >= left; --j)
		{
			array[j + 1] = array[j];
		}
		array[left] = temp;
	}
}



希爾排序

希爾排序是插入排序的改良版,雖然可以使用二分來較少找元素插入位置的時間。但是主要的時間消耗都在搬移元素,而且一次只能搬移一個元素。而希爾排序是在指直接插入排序的基礎上,添加了一個排序增量的方法。把記錄按下標的一定增量分組,對每組使用直接插入排序算法排序;隨着增量逐漸減少,每組包含的關鍵詞越來越多,當增量減至1時,整個文件恰被分成一組,算法便終止。使用不同的希爾增量會有不同的改進效果。

希爾排序算法是直接插入排序算法的一種改進,減少了其複製的次數,速度要快很多。 原因是,當n值很大時數據項每一趟排序需要移動的個數很少,但數據項的距離很長。當n值減小時每一趟需要移動的數據增多,此時已經接近於它們排序後的最終位置。 正是這兩種情況的結合才使希爾排序效率比插入排序高很多。

參考代碼:
增量數值可參考網上數學分析過的數值,下面代碼舉例給了321。

void ShellSort(int*array , int size)
{
	int gap = 3;
	while (gap > 0)
	{
		for (int idx = gap; idx < size; idx += gap)
		{
			int temp = array[idx];

			int end = idx - gap;
			while (end >= 0 && array[end] > temp)
			{
				array[end + gap] = array[end];
				end -= gap;
			}
			array[end + gap] = temp;
		}
		gap--;
	}
}




總結一下:如果目標是把n個元素的序列升序排列,那麼採用插入排序存在最好情況和最壞情況。最好情況就是,序列已經是升序排列了,在這種情況下,需要進行的比較操作需(n-1)次即可。最壞情況就是,序列是降序排列,那麼此時需要進行的比較共有n(n-1)/2次。插入排序的賦值操作是比較操作的次數加上 (n-1)次。平均來說插入排序算法的時間複雜度爲O(n^2)。因而,插入排序不適合對於數據量比較大的排序應用。但是,如果需要排序的數據量很小,例如,量級小於千,那麼插入排序還是一個不錯的選擇。

發佈了127 篇原創文章 · 獲贊 199 · 訪問量 32萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章