1. 程式人生 > >【資料結構】各類排序演算法及其優化總結

【資料結構】各類排序演算法及其優化總結

本文對各類排序演算法的實現、優化、複雜度、穩定性、適用場景作以全面總結,為了突出演算法的簡潔、易懂,去除了一些冗餘操作,預設為升序進行模擬。
在這裡插入圖片描述

一、插入排序

插入排序基本思想:每一步將一個待排序的元素,按其排序碼的大小,插入到前面已經排好序的一組元素的合適位置上去,直到元素插完。

☞ 直接插入排序

在這裡插入圖片描述
基本思想:當我們插入第i(i>=1)個元素時,前面的所有元素已經排好序,此時我們使用當前元素從後向前比較,直到找到一個小於array[i]的節點(若沒有,則插在陣列下標為0的地方),從下一個節點順序後移,將array[i]插入進來。

時間複雜度: O(n^2)
空間複雜度: O(1)
穩定性:穩定

適用場景:1.資料量較小 2.基本接近有序

//直接插入排序
int InsertSort(int *num,int len)
{
    assert(num);
    int i = 0;
    //第一個元素已經為有序序列,所以要進行len-1次排序
    for (; i < len - 1; i++)
    {
        int end = i;
        int tmp = num[i + 1];//儲存非有序區間第一個元素,否則在後邊的移動中會改變
        //比較後移
        while (end >= 0 && num[end]
>tmp) { num[end+1] = num[end]; --end; } //插入到適當位置 num[end + 1] = tmp; } }
☞ 二分插入排序(優化)

基本思想:在直接插入排序的基礎上進行優化,因為待插入元素之前的元素已經有序,我們沒必要每個都遍歷,藉助而二分法的思想可以有效降低查詢的時間。

void BinaryInsertSort(int *arr, int size)
{
	if (arr == NULL || size <= 0)
		return
; int index = 0; for (int idx = 1; idx < size; ++idx) { int tmp = arr[idx];//儲存待插入元素 int left = 0; int right = idx - 1; int mid = (left&right) + ((left^right) >> 1); //二分法查詢插入位置 while (left <= right) { if (tmp < arr[mid]) { right = mid - 1; index = mid;//更新插入位置 } else if (tmp >= arr[mid]) { left = mid + 1; index = mid + 1;//更新插入位置 } mid = (left&right) + ((left^right) >> 1);//縮小空間 } //後移元素 for (int j = idx; j > index; j--) { arr[j] = arr[j - 1]; } //插入新元素 arr[index] = tmp; } }
☞ 希爾排序

在這裡插入圖片描述
基本思想:希爾排序是插入排序的一個變種。不同之處在於我們按步長gap分組,對每組的記錄採用直接插入排序,隨著步長的逐漸減少,分組包含的記錄越來越多,直到gap=1時,構成了一個有序記錄。

時間複雜度: O(n^1.25) ~ 1.6*O(n^1.25)
空間複雜度: O(1)
穩定性:不穩定

適用場景:資料量較大,有序

下圖中為了方便,對gap以gap/2來計算,但最優的演算法是gap/3+1。
在這裡插入圖片描述

void ShellSort(int* arr, int size)
{
	if (arr == NULL || size <= 0)
		return;
	int gap = size;//gap為增量
	while (gap > 1)
	{
		gap = gap / 3 + 1;//這樣給是最優的
		for (int idx = gap; idx < size; ++idx)
		{
			int end = idx - gap;//分組後,當前元素的前一個元素
			int key = arr[idx];//儲存當前元素
			//按升序排序
			while (end >= 0 && arr[end] > key)
			{
				arr[end + gap] = arr[end];
				end -= gap;
			}
			arr[end + gap] = key;
		}
	}
}

二、選擇排序

基本思想:每一趟(例如第i趟,i=0,1,…,n-2)在後面n-i個待排序的資料元素集合中選出關鍵碼最小的資料元素,作為有序元素序列的第i個元素,待到第n-2趟做完,待排序元素集合中只剩 下1個元素,排序結束。

時間複雜度: O(n^2)
空間複雜度: O(1)
穩定性:不穩定

適用場景:資料量較小,交換次數比較少

☞ 選擇排序(單邊縮小空間)

在這裡插入圖片描述
基本思想:我們定義一個maxIndex來標記區間內最大值的下標,第一次遍歷完陣列後,更新maxIndex的指向,與下標為end(最後一個元素)的元素交換,end–縮小空間,繼續遍歷更新maxIndex。

void SelectSort(int* arr, int size)
{
	if (arr == NULL || size <= 0)
		return;
	for (int end = size - 1; end > 0; --end)
	{
		int maxIndex = end;//最大下標
		for (int idx = 0; idx < end; ++idx)
		{
			//比最大的元素還大,更新最大下標
			if (arr[idx] > arr[maxIndex])
				maxIndex = idx;
		}
		//交換end與最大元素的值
		std::swap(arr[maxIndex], arr[end]);
	}
}
☞ 選擇排序(雙邊縮小空間1.0)

基本思想:每一趟遍歷我們需要找出最大元素的下標與最小元素的下標,然後用處於最小下標的元素與begin(首元素下標)交換,用最大下標的元素與end(尾元素下標),最後begin++,end–同時縮短區間,直到begin與end相等。
注意如果begin與maxIndex相同,begin與minPos交換之後,maxPos也應該指向minPos。

void SelectSort(int* arr, int size)
{
	if (arr == NULL || size <= 0)
		return;
	int begin = 0;
	int end = size - 1;
	while (begin < end)
	{
		int maxPos = begin;//最大元素下標
		int minPos = begin;//最小元素下標
		for (int idx = begin + 1; idx <= end; ++idx)
		{
			//比最大的元素還大,更新最大下標
			if (arr[idx] > arr[maxPos])
				maxPos = idx;
			//比最小的元素還小,更新最大下標
			if (arr[idx] < arr[minPos])
				minPos = idx;
		}
		//交換begin與最小元素的值
		std::swap(arr[begin], arr[minPos]);
		//如果最大元素下標與begin相同,上面begin與minPos已經交換,因此maxPos也應該指向minPos
		if (maxPos == begin)
			maxPos = minPos;
		//交換end與最大元素的值
		std::swap(arr[end], arr[maxPos]);
		//縮小區間
		++begin;
		--end;
	}
}
☞ 選擇排序(雙邊縮小空間2.0)

基本思想:1.0版本中,我們需要考慮最大下標與begin的重合問題,為了避免這樣的問題出現,我們將不再使用maxPos和minPos,而改用直接交換資料,然後縮小空間,直到左右空間重合。

void SelectSort(int* arr, int size)
{
	if (arr == NULL || size <= 0)
		return;
	int left = 0;
	int right = size - 1;
	while (left < right)
	{
		for (int idx = left; idx <= right; ++idx)
		{
			//比left小的交換
			if (arr[idx] < arr[left])
				std::swap(arr[idx], arr[left]);
			//比right大的交換
			if (arr[idx] > arr[right])
				std::swap(arr[idx], arr[right]);
		}
		//縮小左右區間
		++left;
		--right;
	}
}
☞ 堆排序

在這裡插入圖片描述
基本思想:堆排序本質上是一種樹形選擇排序。它也是對直接選擇排序的一種優化,堆結構在物理儲存上也是一種陣列,但是它在邏輯上是一棵完全二叉樹,在進行堆排序(升序)時,我們可以先建一個大堆,最大的元素在堆頂上,我們可以以O(1)的時間找到最大的元素,然後和最後一個元素交換。此時,這個堆的左右子樹仍然是一個堆,我們只要把[n-1]個數向下調整一次重新建個大堆即可,直到堆中剩下一個元素,既排序完成。

排升序–>建大堆 && 排降序–>建小堆
我們從最後一個非葉子結點建堆,步驟如下:

⑴ 將堆頂元素與當前最大堆的最後一個節點交換
⑵ 最大堆節點-1,即調整剩下的n-1個節點
⑶ 從堆頂繼續向下調整,試之滿足最大堆,迴圈⑴ ⑵ ,直至剩下一個節點。

時間複雜度: 0(NlogN)
穩定性:不穩定

適用場景:topK問題等

void AdjustDown(int *arr, int root, int size)//建大堆
{
	int parent = root;
	int child = parent * 2 + 1;
	while (child < size)
	{
		//保證child指向較大節點
		if (child + 1 < size && arr[child + 1] > arr[child])
			child += 1;
		if (arr[child] > arr[parent])
		{
			std::swap(arr[child], arr[parent]);
			parent = child;//下濾
			child = parent * 2 + 1;
		}
		else
			break;
	}
}
//堆排序遞迴
void HeapSort(int *arr, int size)
{
	assert(arr && size > 1);
	//從最後一個非葉子節點建堆
	for (int idx = (size - 2) / 2; idx >= 0; --idx)
	{
		AdjustDown(arr, idx, size);//下濾調整
	}
	int end = size - 1;
	while (end > 0)
	{
		//堆頂與最後一個節點交換,升序
		std::swap(arr[0], arr[end]);
		AdjustDown(arr, 0, end);//下濾調整
		--end;
	}
}

四、交換排序

☞ 氣泡排序

在這裡插入圖片描述
基本思想:一次確定一個最大值或者最小值,兩兩比較,將最大值或者最小交換到最右邊或者最左邊,N個元素需要N-1趟排序。

    程式碼實現:

//氣泡排序
void BubbleSort(int* num, int len)
{
	int flag = 0;
	 if (num == NULL || len <= 0)
	  return;
	 //確定迴圈躺數
	 for (int i = 0; i < len - 1; i++)
	 {
		  //確定比較次數
		  for (int j = 0; j < len - 1 - i; j++)
		  {
		   if (num[j]>num[j + 1])
		    {
		        Swap(&num[j], &num[j + 1]);
		    }	  	   
		    flag=1;
	  	  }
	  	  if(0 == flag)
	  	  	break;
	 }
}
 
☞ 快速排序

在這裡插入圖片描述
基本思想:在待排序序列中任意取一個元素作為基準元素,按照該基準元素將待排序序列分為兩個子序列,左邊子序列的值都小於基準值,右邊子序列的值都大於基準值。然後把左右子序列當做一個子問題,以同樣的方法處理左右子序列,直到所有的元素都排列在相對應的位置上為止。快排是一個遞迴問題,它是按照二叉遞迴樹的前序路線去劃分的。

關於快速排序,我詳細將快排的細節總結於我的另一篇部落格快排總結
參考部落格快排總結

五、歸併排序

☞ 歸併排序

在這裡插入圖片描述
基本思想:歸併排序是一個外排序,它可以對磁碟的檔案進行排序。它將待排序的元素序列分成兩個長度相等的子序列,對每一個子序列排序,然後在將他們合併為一個序列。合併兩個子序列的過程稱為二路歸併。歸併排序主要分為兩步分組和歸併。
在這裡插入圖片描述

void MergeSort(int* num, int len)
{
	 if (num == NULL || len <= 0)
	  return;
	 //開闢臨時空間,用來存放每次合併後的子序列
	 int* tmp = (int*)malloc(sizeof(int)*len);
	 _MergeSort(num, 0, len - 1, tmp);
	 //釋放空間
	 free(tmp);
 	 tmp = NULL;
}
//歸併排序分開過程(遞迴樹按照前序路線展開)
void _MergeSort(int* num, int begin, int end,int* tmp)
{
	 assert(num&&tmp);
	 int mid = begin + (end - begin) / 2;
	 //只有一個元素,說明這個序列已經有序
	 if (begin == end)
	  return;
	 //子問題劃分左子序列
	 _MergeSort(num, begin, mid, tmp);
	 //子問題劃分右子序列
	 _MergeSort(num, mid + 1, end, tmp);
	 //合併兩個有序陣列
	 Merge(num, begin, mid, mid + 1, end, tmp);
}
//歸併排序合併過程
void Merge(int* num, int start1, int end1, int start2, int end2, int* tmp)
{
	 assert(num&&tmp);
	 int begin = start1;
	 int index = start1;//從start1的地方合併
	 //和兩條有序單鏈表的合併的過程類似
	 while ((start1 <= end1) && (start2 <= end2))
	 {
		  if (num[start1] < num[start2])
		  {
		  	 tmp[index++] = num[start1++];
		  }
		  else
		  {
		 	  tmp[index++] = num[start2++];
		  }
	 }
	 //把剩餘的合併到tmp上
	 while (start1 <= end1)
	 	 tmp[index++] = num[start1++];
	 while (start2 <= end2)
	  	tmp[index++] = num[start2++];
	 //tmp是個臨時空間,最後到把合併的內容拷貝到num上
 	memcpy(num + begin, tmp + begin, sizeof(int)*(end2 - begin + 1));
}
 

六、計數排序

☞ 計數排序

在這裡插入圖片描述

計數排序又稱為鴿巢原理,它是對雜湊直接定址法的變形應用。計數排序是一種非比較的排序演算法,其核心在於將輸入的資料值轉化為鍵儲存在額外開闢的陣列空間中。這說明計數排序只適合用於比較資料較為集中的資料,如果資料太過分散是非常浪費空間的。
實現步驟

  • 統計所有相同資料出現的次數
  • 根據統計的次數將序列放回到原來的陣列返回

效率分析

計數排序是一個穩定的排序演算法。當輸入的元素是 n 個 0到 k 之間的整數時,時間複雜度是O(n+k)。
空間複雜度也是(n+k),其排序速度快於任何比較排序演算法。當k不是很大並且序列比較集中時,計數排序是一個很有效的排序演算法

void CountSort(int* num,int len)                                                                                                                      
{                                                                                                                                                     
    if(num==NULL||len<=0)                                                                                                                             
        return;                                                                                                                                       
                                                                                                                                                      
    int min=num[0],max=num[0];                                                                                                                        
    int i=0;                                                                                                                                          
    for(;i<len;i++)                                                                                                                                   
    {                                                                                                                                                 
        if(num[i]<min)                                                                                                                                
            min=num[i];                                                                                                                               
        if(num[i]>max)                                                                                                                                
            max=num[i];                                                                                                                               
    }                                                                                                                                                 
                                                                                                                                                      
    int range=max-min+1;                                                                                                                              
    int* tmp=(int*)malloc(sizeof(int)*range);                                                                                                         
    if(tmp==NULL)                                                                                                                                     
    {                                                                                                                                                 
        perror("use malloc");                                                                                                                         
        exit(1);                                                                                                                                      
    }                                                                                                                                                 
    memset(tmp,0,sizeof(int)*range);                                                                                                                  
    for(i=0;i<len;i++)                                                                                                                                
    {                                                                                                                                                 
         tmp[num[i]-min]++;                                                                                                                           
    }                                                                                                                                                 
    int index=0;  
    for(i=0;i<range;i++)