主頁 > 後端開發 > 【演算法】圖解八大排序

【演算法】圖解八大排序

2021-09-25 16:47:13 後端開發

八大排序

文章目錄

  • 前言
  • 一、插入排序
  • 二、希爾排序
  • 三、選擇排序
  • 四、堆排序
  • 五、冒泡排序
  • 六、快速排序
    • 遞回版
      • hoare版
      • 挖坑法
      • 前后指標法
    • 非遞回
  • 七、歸并排序
    • 遞回版
    • 非遞回
  • 八、計數排序
  • 總結


??注:本文基于C語言撰寫,由 VisualStudio 2019 所實作

前言

??在我們生活的這個世界中到處都是被排序過的東東,可以說排序是無處不在,舉例
常見的莫過于點外賣,按照「銷量最高」「好評最多」等選擇你今日的午餐;考試按照「分數高低」排名次,
分數排名
值得注意的是:排序有很多種,它們適合的情況不同,需根據不同場景運用,這些排序演算法中對應一些基本思想,了解實作原理比光看代碼更重要!

提示:以下排序均以升序實作,代碼僅供參考


??為方便資料交換,提高代碼可讀性,先寫一個交換函式
void Swap(int* p1, int* p2)
{
	int tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}

一、插入排序

??插入排序的原理應該是最容易理解的了,因為只要你打過撲克牌,應該能夠秒懂,插入排序是一種最簡單直觀的排序演算法,它的原理是通過構建有序序列,對于未排序資料,在已排序序列中從后向前掃描,找到相應位置并插入,如下圖:
插入排序

🌱演算法思想

通過不斷將當前元素 「插入」「有序」 序列中,直到所有元素都執行過 「插入」 操作,則排序結束,

??動圖演示

插入排序

圖示含義
的長柱表示正在進行 比較或移動 的數
的長柱表示已排好序的數
的長柱表示正在執行插入的數
的長柱表示待排的數

🎯演算法分析

單趟分析:

1.假定 「前n-1個」數已經有序,「第n個」數從「第 n-1個」開始,自后向前逐一比較,
2.當前一個數 大于「第n個」數時,將該「元素」往后移.
3.當遇到一個 小于等于「第n個」數的「元素」或來到「第一個元素位置」時,先將「該元素」往后移,以空出該位置,并將「第n個」數移動到此處,至此,單趟結束

多趟分析:

如何做到前n個元素有序呢?即從第一個元素開始,依次往后進行插入操作,直至最后一個元素為止

例:前5個元素已經有序

??我們看到,首先需要將 「第六個元素」 「第五個元素」 進行 「比較」 ,如果 前者 大于 后者 ,則進行 「交換」 ,然后再比較 「第六個元素」 「第四個元素」 ,以此類推,直到 「第六個元素」 被移動到 「合適位置」
??然后,進行第二輪 「比較」,直到 「第七個元素」 被移動到 「合適位置」
??最后,經過一定輪次的 「比較」 「交換」 之后,一定可以保證所有元素都是 「升序」 排列的,

🔑參考代碼
??值得注意的是:當第 i 個元素向后移時,即第 i+1 個元素的值被覆寫為第 i 個元素的值,但第 i 個元素值仍未變.因此,跳出回圈后,第 i 個元素位置即為待插入位置
代碼如下:

void InsertSort(int* a, int n)
{
	assert(a);
	//所有趟
	for (int i = 0; i < n - 1; i++)
	{
		// 單趟排序
		// end 表示有序序列最后元素的下表
		int end =  i;
		// 待插入的元素
		int tmp = a[end + 1];
		while (end >= 0)
		{
			if (tmp < a[end])
			{
				//后一個元素被前一個元素覆寫
				a[end + 1] = a[end];
				end--;
			}
			else
			{
				break;
			}
		}
		// 待插入位置,值為 tmp 
		a[end + 1] = tmp;
	}
}

🕘時間復雜度
??由圖可以看出,當待排序列越接近有序,其時間復雜度越低,越接近無序,時間復雜度越高,例如:當整體序列為「升序序列」時為O(N) ,最壞情況下,即整體序列為「降序序列」時為O( N 2 N^2 N2)

二、希爾排序

??希爾排序,也稱遞減增量排序演算法,按其設計者希爾(Donald Shell)的名字命名,該演算法由希爾 1959 年公布,是插入排序的一種更高效的改進版本,但希爾排序是非穩定排序演算法,

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

1.插入排序在對幾乎已經排好序的資料操作時,效率高,即可以達到線性排序的效率;
2.但插入排序一般來說是低效的,因為插入排序每次只能將資料移動一位;

🌱演算法思想

??希爾排序的基本思想是:先將整個待排序的記錄序列分割成為若干子序列分別進行直接插入排序,待整個序列中的記錄“基本有序”時,再對全體記錄進行依次直接插入排序,
??即,先進行預排序,使之接近有序,再進行插入排序,
??動圖演示
希爾排序1
在這里插入圖片描述

注:每一趟排序中,相同顏色為一組

🎯演算法分析

??由圖可以看出,希爾排序一個特點是:子序列的構成不是簡單的 「逐段分割」,而是將相隔某個增量 「gap」 的資料組成一個子序列,如上圖:

第一趟排序時: gap = 5 9 4 為一組, 1 8 為一組, 2 6 為一組, 3 5 為一組, 5 7 為一組,
第二趟排序時: gap = 2 4,2,5,5,8為一組, 1,3,6,7,9為一組.
第三趟排序時: gap = 1 ,整體為一組

??因為是相隔 gap 的元素為一組,每組各自進行排序,因此在整體來看,每個元素的移動是「跳躍式」
??不斷減小 gap,整體愈加接近「有序」
??當 gap = 1 時,即為 「插入排序」,只需要對以上數列進行簡單的微調,不需要大量的移動操作即可完成整個陣列的排序,

??這里有個問題: gap 取多少合適?

gap 越大,資料挪動快,但越不接近有序
gap 越小,挪動越慢,但越接近有序

??事實上,gap 的取法有多種,最初Shell提出取gap = 「n / 2」,gap=「gap / 2」,直到 gap = 1,后來Knuth提出取gap=「gap / 3」+1,還有人提出都取奇數為好,也有人提出各gap互質為好,無論哪一種主張都沒有得到證明,

🔑參考代碼

void ShellSort(int* a, int n)
{
	// gap > 1  預排序
	// gap == 1 直接插入排序
	int gap = n;
	while (gap > 1)
	{
		// 保證最后一次為 1
		gap = gap / 3 + 1;
		// 間隔為gap,多組并排
		for (int i = 0; i < n - gap; i++)
		{
			int end = i;
			int tmp = a[end + gap];
			while (end >= 0)
			{
				if (a[end] > tmp)
				{
					a[end + gap] = a[end];
					end -= gap;
				}
				else
				{
					break;
				}
			}
			a[end + gap] = tmp;
		}
	}
}

🕘時間復雜度
??時間復雜度:O(N log N)
??其中,《資料結構(C語言版)》— 嚴蔚敏 有以下說明
希爾排序

三、選擇排序

🌱演算法思想
??每一次從待排序的資料元素中選出最小(或最大)的一個元素,存放在待排序列的起始位置,直到全部待排序的資料元素排完 ,

??動圖演示
選擇排序1

圖示含義
的長柱表示正在進行比較的數
的長柱表示已排好序的數
的長柱表示最小的數
的長柱表示待排的數

🎯演算法分析

??首先,找到待排序列中「最小/大」的元素,拎出來,將它和序列的「第一個元素」交換位置,第二步,在剩下的元素中繼續尋找「最小/大」的元素,拎出來,和序列的「第二個元素」交換位置,如此回圈,直到整個序列排序完成,
??實際上,我們可以一次選出「最小」「最大」的元素,分別置于待排序列的「起始」「末尾」,以提高演算法效率,這就要求,我們必須考慮待排序列的「末端位置」,以及「最大/小」元素的「下標」

🔑參考代碼
錯誤范例:

void SelectSort(int* a, int n)
{
	int begin = 0;
	int end = n - 1;
	// 當begin 和 end 相遇,即只有一個元素
	while (begin < end)
	{
		// 分別用以記錄最大/小值下標
		int maxi = begin;
		int mini = begin;
		for (int i = begin; i <= end; i++)
		{
			//大于最大值,重新賦值
			if (a[i] > a[maxi])
			{
				maxi = i;
			}
			//小于最小值,重新賦值
			if (a[i] < a[mini])
			{
				mini = i;
			}
		}
		//將最小值與起始位置交換
		Swap(&a[mini], &a[begin]);
		//最大值與末尾交換
		Swap(&a[maxi], &a[end]);
		//對應新的首尾位置
		end--;
		begin++;
	}
	//跳出回圈,則排序完成
}

??事實上,運行結果并不正確,示例如圖
在這里插入圖片描述
??分析第二次運行,第一趟交換,可以發現,當「maxi 」「begin 」重合時,進行第一次「交換」后,「maxi 」對應值來到了 「mini」的位置,故須考慮「maxi 」是否與「begin 」重合的情況,如圖:
選擇排序分析圖
正確代碼如下:

void SelectSort(int* a, int n)
{
	int begin = 0;
	int end = n - 1;
	// 當begin 和 end 相遇,即只有一個元素
	while (begin < end)
	{
		// 分別用以記錄最大/小值下標
		int maxi = begin;
		int mini = begin;
		for (int i = begin; i <= end; i++)
		{
			if (a[i] > a[maxi])
			{
				maxi = i;
			}
			if (a[i] < a[mini])
			{
				mini = i;
			}
		}
		Swap(&a[mini], &a[begin]);
		//考慮maxi 與 begin關系,相同,則maxi對應位置變化為 mini
		if (maxi == begin)
		{
			maxi = mini;
		}
		Swap(&a[maxi], &a[end]);
		//對應新的首尾位置
		end--;
		begin++;
	}
	//跳出回圈,則排序完成
}

🕘時間復雜度
??時間復雜度:O( N 2 N^2 N2)
??直接選擇排序思想非常好理解,但是效率不是很好,實際中很少使用,且其穩定性:不穩定

四、堆排序

??堆排序(Heapsort)是指利用堆積樹(堆)這種資料結構所設計的一種排序演算法,它是選擇排序的一種,它是通過堆來進行選擇資料,需要注意的是排升序要建大堆,排降序建小堆,
💭 準備知識
大根堆和小根堆
性質:每個結點的值都大于其左孩子和右孩子結點的值,稱之為大根堆;每個結點的值都小于其左孩子和右孩子結點的值,稱之為小根堆,如圖:
在這里插入圖片描述
基本概念
查找陣列中某個數的父結點和左右孩子結點,比如已知索引為i的數,那么
父結點索引:(i - 1) / 2(取整)
左孩子索引:2 * i + 1
右孩子索引:2 * i + 2

🌱演算法思想

1.首先將待排序的陣列構造成一個大根堆,此時,整個陣列的最大值就是堆結構的頂端
2.將頂端的數與末尾的數交換,此時,末尾的數為最大值,剩余待排序陣列個數為 n - 1
3.將剩余的 n - 1 個數再構造成大根堆,再將頂端數與 n - 1 位置的數交換,如此反復執行,便能得到有序陣列

??動圖演示
建大堆:
建大堆

圖示含義
??表示正在比較的數
的矩形表示堆中正在比較的數,對應陣列元素位置

堆排序:

在這里插入圖片描述

🎯演算法分析
建大堆:

建堆有一個方法,叫做「向下調整法」 ,那么如何實作呢?
如圖:左右子樹均為大堆
在這里插入圖片描述

1.選出左右孩子中較大元素,若大于根節點,與之交換
2.原來的大孩子變為父親節點,與其孩子比較,重復步驟一,直至調整到葉子節點為止,
若孩子均小于父親節點,則無需再處理,已經是大根堆了,如圖所示:
在這里插入圖片描述
那么,如何保證左右子樹均為大根堆呢?即從最后一個根節點往前進行「向下調整法」 ,即可保證左右子樹均為大堆,

🔑參考代碼
向下調整法:

void AdjustDown(int* a, int n, int root)
{
	//創建孩子節點
	int child = root * 2 + 1;
	while (child < n)
	{
		// 若child == n - 1,不可加
		//找到最大的孩子
		if (child + 1 < n && a[child] < a[child + 1])
		{
			child++;
		}
		// 與最大的孩子進行交換
		if (a[root] < a[child])
		{
			Swap(&a[root], &a[child]);
			root = child;
			child = child * 2 + 1;
		}
		else
		{
			break;
		}
	}
}

堆排序:

void HeapSort(int* a, int n)
{
	assert(a);
	//建大堆
	int root = (n - 1 - 1) / 2;
	while (root >= 0)
	{
		AdjustDown(a, n, root);
		root--;
	}
	//堆排序
	while (n)
	{
		//首位交換,保證最大的來到最后
		Swap(&a[0], &a[n - 1]);
		// 參與排序的元素個數減 1
		n--;
		// 向下調整
		AdjustDown(a, n, 0);
	}
}

🕘時間復雜度

??時間復雜度:O(N log N)

五、冒泡排序

??冒泡排序(Bubble Sort)也是一種簡單直觀的排序演算法,它重復地走訪過要排序的數列,一次比較兩個元素,如果他們的順序錯誤就把他們交換過來,走訪數列的作業是重復地進行直到沒有再需要交換,也就是說該數列已經排序完成,這個演算法的名字由來是因為越小的元素會經由交換慢慢“浮”到數列的頂端

🌱演算法思想
??通過不斷比較相鄰的元素,如果「左邊的元素」 大于 「右邊的元素」,則進行「交換」,直到所有相鄰元素都保持升序,則排序結束,
在這里插入圖片描述

??動圖演示
在這里插入圖片描述

圖示含義
的長柱表示正在進行比較或交換的數
的長柱表示已排好序的數
的長柱表示待排的數

🎯演算法分析

1.比較相鄰的元素,如果第一個比第二個大,就交換他們兩個,
2.對每一對相鄰元素作同樣的作業,從開始第一對到結尾的最后一對,這步做完后,最后的元素會是最大的數,
3.針對所有的元素重復以上的步驟,除了最后一個,
4.持續每次對越來越少的元素重復上面的步驟,直到沒有任何一對數字需要比較,
單趟分析:

假設從第一趟開始,將「第一個」元素與「第二個」元素進行「比較」,若「前者」大于「后者」,進行「交換」,隨后,將「第二個」元素與「第三個」元素進行上述操作,直至將「倒數第二個」元素與「最后一個」元素進行「比較」,至此,「最大」元素來到了最后,

多趟分析:

假設共有 n 個數,則須進行 n - 1 趟上述操作,每一趟比較 前 n - i + 1 個數,最大元素來到第 n - i -1 的位置( i = 1,2,3~~n)

因此冒泡的代碼還是相當簡單的,兩層回圈,外層冒泡趟數,里層依次比較,江湖中人人盡皆知,

🔑參考代碼
??冒泡有一個最大的問題就是這種演算法不管你有序還是無序,閉著眼睛把你回圈比較了再說,針對這個問題,我們可以設定一個臨時遍歷來標記該陣列是否已經有序,如果有序了就不用遍歷了,
代碼如下:

void BubbleSort(int* a, int n)
{
	// 該回圈用于控制趟數
	for (int end = n - 1; end > 0; end--)
	{
		//設定臨時變數exchange,記錄資料是否交換
		int exchange = 0;
		for (int i = 0; i < end - 1; i++)
		{
			if (a[i] > a[i + 1])
			{
				Swap(&a[i], &a[i + 1]);
				//exchange 變為1,表示該趟有資料交換
				exchange = 1;
			}
		}
		//若exchange == 0,表示該趟無資料交換,已經有序,跳出回圈
		if (exchange == 0)
			break;
	}
}

🕘時間復雜度
??我們看到嵌套回圈,應該立馬就可以得出這個演算法的時間復雜度:O( N 2 N^2 N2)

六、快速排序

??快速排序(QuickSort)是對冒泡排序的一種改進,
🌱演算法思想
??它的基本思想是,通過一趟排序將待排記錄分割成獨立的兩部分,其中一部分記錄的關鍵字均比另一部分記錄的關鍵字小,則可分別對這兩部分記錄繼續進行排序,以達到整個序列有序,

遞回版

hoare版

🌱演算法思想
??快速排序是Hoare于1962年提出的一種二叉樹結構的交換排序方法,其基本思想為:任取待排序元素序列中的某元素作為基準值,按照該排序碼將待排序集合分割成兩子序列,左子序列中所有元素均小于基準值,右子序列中所有元素均大于基準值,然后最左右子序列重復該程序,直到所有元素都排列在相應位置上為止,

🎯演算法圖解
單趟:

??選擇最左邊的元素作為關鍵值key,在這里插入圖片描述
??首先哨兵「 j 」 開始出動,因為此處設定的基準數是最左邊的數,所以需要讓哨兵 「 j 」 先出動,這一點非常重要(請自己想一想為什么),哨兵「 j 」 一步一步地向左挪動(即 j – ),直到找到一個小于 「 key」 的數停下來,接下來哨兵 「 i 」 再一步一步向右挪動(即 i++ ),直到找到一個大于 「 key」的數停下來,最后哨兵 j 停在了數字 5 面前,哨兵 「 i 」 停在了數字 7 面前,交換資料…
在這里插入圖片描述
??接下來哨兵 「 j 」 繼續向左挪動(注:每次必須是哨兵「 j 」 先出發),他發現了4 (比基準數「 key」要小,滿足要求)之后停了下來,哨兵「 i 」 也繼續向右挪動,他發現了 9(比基準數 「 key」 要大,滿足要求)之后停了下來,此時再次進行交換…
在這里插入圖片描述
??第二次交換結束,“探測”繼續,哨兵 「 j 」 繼續向左挪動,他發現了 3之后又停了下來,哨兵「 i 」 繼續向右移動,糟啦!此時哨兵「 i 」 和哨兵「 j 」相遇了,說明此時“探測”結束,我們將「 key」 和哨兵「 i 」 所處地址的值進行交換,單趟探測結束,并回傳基準值的下標
在這里插入圖片描述

多趟:

??每一趟回傳一個基準值下標,表示該基準值已來到正確位置,通過回傳值,將待排序列分割左右兩組,重復上述步驟,

🔑參考代碼

單趟代碼如下:

int PartSort1(int* a, int left, int right)
{
	//選擇一個基準值
	int keyi = left;
	while (left < right)
	{
		//注意:left < right 這一判斷條件必不可少
		//遇到小于基準值則停下
		while (left < right && a[right] >= a[keyi])
		{
			right--;
		}
		//遇到大于基準值則停下
		while (left < right && a[left] <= a[keyi])
		{
			left++;
		}
		//交換
		Swap(&a[left], &a[right]);
	}
	Swap(&a[left], &a[keyi]);
	return left;
}

🕘時間復雜度
??時間復雜度:O(N log N)

挖坑法

🎯演算法圖解
單趟動圖如下:

挖坑法

如圖可以看出:

1.先選出一個值存放在key中,通常為最左或最右邊的值
2.定義LR,(若key選自最左邊,則R先走)
3.當R遇到小于key的值,將該值移動到坑(hole)中,并將該處變為新的坑(hole)
4.當L遇到大于key的值,將該值移動到坑(hole)中,并將該處變為新的坑(hole)
5.重復上述步驟3、4,當LR相遇,停止移動,并將key移動到 坑(hole) 中,至此,單趟結束,

🔑參考代碼
單趟代碼如下:

int PartSort2(int* a, int left, int right)
{
	// 所需正確移動的關鍵數
	int key = a[left];
	// 初始坑位
	int hole = left;
	while (left < right)
	{
		// 右邊,遇到小的則停止
		while (left < right && a[right] >= key)
		{
			right--;
		}
		// 填坑,更換新的坑位置
		a[hole] = a[right];
		hole = right;
		// 左邊,遇到大的則停止
		while (left < right && a[left] <= key)
		{
			left++;
		}
		// 填坑,更換新的坑位置
		a[hole] = a[left];
		hole = left;
	}
	// 當左右相遇,即 key 的正確位置
	a[hole] = key;
	return left;
}

🕘時間復雜度
??時間復雜度:O(N log N)

前后指標法

🎯演算法圖解
單趟動圖如下:
前后指標法

由圖可知其步驟如下:
1.先選出一個值存放在key中,通常為最左或最右邊的值
2.定義prevcur, prev 指向待排序列起始位置,且cur= prev + 1
3.當cur指向的值小于key,先 ++prev ,再將prev所指向的值與cur所指向的值進行交換,如此重復至cur越界
4.此時,將keyprev所指向的值交換,可以得到,key左邊值均比其小,key右邊值均比其大,

🔑參考代碼
單趟代碼如下:

int PartSort3(int* a, int left, int right)
{
	
	int keyi = left;
	int cur = left + 1;
	int prev = left;

	while (cur <= right)
	{
		if (a[cur] <= a[keyi] && ++prev != cur)
		{
			Swap(&a[cur], &a[prev]);
		}
		cur++;
	}
	Swap(&a[keyi], &a[prev]);

	return prev;
}

🕘時間復雜度
??時間復雜度:O(N log N)

📖代碼優化
??實際上,上述圖片僅僅是理想狀態下所實作,舉個例子,倘若每次選擇的key均是最大/小值,則上述區間被劃分為分別包含 n - 1 個和 0 個元素的區間,故而時間復雜度O( N 2 N^2 N2).因此選擇一個適中的key是必要的,在此給出三數取中法,如下:

??選擇給定區間的左值右值,以及中間元素的值,將次大的元素與區間首元素交換,再將key選定為新的首元素,從而保證key左右均有元素,

代碼如下:

int GetMidIndex(int* a, int left, int right)
{
	//防止溢位
	int mid = left + (right - left) / 2;

	if (a[left] > a[right])
	{
		if (a[mid] > a[left])
		{
			return left;
		}
		else if (a[mid] > a[right])
		{
			return mid;
		}
		else
		{
			return right;
		}
	}
	else
	{
		if (a[mid] > a[right])
		{
			return right;
		}
		else if (a[mid] > a[left])
		{
			return mid;
		}
		else
		{
			return left;
		}
	}
}

優化后代碼如下:

int PartSort1(int* a, int left, int right)
{
	//得到中間值元素下標
	int keyi = GetMidIndex(a, left, right);
	//與首元素交換
	Swap(&a[left], &a[keyi]);
	keyi = left;

	while (left < right)
	{
		while (left < right && a[right] >= a[keyi])
		{
			right--;
		}
		while (left < right && a[left] <= a[keyi])
		{
			left++;
		}
		Swap(&a[left], &a[right]);

	}
	Swap(&a[left], &a[keyi]);
	return left;
}

其他兩種方法優化方式同上,

🌳多趟代碼

void QuickSort(int* a, int left, int right)
{
	//遞回條件
	if (left >= right)
	{
		return;
	}
	// 中間值下標
	//int key = PartSort1(a, left, right);
	//int key = PartSort2(a, left, right);
	int key = PartSort3(a, left, right);
	//遞回左區間
	QuickSort(a, left, key - 1);
	//遞回右區間
	QuickSort(a, key + 1, right);
}

非遞回

??我們都知道,遞回會開辟新的函式堆疊幀,因此將遞回版轉化為非遞回版,需借用一個資料結構–堆疊 不熟悉的可以參看一下我的這篇文章👉【資料結構】堆疊
??通過上述文章,我們可以發現,三種遞回版本不過是單趟排序有些許差異,多趟排序仍一致且遞回實作,因此我們可以將單趟函式封裝起來,將遞回轉化為非遞回,可以發現,遞回實際上是將大區間分成許多小區間,將小區間有序化,則大區間亦有序,

🎯演算法思路

??

1.將待排區間的第一個最后一個元素下標入堆疊
2.進行一趟排序,得到回傳的key的下標,則得到新的兩段區間,將舊區間出堆疊,新區間入堆疊,
3.重復步驟2,直到堆疊為空,

🔑參考代碼

void QuickSortNonR(int* a, int left, int right)
{
	assert(a);
	//借助堆疊來實作
	//創建堆疊
	Stack st;
	//初始化堆疊
	StackInint(&st);
	//先入堆疊,否則無法進入回圈
	StackPush(&st, right);
	StackPush(&st, left);
	//當堆疊為空,則已排好序
	while (!StachEmpty(&st))
	{
		//獲取堆疊頂元素,需注意順序
		int begin = StackTop(&st);
		StackPop(&st);
		int end = StackTop(&st);
		StackPop(&st);
		//取中間值
		int mid = PartSort3(a, begin, end);
		//如果左區間范圍大于1,進行入堆疊
		if (begin < mid - 1)
		{
			StackPush(&st, mid - 1);
			StackPush(&st, begin);
		}
		//右區間同理
		if (end > mid + 1)
		{
			StackPush(&st, mid + 1);
			StackPush(&st, end);
		}

	}
	StackDestroy(&st);
	//free()
}

七、歸并排序

遞回版

🌱演算法思想
?? 歸并排序(MERGE-SORT)是建立在歸并操作上的一種有效的排序演算法,該演算法是采用分治法(Divide and Conquer)的一個非常典型的應用,將已有序的子序列合并,得到完全有序的序列;即先使每個子序列有序,再使子序列段間有序,若將兩個有序表合并成一個有序表,稱為二路歸并,

??動圖演示
圖一:

在這里插入圖片描述
圖二:

在這里插入圖片描述

🎯演算法分析

1.由圖可以發現,我們需先將待排序列分成許多子序列,直至每個序列僅有1個元素,并開辟 新的相同大小的陣列
2.將子序列元素進行排序,因為每個序列分為新的1~2 個子序列,排完子序列后賦值給原陣列,回傳上一層,再進行新一輪排序,再回傳上一輪,直至排序完成,
3.基于上述分析,可以發現,我們需先遞回,再排序,且需要一個子函式來實作遞回功能,

🔑參考代碼

void _MergeSort(int* a, int left, int right,int* tmp)
{
	if (left >= right)
		return;
	//防止left + right溢位
	int mid = left + (right - left) / 2;
	_MergeSort(a, left, mid, tmp);
	_MergeSort(a, mid + 1, right, tmp);

	//記錄left初始位置
	int index = left;
	//左區間
	int begin1 = left;
	int end1 = mid;
	//右區間
	int begin2 = mid + 1;
	int end2 = right;
	//將兩組元素進行排序至臨時陣列中
	while (begin1 <= end1 && begin2 <= end2)
	{
		if (a[begin1] < a[begin2])
			tmp[left++] = a[begin1++];
		else
			tmp[left++] = a[begin2++];
	}
	while (begin1 <= end1)
	{
		tmp[left++] = a[begin1++];
	}
	while (begin2 <= end2)
	{
		tmp[left++] = a[begin2++];
	}
	//歸并后,拷貝至原陣列
	for (int i = index; i <= right; i++)
	{
		a[i] = tmp[i];
	}
}
void MergeSort(int* a, int n)
{
	int* tmp = (int*)malloc(n * sizeof(int));
	//避免malloc失敗
	if (tmp == NULL)
	{
		printf("malloc failed\n");
		exit(-1);
	}
	_MergeSort(a, 0, n - 1, tmp);
	free(tmp);
	tmp = NULL;
}

🕘時間復雜度
??時間復雜度:O( N 2 N^2 N2) ,歸并的缺點在于需要O(N) 的空間復雜度,歸并排序的思考更多的是解決在磁盤中的外排序問題,

非遞回

🎯演算法分析

??通過上述內容,我們可以發現,每次遞回先將待排序列分為多組,每組有左右兩個區間,直至每個區間僅有一個元素后,再排序,由此我們可以先將待排序列分為多組,每個區間僅一個元素,每一趟將兩個區間進行一次歸并,再歸并下一組,下一趟排序時,新區間又是由上一趟兩個區間元素構成,基于此,我們有以下思路:

1.每一趟進行多組排序
2.每進行一趟排序,重新分組

如圖:
在這里插入圖片描述

??不過,有以下三種情況值得注意:
情況一:右區間不存在
在這里插入圖片描述
情況二:左區間不完整
在這里插入圖片描述
情況三:右區間不完整

在這里插入圖片描述

🔑參考代碼

void MergeSortNonR(int* a, int n)
{
	int* tmp = (int*)malloc(n * sizeof(int));
	//避免malloc失敗
	if (tmp == NULL)
	{
		printf("malloc failed\n");
		exit(-1);
	}
	//初始組內元素個數為1
	int GroupNum = 1;
	while (GroupNum < n)
	{
		for (int i = 0; i < n; i += 2 * GroupNum)
		{
			//左右區間
			int begin1 = i;
			int end1 = i + GroupNum - 1;
			int begin2 = i + GroupNum;
			int end2 = i + 2 * GroupNum - 1;
			//記錄區間初始位置
			int index = begin1;
			//end2越界,需要修正后歸并
			if (end2 >= n)
			{
				end2 = n - 1;
			}
			//[begin2,end2] 不存在, 修正為一個不存在的區間
			if (begin2 >= n)
			{
				begin2 = n + 1;
				end2 = n;
			}
			//end1越界,修正一下
			if (end1 >= n)
			{
				end1 = 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++];
			}
		}	
		//歸并后,拷貝至原陣列
		for (int i = 0; i < n - 1; i++)
		{
			a[i] = tmp[i];
		}
		GroupNum *= 2;
	}
	free(tmp);
}

八、計數排序

??計數排序是一種非基于比較的排序演算法,我們之前介紹的各種排序演算法幾乎都是基于元素之間的比較來進行排序的,
🌱 演算法思想
??計數排序又稱為鴿巢原理,是對哈希直接定址法的變形應用,
??動圖演示

在這里插入圖片描述

🎯演算法分析

??計數排序,顧名思義,該演算法不是通過比較資料的大小來進行排序的,而是通過統計待排序列中元素出現的次數,然后通過統計的結果將序列回收到原來的序列中,步驟如下:

1.首先,準備一個「 計數器陣列 」 ,通過一次 「 遍歷 」 ,找出最大值「 max」
2.開辟出一個大小為「 max + 1」的陣列,
3.再次 「 遍歷 」 原陣列,統計元素每個元素出現次數,每個元素值與新陣列下標「 一 一對應」,例如,數字 「5」對應新陣列下標為 5 的地址,每出現一次 「5」,則新陣列下標為 5 處的值++.
4. 「 遍歷 」 新陣列,嵌套兩層回圈,外層新陣列從頭到尾 「 遍歷 」 ,內層進行「 原陣列賦值 」,如果新陣列遍歷處值為 0 ,則無需賦值,直至遍歷完,

??有個問題值得思考:假設原陣列為{1000,1002,1005,1003,1008,1004}.那么開辟 1008 + 1個元素大小的陣列,勢必造成前1000個空間浪費,此外,倘若陣列為 {-1,-2,-3},還會出現陣列越界的情況,因此,我們必須考慮上述兩種情況,

??在此,我們再來優化一下,通過一次 「 遍歷 」 ,找出最大值「 max」和最小值「 min」.則須開辟新陣列大小為「 max - min + 1」.則對應關系相應變化,值為 i 的元素,對應新陣列下標為「 i - min」,下標為 j 的新陣列下標,對應值為「 j + min」.

🔑參考代碼
??基于上述分析,有如下代碼:

void CountSort(int* a, int n)
{
	// 定義最大/小值
	int max = a[0];
	int min = a[0];
	//找出最大/小值
	for (int i = 1; i < n; i++)
	{
		if (a[i] > max)
			max = a[i];
		if (a[i] < min)
			min = a[i];
	}
	//開辟新陣列,同時賦值為 0
	int* tmp = (int*)calloc(max - min + 1, sizeof(int));
	if (tmp == NULL)
	{
		printf("malloc failed\n");
		exit(-1);
	}
	// 遍歷原陣列,統計個數
	for (int j = 0; j < n; j++)
	{
		tmp[a[j] - min]++;
	}
	// 遍歷新陣列
	for (int i = 0,j = 0; j < max - min + 1; j++)
	{
		//進行原陣列重新賦值
		while (tmp[j]--)
		{
			a[i++] = j + min;
		}
	}
}

🕘時間復雜度
??我們看到嵌套回圈,第一反應就是這個演算法的時間復雜度:O( N 2 N^2 N2) ?但事實上并非如此,細想一下,兩層回圈,加起來不過是原陣列重新賦值,故正確的時間復雜度:O(N + range)??
??此外,計數排序也有它的局限性,計數排序只適合「 資料較為集中」,且資料均為「整形」的序列,若資料分散,則會造成資料浪費的情況,


總結

??完整代碼鏈接👉八大排序

排序演算法平均情況最好情況最壞情況穩定性
冒泡排序O( N 2 N^2 N2)O(N)O( N 2 N^2 N2)穩定
簡單選擇排序O( N 2 N^2 N2)O( N 2 N^2 N2)O( N 2 N^2 N2)不穩定
直接插入排序O( N 2 N^2 N2)O(N)O( N 2 N^2 N2)穩定
希爾排序O(N log N) ~ O( N 2 N^2 N2)O( N 1.3 N^{1.3} N1.3)O( N 2 N^2 N2)不穩定
堆排序O(N log N)O(N log N)O(N log N)不穩定
歸并排序O(N log N)O(N log N)O(N log N)穩定
快速排序O(N log N)O(N log N)O( N 2 N^2 N2)不穩定

對一千萬個資料進行排序,用時如下:
在這里插入圖片描述

??以上就是本文全部內容了,如有幫助,一鍵三連支持一下吧
在這里插入圖片描述

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/302775.html

標籤:java

上一篇:我把面試問爛了的?JVM?總結了一下(帶答案,萬字總結,精心打磨,建議收藏)

下一篇:Java多執行緒連環50問(八股文背誦版)

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【C++】Microsoft C++、C 和匯編程式檔案

    ......

    uj5u.com 2020-09-10 00:57:23 more
  • 例外宣告

    相比于斷言適用于排除邏輯上不可能存在的狀態,例外通常是用于邏輯上可能發生的錯誤。 例外宣告 Item 1:當函式不可能拋出例外或不能接受拋出例外時,使用noexcept 理由 如果不打算拋出例外的話,程式就會認為無法處理這種錯誤,并且應當盡早終止,如此可以有效地阻止例外的傳播與擴散。 示例 //不可 ......

    uj5u.com 2020-09-10 00:57:27 more
  • Codeforces 1400E Clear the Multiset(貪心 + 分治)

    鏈接:https://codeforces.com/problemset/problem/1400/E 來源:Codeforces 思路:給你一個陣列,現在你可以進行兩種操作,操作1:將一段沒有 0 的區間進行減一的操作,操作2:將 i 位置上的元素歸零。最終問:將這個陣列的全部元素歸零后操作的最少 ......

    uj5u.com 2020-09-10 00:57:30 more
  • UVA11610 【Reverse Prime】

    本人看到此題沒有翻譯,就附帶了一個自己的翻譯版本 思考 這一題,它的第一個要求是找出所有 $7$ 位反向質數及其質因數的個數。 我們應該需要質數篩篩選1~$10^{7}$的所有數,這里就不慢慢介紹了。但是,重讀題,我們突然發現反向質數都是 $7$ 位,而將它反過來后的數字卻是 $6$ 位數,這就說明 ......

    uj5u.com 2020-09-10 00:57:36 more
  • 統計區間素數數量

    1 #pragma GCC optimize(2) 2 #include <bits/stdc++.h> 3 using namespace std; 4 bool isprime[1000000010]; 5 vector<int> prime; 6 inline int getlist(int ......

    uj5u.com 2020-09-10 00:57:47 more
  • C/C++編程筆記:C++中的 const 變數詳解,教你正確認識const用法

    1、C中的const 1、區域const變數存放在堆疊區中,會分配記憶體(也就是說可以通過地址間接修改變數的值)。測驗代碼如下: 運行結果: 2、全域const變數存放在只讀資料段(不能通過地址修改,會發生寫入錯誤), 默認為外部聯編,可以給其他源檔案使用(需要用extern關鍵字修飾) 運行結果: ......

    uj5u.com 2020-09-10 00:58:04 more
  • 【C++犯錯記錄】VS2019 MFC添加資源不懂如何修改資源宏ID

    1. 首先在資源視圖中,添加資源 2. 點擊新添加的資源,復制自動生成的ID 3. 在解決方案資源管理器中找到Resource.h檔案,編輯,使用整個專案搜索和替換的方式快速替換 宏宣告 4. Ctrl+Shift+F 全域搜索,點擊查找全部,然后逐個替換 5. 為什么使用搜索替換而不使用屬性視窗直 ......

    uj5u.com 2020-09-10 00:59:11 more
  • 【C++犯錯記錄】VS2019 MFC不懂的批量添加資源

    1. 打開資源頭檔案Resource.h,在其中預先定義好宏 ID(不清楚其實ID值應該設定多少,可以先新建一個相同的資源項,再在這個資源的ID值的基礎上遞增即可) 2. 在資源視圖中選中專案資源,按F7編輯資源檔案,按 ID 型別 相對路徑的形式添加 資源。(別忘了先把檔案拷貝到專案中的res檔案 ......

    uj5u.com 2020-09-10 01:00:19 more
  • C/C++編程筆記:關于C++的參考型別,專供新手入門使用

    今天要講的是C++中我最喜歡的一個用法——參考,也叫別名。 參考就是給一個變數名取一個變數名,方便我們間接地使用這個變數。我們可以給一個變數創建N個參考,這N + 1個變數共享了同一塊記憶體區域。(參考型別的變數會占用記憶體空間,占用的記憶體空間的大小和指標型別的大小是相同的。雖然參考是一個物件的別名,但 ......

    uj5u.com 2020-09-10 01:00:22 more
  • 【C/C++編程筆記】從頭開始學習C ++:初學者完整指南

    眾所周知,C ++的學習曲線陡峭,但是花時間學習這種語言將為您的職業帶來奇跡,并使您與其他開發人員區分開。您會更輕松地學習新語言,形成真正的解決問題的技能,并在編程的基礎上打下堅實的基礎。 C ++將幫助您養成良好的編程習慣(即清晰一致的編碼風格,在撰寫代碼時注釋代碼,并限制類內部的可見性),并且由 ......

    uj5u.com 2020-09-10 01:00:41 more
最新发布
  • Rust中的智能指標:Box<T> Rc<T> Arc<T> Cell<T> RefCell<T> Weak

    Rust中的智能指標是什么 智能指標(smart pointers)是一類資料結構,是擁有資料所有權和額外功能的指標。是指標的進一步發展 指標(pointer)是一個包含記憶體地址的變數的通用概念。這個地址參考,或 ” 指向”(points at)一些其 他資料 。參考以 & 符號為標志并借用了他們所 ......

    uj5u.com 2023-04-20 07:24:10 more
  • Java的值傳遞和參考傳遞

    值傳遞不會改變本身,參考傳遞(如果傳遞的值需要實體化到堆里)如果發生修改了會改變本身。 1.基本資料型別都是值傳遞 package com.example.basic; public class Test { public static void main(String[] args) { int ......

    uj5u.com 2023-04-20 07:24:04 more
  • [2]SpinalHDL教程——Scala簡單入門

    第一個 Scala 程式 shell里面輸入 $ scala scala> 1 + 1 res0: Int = 2 scala> println("Hello World!") Hello World! 檔案形式 object HelloWorld { /* 這是我的第一個 Scala 程式 * 以 ......

    uj5u.com 2023-04-20 07:23:58 more
  • 理解函式指標和回呼函式

    理解 函式指標 指向函式的指標。比如: 理解函式指標的偽代碼 void (*p)(int type, char *data); // 定義一個函式指標p void func(int type, char *data); // 宣告一個函式func p = func; // 將指標p指向函式func ......

    uj5u.com 2023-04-20 07:23:52 more
  • Django筆記二十五之資料庫函式之日期函式

    本文首發于公眾號:Hunter后端 原文鏈接:Django筆記二十五之資料庫函式之日期函式 日期函式主要介紹兩個大類,Extract() 和 Trunc() Extract() 函式作用是提取日期,比如我們可以提取一個日期欄位的年份,月份,日等資料 Trunc() 的作用則是截取,比如 2022-0 ......

    uj5u.com 2023-04-20 07:23:45 more
  • 一天吃透JVM面試八股文

    什么是JVM? JVM,全稱Java Virtual Machine(Java虛擬機),是通過在實際的計算機上仿真模擬各種計算機功能來實作的。由一套位元組碼指令集、一組暫存器、一個堆疊、一個垃圾回收堆和一個存盤方法域等組成。JVM屏蔽了與作業系統平臺相關的資訊,使得Java程式只需要生成在Java虛擬機 ......

    uj5u.com 2023-04-20 07:23:31 more
  • 使用Java接入小程式訂閱訊息!

    更新完微信服務號的模板訊息之后,我又趕緊把微信小程式的訂閱訊息給實作了!之前我一直以為微信小程式也是要企業才能申請,沒想到小程式個人就能申請。 訊息推送平臺🔥推送下發【郵件】【短信】【微信服務號】【微信小程式】【企業微信】【釘釘】等訊息型別。 https://gitee.com/zhongfuch ......

    uj5u.com 2023-04-20 07:22:59 more
  • java -- 緩沖流、轉換流、序列化流

    緩沖流 緩沖流, 也叫高效流, 按照資料型別分類: 位元組緩沖流:BufferedInputStream,BufferedOutputStream 字符緩沖流:BufferedReader,BufferedWriter 緩沖流的基本原理,是在創建流物件時,會創建一個內置的默認大小的緩沖區陣列,通過緩沖 ......

    uj5u.com 2023-04-20 07:22:49 more
  • Java-SpringBoot-Range請求頭設定實作視頻分段傳輸

    老實說,人太懶了,現在基本都不喜歡寫筆記了,但是網上有關Range請求頭的文章都太水了 下面是抄的一段StackOverflow的代碼...自己大修改過的,寫的注釋挺全的,應該直接看得懂,就不解釋了 寫的不好...只是希望能給視頻網站開發的新手一點點幫助吧. 業務場景:視頻分段傳輸、視頻多段傳輸(理 ......

    uj5u.com 2023-04-20 07:22:42 more
  • Windows 10開發教程_編程入門自學教程_菜鳥教程-免費教程分享

    教程簡介 Windows 10開發入門教程 - 從簡單的步驟了解Windows 10開發,從基本到高級概念,包括簡介,UWP,第一個應用程式,商店,XAML控制元件,資料系結,XAML性能,自適應設計,自適應UI,自適應代碼,檔案管理,SQLite資料庫,應用程式到應用程式通信,應用程式本地化,應用程式 ......

    uj5u.com 2023-04-20 07:22:35 more