數據結構高階–八大排序匯總

排序總覽

什麼是排序?

🔥排序:所謂排序,就是使一串記錄,按照其中的某個或某些關鍵字的大小,遞增或遞減的排列起來的操作。
✍️排序的穩定性:假定在待排序的記錄序列中,存在多個具有相同的關鍵字的記錄,若經過排序,這些記錄的相對次序保持不變,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序後的序列中,r[i]仍在r[j]之前,則稱這種排序算法是穩定的;否則稱為不穩定的。

排序的分類

插入排序

直接插入排序

基本思想:把待排序的數逐個插入到一個已經排好序的有序序列中,直到所有的記錄插入完為止,得到一個新的有序序列

✍️一般地,我們把第一個看作是有序的,所以我們可以從第二個數開始往前插入,使得前兩個數是有序的,然後將第三個數插入直到最後一個數插入

口頭說還是太抽象了,那麼我們用一個具體例子來介紹一下吧

所以直接插入排序的代碼實現如下:

void InsertSort(int* a, int n)
{
	int i = 0;
	for (i = 0; i < n - 1; i++)
	{
		int end = i;
		//先定義一個變量,將要插入的數保存起來
		int x = a[end + 1];
		//直到後面的數比前面大的時候就不移動,就直接把這個數放在end的後面
		while (end >= 0)
		{
			if (a[end] > x)
			{
				a[end + 1] = a[end];
				end--;
			}
			else
			{
				break;
			}
		}
		a[end + 1] = x;
	}
}

時間複雜度和空間複雜度

時間複雜度: 第一趟end最多往前移動1次,第二趟是2次……第n-1趟是n-1次,所以總次數是1+2+3+……+n-1=n*(n-1)/2,所以說時間複雜度是O(n^2)

最好的情況: 順序
最壞的情況: 逆序

空間複雜度:由於沒有額外開闢空間,所以空間複雜度為O(1)

直接插入排序穩定性

✍️直接插入排序在遇到相同的數時,可以就放在這個數的後面,就可以保持穩定性了,所以說這個排序是穩定的

希爾排序

🐾基本思想:希爾排序是建立在直接插入排序之上的一種排序,希爾排序的思想上是把較大的數儘快的移動到後面,把較小的數儘快的移動到後面。先選定一個整數,把待排序文件中所有記錄分成個組,所有距離為的記錄分在同一組內,並對每一組內的記錄進行排序。(直接插入排序的步長為1),這裡的步長不為1,而是大於1,我們把步長這個量稱為gap,當gap>1時,都是在進行預排序,當gap==1時,進行的是直接插入排序
🔥同樣的我們看圖說話!

我們先來一個單趟的排序:

int end = 0;
int x = a[end + gap];
while (end >= 0)
{
	if (a[end] > x)
	{
		a[end + gap] = a[end];
		end =end - gap;
	}
	else
	{
		break;
	}
}
a[end + gap] = x;

這裡的單趟排序的實現和直接插入排序差不多,只不過是原來是gap = 1,現在是gap了。
由於我們要對每一組都進行排序,所以我們可以一組一組地排,像這樣:

// gap組
for (int j = 0; j < gap; j++)
{
	int i = 0;
	for (i = 0; i < n-gap; i+=gap)
	{
		int end = i;
		int x = a[end + gap];
		while (end >= 0)
		{
			if (a[end] > x)
			{
				a[end + gap] = a[end];
				end -= gap;
			}
			else
			{
				break;
			}
		}
		a[end + gap] = x;
	}
}

也可以對代碼進行一些優化,直接一起排序,不要一組一組地,代碼如下:

int i = 0;
for (i = 0; i < n - gap; i++)// 一起預排序
{
	int end = i;
	int x = a[end + gap];
	while (end >= 0)
	{
		if (a[end] > x)
		{
			a[end + gap] = a[end];
			end -= gap;
		}
		else
		{
			break;
		}
	}
	a[end + gap] = x;
}

當gap>1時,都是在進行預排序,當gap==1時,進行的是直接插入排序。

  • gap越大預排越快,預排後越不接近有序
  • gap越小預排越慢,預排後越接近有序
  • gap==1時,進行的是直接插入排序。
  • 所以接下來我們要控制gap,我們可以讓最初gap為n,然後一直除以2直到gap變成1,也可以這樣:gap = gap/3+1。只要最後一次gap為1就可以了。

所以最後的代碼實現如下:

void ShellSort(int* a, int n)
{
	int gap = n;
	while (gap > 1)// 不要寫等於,會導致死循環
	{
		// gap > 1 預排序
		// gap == 1 插入排序
		gap /= 2;
		int i = 0;
		for (i = 0; i < n - gap; i++)// 一起預排序
		{
			int end = i;
			int x = a[end + gap];
			while (end >= 0)
			{
				if (a[end] > x)
				{
					a[end + gap] = a[end];
					end -= gap;
				}
				else
				{
					break;
				}
			}
			a[end + gap] = x;
		}
	}
}

時間複雜度和空間複雜度

時間複雜度:外層的循環次數,複雜度是O(logN)

每一組的數的個數大概是N/gap,總共有N/n/gap個組,所以調整的次數應該是(1+2+……+N/gap-1)*gap,所以我們分成兩種極端來看待這個問題:

當gap很大,也就是gap = N/2的時候,調整的次數是N/2,也就是O(N)

當gap很小,也就是gap = 1的時候,按道理來講調整的次數應該(1+2+……+N-1)*gap,應該是O(n^2),但是這時候應該已經接近有序,次數沒有那麼多,所以我們不如就看作時間複雜度為O(N)

綜上:希爾排序的時間複雜度應該是接近O(N*logN)

空間複雜度:由於沒有額外開闢空間,所以空間複雜度為O(1)

希爾排序穩定性

✍️我們可以這樣想,相同的數被分到了不同的組,就不能保證原有的順序了,所以說這個排序是不穩定的

選擇排序

直接選擇排序

🐾基本思想:每一次從待排序的數據元素中選出最小(或最大)的一個元素,存放在序列的起始位置,直到全部待排序的數據元素排完

🔥同樣的我們看圖說話!


整體排序就是begin往前走,end往後走,相遇就停下,所以整體代碼實現如下:

void SelectSort(int* a, int n)
{
	int begin = 0;
	int end = n - 1;
	while (begin < end)
	{
		int mini = begin;
		int maxi = begin;
		int i = 0;
		for (i = begin; i <= end; i++)
		{
			if (a[i] > a[maxi])
			{
				maxi = i;
			}
			if (a[i] < a[mini])
			{
				mini = i;
			}
			// 如果maxi和begin相等的話,要對maxi進行修正
			if (maxi == begin)
			{
				maxi = mini;
			}
			swap(a[begin], a[mini]);
			swap(a[end], a[maxi]);
			begin++;
			end--;
		}
	}
}

這裡說明一下,其中加了一段修正maxi的代碼,就是為了防止begin和maxi相等時,mini與begin交換會導致maxi的位置發生變化,而此時begin就是maxi,若此時交換maxi和end,換到end處的不是最大值,而是最小值mini,所以提前將mini賦值給maxi,當begin與mini交換的時候,mini處就是begin也就是最大值,這樣maxi與end交換就不會出現錯誤

時間複雜度和空間複雜度

時間複雜度:第一趟遍歷n-1個數,選出兩個數,第二趟遍歷n-3個數,選出兩個數……最後一次遍歷1個數(n為偶數)或2個數(n為奇數),所以總次數是n-1+n-3+……+2,所以說時間複雜度是O(n^2)

✍️最好的情況: O(n^2)(順序)
✍️最壞的情況: O(n^2)(逆序)

空間複雜度:由於沒有額外開闢空間,所以空間複雜度為O(1)

選擇排序穩定性

直接選擇排序是不穩定的

舉個例子:假設順序是3 2 3 0,遍歷選出最小的0,此時0與3交換,兩個3的前後順序明顯發生了變化,所以是不穩定的

堆排序

數據結構初階–堆排序+TOPK問題 – 一隻少年a – 博客園 (cnblogs.com)

這篇已經介紹過了

補充一點:堆排序是不穩定的

交換排序

冒泡排序

🐾基本思想:它重複地走訪過要排序的元素列,依次比較兩個相鄰的元素,如果順序(如從大到小、首字母從Z到A)錯誤就把他們交換過來。走訪元素的工作是重複地進行直到沒有相鄰元素需要交換,也就是說該元素列已經排序完成(依次向後比較兩個元素,將大的元素放到後面)

🔥同樣的我們看圖說話!

冒泡排序整體代碼實現如下:

void BubbleSort(int* a, int n)
{
	int i = 0;
	//外層循環,需要進行幾次排序
	for (i = 0; i < n - 1; i++)
	{
		int j = 0;
		//內部循環,比較次數
		for (j = 0; j < n - i - 1; j++)
		{
			if (a[j] > a[j + 1])
			{
				swap(a[j], a[j + 1]);
			}
		}
	}
}

✍️我們思考一個問題,假設當前的序列已經有序了,我們有沒有什麼辦法直接結束排序?就像圖中的情況,在第三次排序的時候已經有序,後面的比較是沒必要的

這當然是有的,我們可以定義一個exchange的變量,如果這趟排序發生交換就把這個變量置為1,否則就不變,不發生交換的意思就是該序列已經有序了,利用這樣一個變量我們就可以直接結束循環了

優化後的冒泡排序代碼:

void BubbleSort(int* a, int n)
{
	int i = 0;
	for (i = 0; i < n - 1; i++)
	{
		int exchange = 0;
		int j = 0;
		for (j = 0; j < n - i - 1; j++)
		{
			if (a[j] > a[j + 1])
			{
				exchange = 1;
				Swap(&a[j], &a[j + 1]);
			}
		}
		// 不發生交換
		if (exchange == 0)
			break;
	}
}

時間複雜度和空間複雜度

時間複雜度: 第一趟最多比較n-1次,第二趟最多比較n-2次……最後一次最多比較1次,所以總次數是n-1+n-2+……+1,所以說時間複雜度是O(n^2)

最好的情況: O(n)(順序)
最壞的情況: O(n^2)(逆序)

空間複雜度:由於沒有額外開闢空間,所以空間複雜度為O(1)

冒泡排序穩定性

✍️冒泡排序在比較遇到相同的數時,可以不進行交換,這樣就保證了穩定性,所以說冒泡排序數穩定的

快速排序(遞歸版本)

🐾基本思想:通過一趟排序將要排序的數據分割成獨立的兩部分,其中一部分的所有數據都比另外一部分的所有數據都要小,然後再按此方法對這兩部分數據分別進行快速排序,整個排序過程可以遞歸進行,以此達到整個數據變成有序序列

✍️快速排序的基本流程

  • 首先在待排序列中確定一個基準值,遍歷整個序列,將小於(可包含等於)基準值的元素放到基準值左邊,將大於(可包含等於)基準值的元素放到其右邊。(降序序列可將位置調整)
  • 此時基準值將序列分割成倆個部分,左邊的元素全部小於基準值,右邊的元素全部大於基準值
  • 將分割的左右倆部分進行如上倆步操作,實則為遞歸
  • 通過遞歸將左右倆側排好序,直至分割的小序列個數為1,排序全部完成

hoare版本

🐾基本思想:任取待排序元素序列中的某元素作為基準值,按照該排序碼將待排序集合分割成兩子序列,左子序列中所有元素均小於基準值,右子序列中所有元素均大於基準值,然後最左右子序列重複該過程,直到所有元素都排列在相應位置上為止。

🔥同樣的我們看圖說話!

🌰我們要遵循一個原則:關鍵詞取左,右邊先找小再左邊找大;關鍵詞取右,左邊找先大再右邊找小

🌰一次過後,2也就來到了排序後的位置,接下來我們就是利用遞歸來把key左邊區間和右邊的區間遞歸排好就可以了,如下:

遞歸左區間:[left, key-1] key 遞歸右區間:[key+1, right]

hoare版本找key值代碼實現如下:

int PartSort1(int* a, int left, int right)
{
	int key = left;
	while (left < right)
	{
		// 右邊找小
		while (left < right && a[right] >= a[key])
		{
			right--;
		}

		// 左邊找大
		while (left < right && a[left] <= a[key])
		{
			left++;
		}
		Swap(&a[left], &a[right]);
	}
	Swap(&a[key], &a[left]);

	return left;
}

快排代碼實現如下:

void QuickSort(int* a, int left, int right)
{
	if (left > right)
		return;

	int div = PartSort1(a, left, right);

	// 兩個區間 [left, div-1] div [div+1, right]
	QuickSort(a, left, div - 1);
	QuickSort(a, div + 1, right);
}

✍️我們考慮這樣一種情況,當第一個數是最小的時候,順序的時候會很糟糕,因為每次遞歸right都要走到頭,看下圖:

為了優化這裡寫了一個三數取中的代碼,三數取中就是在序列的首、中和尾三個位置選擇第二大的數,然後放在第一個位置,這樣就防止了首位不是最小的,這樣也就避免了有序情況下,情況也不會太糟糕。

下面是三數取中代碼:

int GetMidIndex(int* a, int left, int right)
{
	int mid = left + (right - left) / 2;
	if (a[mid] > a[left])
	{
		if (a[right] > a[mid])
		{
			return mid;
		}
		// a[right] <= a[mid]
		else if (a[left] > a[right])
		{
			return left;
		}
		else
		{
			return right;
		}
	}
	// a[mid] <= a[left]
	else
	{
		if (a[mid] > a[right])
		{
			return mid;
		}
		// a[mid] <= a[right]
		else if (a[left] > a[right])
		{
			return right;
		}
		else
		{
			return left;
		}
	}
}

所以加上三數取中優化後的代碼如下:

int PartSort1(int* a, int left, int right)
{
	int index = GetMidIndex(a, left, right);
	Swap(&a[index], &a[left]);
	int key = left;
	while (left < right)
	{
		// 右邊找小
		while (left < right && a[right] >= a[key])
		{
			right--;
		}

		// 左邊找大
		while (left < right && a[left] <= a[key])
		{
			left++;
		}
		Swap(&a[left], &a[right]);
	}
	Swap(&a[key], &a[left]);

	return left;
}

挖坑法

🐾基本思想:設定一個基準值(一般為序列的最左邊元素,也可以是最右邊的元素)此時最左邊的是一個坑。開闢兩個指針,分別指向序列的頭結點和尾結點(選取的基準值在左邊,則先從右邊出發。反之,選取的基準值在右邊,則先從左邊出發)。 從右指針出發依次遍歷序列,如果找到一個值比所選的基準值要小,則將此指針所指的值放在坑裡,左指針向前移。 後從左指針出發(選取的基準值在左邊,則後從左邊出發。反之,選取的基準值在右邊,則後從右邊出發),依次便利序列,如果找到一個值比所選的基準值要大,則將此指針所指的值放在坑裡,右指針向前移。 依次循環步驟,直到左指針和右指針重合時,我們把基準值放入這兩個指針重合的位置。

🔥同樣的我們看圖說話!

挖坑法我們要遵循一個原則:坑在左,右邊找小;坑在右,左邊找大

挖坑法代碼實現如下(加了三數取中算法):

int PartSort2(int* a, int left, int right)
{
	int index = GetMidIndex(a, left, right);
	Swap(&a[index], &a[left]);
	//pivot就是那個坑
	int pivot = left;
	int key = a[pivot];
	while (left < right)
	{
		// 坑在左邊,右邊找小
		while (left < right && a[right] >= key)
		{
			right--;
		}
		Swap(&a[pivot], &a[right]);
		pivot = right;

		// 坑在右邊邊,右邊找大
		while (left < right && a[left] <= key)
		{
			left++;
		}
		Swap(&a[pivot], &a[left]);
		pivot = left;
	}
	a[pivot] = key;
	return pivot;
}

前後指針法

🐾基本思想:前後指針法就是有兩個指針prevcurcur個在前,prev在後,cur在前面找小,找到了,prev就往前走一步,然後交換prevcur所在位置的值,然後cur繼續找小,直到cur走到空指針的位置就結束,最後將prev的值與key交換就完成了一次分割區間的操作

🔥同樣的我們看圖說話!

代碼實現:

int PartSort3(int* a, int left, int right)
{
	int index = GetMidIndex(a, left, right);
	Swap(&a[index], &a[left]);
	int key = a[left];
	int prev = left;
	int cur = left + 1;
	while (cur <= right)
	{
		if (a[cur] < key)
		{
			prev++;
			if (prev != cur)
				Swap(&a[cur], &a[prev]);
		}
		cur++;
	}
	Swap(&a[prev], &a[left]);
	return prev;
}

小區間優化快速排序

🐾小區間優化原理:當快速排序在遞歸過程中一直切分區間時,最後會被分成很小的區間,當區間中的數據個數很小時,其實這是已經是沒有必要進行再分割的,且最後一層基本上佔據了快速排序一半的遞歸,這是我們可以選擇其他的排序來解決這個小區間的排序

🐾還有一個我們要思考的問題就是最後這段小區間用什麼排序比較好?

希爾排序適應的是比較多的數據才有優勢,堆排序也不行,需要建堆,有點殺雞用牛刀的感覺,其他三個插入排序、選擇排序和冒泡排序相比,還是插入排序比較優,所以我們小區間選擇用插入排序進行排序

void QuickSort(int* a, int left, int right)
{
	if (left > right)
		return;
	
	int div = PartSort3(a, left, right);
	// 兩個區間 [left, div-1] div [div+1, right]
	if (div - 1 - left > 10)
	{
		QuickSort(a, left, div - 1);
	}
	else
	{
		InsertSort(a + left, (div - 1) - left + 1);
	}

	if (right - div - 1 > 10)
	{
		QuickSort(a, div + 1, right);
	}
	else
	{
		InsertSort(a + div + 1, right - (div + 1) + 1);
	}

}

快速排序(非遞歸版本)

🐾 基本思想:利用來模擬實現遞歸調用的過程,利用壓棧的順序來實現排序的順序。
給大家看一個利用棧模擬實現的動圖

🔥同樣的我們看圖說話!

我們拿數組arr=[5,2,4,7,9,1,3,6]來舉個例子:

第一步:我們先把區間的右邊界值7進行壓棧,然後把區間的左邊界值0進行壓棧,那我們取出時就可以先取到左邊界值,後取到後邊界值

第二步:我們獲取棧頂元素,先取到0給left,後取到7給right,進行單趟排序

第三步:第一趟排完後,區間被分為左子區間和右子區間。為了先處理左邊,所以我們先將右子區間壓棧,分別壓入7和5,然後壓入左子區間,3和0

第四步:取出0和3進行單趟排序

第五步:此時左子區間又被劃分為左右兩個子區間,但是右子區間只有4一個值,不再壓棧,所以只入左子區間,將1和0壓棧

第六步:取出0和1進行單趟排序

第七步:至此,左子區間全部被排完,這時候才可以出5和7排右子區間,是不是很神奇?這個流程其實和遞歸是一模一樣的,順序也沒變,但解決了遞歸的致命缺陷——棧溢出。後面的流程就不一一展現了

void QuickSortNonR(int* a, int left, int right)
{
	stack<int> s;
	s.push(right);
	s.push(left);
	
	while (!s.empty())
	{
		int newLeft = s.top();
		s.pop();
		int newRight = s.top();
		s.pop();
		
		//挖洞法
		int div = PartSort2(a, newLeft, newRight);
		// 兩個區間 [left, div-1] div [div+1, right]
		// 壓右區間
		if (div + 1 < newRight)
		{
			s.push(newRight);
			s.push(div + 1);
		}

		// 壓左區間
		if (newLeft < div - 1)
		{
			s.push(div - 1);
			s.push(newLeft);
		}
	}
}

快速排序時間複雜度和空間複雜度

空間複雜度:

最優的情況下空間複雜度為:O(logN) ;每一次都平分數組的情況

最差的情況下空間複雜度為:O( N ) ;退化為冒泡排序的情況

時間複雜度:

快速排序最優的情況下時間複雜度為:O( NlogN )

快速排序最差的情況下時間複雜度為:O( N^2 )

快速排序的平均時間複雜度也是:O(NlogN)

快速排序穩定性

快速排序顯然是不穩定的,我們試想一下:5 ….5…1….5,這種情況,交換第一個5和1的時候,顯然三個5的前後順序發生了變化,是不穩定的

歸併排序

遞歸版本

🐾 基本思想:(MERGE-SORT)是建立在歸併操作上的一種有效的排序算法,該算法是採用分治法(Divide andConquer)的一個非常典型的應用。將已有序的子序列合併,得到完全有序的序列;即先使每個子序列有序,再使子序列段間有序。若將兩個有序表合併成一個有序表,稱為二路歸併。
🌴歸併條件: 左區間有序 右區間有序

🔥同樣的我們看圖說話!

上半部分遞歸樹為將當前長度為 n 的序列拆分成長度為 n/2 的子序列,下半部分遞歸樹為合併已經排序的子序列

再來一張動態圖應該更好理解吧~

歸併排序代碼實現:

void _MergeSort(int* a, int left, int right, int* tmp)
{
	if (left >= right)
		return;
	int mid = left + (right - left) / 2;
	// 歸併條件:左區間有序 右區間有序 
	// 如何做到?遞歸左右區間
	// [left, mid] [mid + 1, right]
	_MergeSort(a, left, mid, tmp);
	_MergeSort(a, mid + 1, right, tmp);

	//歸併
	int begin1 = left;
	int end1 = mid;
	int begin2 = mid + 1;
	int end2 = right;
	int i = left;
	//對歸併的數組進行排序,暫存到tmp數組中
	while (begin1 <= end1 && begin2 <= end2)
	{
		if (a[begin1] < a[begin2])
		{
			tmp[i++] = a[begin1++];
		}
		else
		{
			tmp[i++] = a[begin2++];
		}
	}
	//兩個while循環將兩個歸併數組未加入tmp中的元素加入到tmp當中
	while (begin1 <= end1)
	{
		tmp[i++] = a[begin1++];
	}

	while (begin2 <= end2)
	{
		tmp[i++] = a[begin2++];
	}
	//將tmp數組的值賦值給數組a,因為a是指針,所以對形參進行修改對應實參也會修改
	for (i = left; i <= right; i++)
	{
		a[i] = tmp[i];
	}
}
void MergeSort(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		printf("malloc fail\n");
		exit(-1);
	}
	_MergeSort(a, 0, n - 1, tmp);

	free(tmp);
	tmp = NULL;

}

歸併排序時間複雜度和空間複雜度

時間複雜度:O(N*logN)

空間複雜度: O(N),要來一個臨時空間存放歸併好的區間的數據

歸併排序穩定性

歸併排序在遇到相同的數時,可以就先將放前一段區間的數,再放後一段區間的數就可以保持穩定性了,所以說這個排序是穩定的

非遞歸版本

🐾 基本思想: 這裡我們用循環來實現這個非遞歸的歸併排序,我們可以先兩兩一組,在四個四個一組歸併……

🔥同樣的我們看圖說話!(分兩種情況討論)

特殊情況(元素個數為2^i)

根據上面這個圖,我們可以很快的寫出一個框架,例如下面的代碼:

void MergeSortNonR(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		printf("malloc fail\n");
		exit(-1);
	}
	int gap = 1;//每趟合併後序列的長度
	while (gap < n)//合併趟數的結束條件是:最後合併後的序列長度>=數組元素的個數
	{
		int i = 0;
		//每趟進行兩兩合併
		for (i = 0; i < n; i += 2 * gap)
		{
			// [i, i+gap-1] [i+gap, i+2*gap-1]
			
			int begin1 = i;
			int end1 = i + gap - 1;
			int begin2 = i + gap;
			int end2 = i + 2 * gap - 1;
			int index = i;
			
			while (begin1 <= end1 && begin2 <= end2)
			{
				if (a[begin1] < a[begin2])
				{
					tmp[index++] = a[begin1++];
				}
				else
				{
					tmp[index++] = a[begin2++];
				}
			}

			while (begin1 <= end1)
			{
				tmp[index++] = a[begin1++];
			}

			while (begin2 <= end2)
			{
				tmp[index++] = a[begin2++];
			}

			int j = 0;
			for (j = i; j <= end2; j++)
			{
				a[j] = tmp[j];
			}
		}
		gap *= 2;
	}
	free(tmp);
	tmp = NULL;
}

一般情形(數組的元素個數不一定是2^i )

雖然元素個數不一定是 2^i 個,但是任意元素的個數為n,都必然可以拆寫成 2^j+m 個元素的情況

由圖可知,這種情況下存在兩種特殊情況:

  • 橙色箭頭代表無需合併,因為找不到配對的元素,造成該情況的原因是歸併過程中,右半區間不存在,此時我們可以不進行這次歸併,直接跳出循環,也就是begin2>=n的時候,我們就break跳出這次循環,不進行歸併
  • 綠色箭頭代表兩個長度不相等的元素也要合併,歸併過程中,左區間存在,右區間也存在,但是右區間和左區間長度不一樣,就意味着end2>=n的情況,此時我們只需要對end2進行調整,使得右區間範圍縮小,不越界,就可以繼續歸併

我們可以看到這種情況在一次歸併中僅存在一次或者零次。

所以調整後的代碼如下:

void MergeSortNonR(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		printf("malloc fail\n");
		exit(-1);
	}
	int gap = 1;
	while (gap < n)
	{
		int i = 0;
		for (i = 0; i < n; i += 2 * gap)
		{
			// [i, i+gap-1] [i+gap, i+2*gap-1]
			// 兩種需要調整的情況:
			// 1.右區間不存在
			// 2.正要歸併的右區間和左區間長度不一樣
			int begin1 = i;
			int end1 = i + gap - 1;
			int begin2 = i + gap;
			int end2 = i + 2 * gap - 1;
			int index = i;
			// 情況1:當右區間不存在的時候,右區間的範圍是[begin2,end2],所以begin2越界,就代表着右區間不存在的情況
			if (begin2 >= n)
				break;
			// 情況2,左右區間長度不一,同樣此時右區間是存在的,但是end2越界,就代表了左右區間長度不一的情況,此時我們需要做調整
			//將end2的長度設置成n-1,將原來的end2(越界)設置成數組a的最後一個元素的位置,因為不平衡的區間最後一個元素一定是數組a的最後一個元素
			if (end2 >= n)
				end2 = n - 1;
			while (begin1 <= end1 && begin2 <= end2)
			{
				if (a[begin1] < a[begin2])
				{
					tmp[index++] = a[begin1++];
				}
				else
				{
					tmp[index++] = a[begin2++];
				}
			}

			while (begin1 <= end1)
			{
				tmp[index++] = a[begin1++];
			}

			while (begin2 <= end2)
			{
				tmp[index++] = a[begin2++];
			}

			int j = 0;
			for (j = i; j <= end2; j++)
			{
				a[j] = tmp[j];
			}
		}
		gap *= 2;
	}
	free(tmp);
	tmp = NULL;
}

🌾這樣非遞歸的歸併排序就這樣被我們實現了。非遞歸歸併排序的實現的難點不在框架,而在邊界控制,我們要把邊界控制的到位,這樣就能夠很好地實現這個非遞歸

計數排序

🐾 基本思想: 它的優勢在於在對一定範圍內的整數排序時,它的複雜度為Ο(n+k)(其中k是整數的範圍),快於任何比較排序算法。 當然這是一種犧牲空間換取時間的做法

🔥同樣的我們看圖說話!

我們可以先計數出這個序列數據的範圍也就是range = max – min + 1,最大值和最小值都可以通過遍歷一遍序列來選出這兩個數。然後我們可以開一個大小為range的計數的空間count中,然後將序列中的每一個數都減去min,然後映射到count這個空間中,然後我們再一次取出並加上min依次放進原數組空間中,這樣我們就順利地完成了排序

具體代碼實現如下:

void CountSort(int* a, int n)
{
	int min = a[0];
	int max = a[0];
	int i = 0;
	for (i = 1; i < n; i++)
	{
		if (a[i] > max)
		{
			max = a[i];
		}
		if (a[i] < min)
		{
			min = a[i];
		}
	}

	int range = max - min + 1;

	int* count = (int*)malloc(sizeof(int) * range);
	if (count == NULL)
	{
		printf("malloc error\n");
		exit(-1);
	}
	// 初始化開闢的空間
	memset(count, 0, sizeof(int) * range);
	for (i = 0; i < n; i++)
	{
		count[a[i] - min]++;
	}

	int index = 0;
	for (int i = 0; i < range; i++)
	{
		while (count[i]--)
		{
			a[index++] = i + min;
		}
	}

	free(count);
	count = NULL;
}

計數排序時間複雜度和空間複雜度

空間複雜度: O(N),要來一個臨時空間存放歸併好的區間的數據

時間複雜度:O(MAX(N,範圍))(以空間換時間)

計數排序穩定性

計數排序在我們這個實現里是不穩定的

排序比較

排序方法 平均情況 最好情況 最壞情況 輔助空間 穩定性
直接插入排序 O(n^2) O(n) O(n^2) O(1) 穩定
希爾排序 O(nlogn~n^2) O(n^1.3) O(n^2) O(1) 不穩定
直接選擇排序 O(n^2) O(n^2) O(n^2) O(1) 不穩定
堆排序 O(nlogn) O(nlogn) O(nlogn) O(1) 不穩定
冒泡排序 O(n^2) O(n) O(n^2) O(1) 穩定
快速排序 O(nlogn) O(nlogn) O(n^2) O(1) 不穩定
歸併排序 O(nlogn) O(nlogn) O(nlogn) O(n) 穩定