總結:八大排序算法(圖解+代碼)

一、直接插入排序

基本思想:

     把n個待排序的元素看成爲一個有序表和一個無序表,開始時有序表中只包含1個元素,無序表中包含有n-1個元素,排序過程中每次從無序表中取出第一個元素,將它插入到有序表中的適當位置,使之成爲新的有序表,重複n-1次可完成排序過程.

圖解:

這裏寫圖片描述

代碼實現:
void InsertSort(int *a, int size)
{
	assert(a);
	for (int i = 0; i < size - 1; i++)
	{
		int end = i; //有序表最後一個下標
		int key = a[end + 1]; //待插入元素
		while (end >= 0 && key < a[end]) //查找合適位置
		{
			a[end + 1] = a[end];
			end--;
		}
		a[end + 1] = key; //插入
	}
}
分析:

時間複雜度:

  • 最好情況:
    如果待排序的元素本身有序,那麼在進行插入排序時,每一個元素直接在前面有序表末尾處進行插入,整個過程下來,時間複雜度爲O(N)
  • 最壞情況:
    如果待排序的元素無序,那麼在進行插入排序時,每一個元素都需要在前面的有序表中找到其合適的插入位置,整個過程下來,時間複雜度爲O(N^2)
  • 平均情況:O(N^2)

空間複雜度:O(1)
穩定性:穩定
說明:設在數列中存在a[i]=a[j],若在排序之前,a[i]在a[j]前面,在排序之後,a[i]仍然在a[j]前面,則這個排序算法是穩定的。

二、希爾排序

基本思想:

(1)預排序:先將整個待排元素序列分割成若干個子序列(由相隔某個“增量”的元素組成),分別進行直接插入排序,然後依次縮減增量再進行排序,使整個序列接近有序
(2)當整個序列中的元素基本有序時,再對全體元素進行一次直接插入排序
     因爲直接插入排序在元素基本有序的情況下效率是很高的,因此希爾排序在時間效率上相對於直接插入排序有較大提高。

圖解:

這裏寫圖片描述

代碼實現:
void ShellSort(int *a, int size)
{
	assert(a);
	int gap = size;
	while (gap > 1)
	{
		gap = gap / 3 + 1; //增量(步長)
		for (int i = 0; i < size - gap; i++)
		{
			int end = i; //有序表最後一個下標
			int tmp = a[end + gap]; //待插入元素
			while (end >= 0 && tmp < a[end]) //查找合適位置
			{
				a[end + gap] = a[end];
				end -= gap;
			}
			a[end + gap] = tmp; //插入
		}
	}
}
分析:

時間複雜度:

  • 最好情況:O(N)
  • 最壞情況:O(N^2)
  • 平均情況:O(N^1.3)

空間複雜度:O(1)
穩定性:不穩定
     例如:待排序列3 2 2* 4,當gap爲2時進行希爾排序,經過排序後變爲2* 2 3 4,此時2和2*之間的相對位置發生變化。

三、直接選擇排序

基本思想:

     在元素序列a[i] ~ a[n-1]中選擇關鍵碼最大(最小)的數據元素,將它與這組元素中的最後一個(第一個)元素進行交換,接着在剩餘的元素序列a[i] ~ a[n-2](a[i+1] ~ a[n-1])中重複上述步驟,直到剩餘1個元素時完成排序。

圖解:

這裏寫圖片描述

代碼實現:
void SelectSort(int *a, int size)
{
	assert(a);
	int left = 0;
	int right = size - 1;
	while (left < right)
	{
		int min = left;
		int max = left;
		for (int i = left; i <= right; i++)
		{
			if (a[i] < a[min]) //選出最小
			{
				min = i;
			}
			if (a[i] > a[max]) //選出最大
			{
				max = i;
			}
		}
		Swap(&a[left], &a[min]);
		if (max == left) //若max和left相等,則經過上一步交換,導致原max處爲最小值,而min處爲最大值
		{
			max = min; //更新max,讓其位置爲最大值
		}
		Swap(&a[right], &a[max]);
 		left++;
		right--;
	}
}
分析:

時間複雜度:O(N^2)
空間複雜度:O(1)
穩定性:不穩定
     例如上述圖解,排序前 25 在 25* 之前,而在排序後 25 在 25* 後。

四、堆排序

基本思想:

     升序建大堆,降序建小堆
     以升序爲例,先將整個序列的元素建造成一個大堆,接着把堆頂元素和當前堆的最後一個元素進行交換,然後堆元素個數減1,接着從根節點通過向下調整使得當前堆恢復到大堆,重複上述過程,直到當前堆的元素個數爲1時完成排序。

圖解:

這裏寫圖片描述
這裏寫圖片描述

代碼實現:
//自頂向下調整
void AdjustDown(int *a, int size, int root)
{
	int parent = root;
	int child = parent * 2 + 1;
	while (child < size)
	{
		int flag = 0;
		if (child + 1 < size)
		{
			if (a[child + 1] > a[child])
			{
				child++;
			}
		}
		if (a[child]>a[parent])
		{
			flag = 1;
			Swap(&a[child], &a[parent]);
		}
		if (flag == 0) //優化
		{
			break;
		}
		parent = child;
		child = parent * 2 + 1;
	}
}

void HeapSort(int *a, int size)
{
	assert(a);
	int i = (size - 2) / 2;
	for (; i >= 0; i--) //先建大堆
	{
		AdjustDown(a, size, i);
	}

	for (i = size - 1; i > 0; i--) //排序
	{
		Swap(&a[0], &a[i]);
		AdjustDown(a, i, 0);
	}	
}
分析:

時間複雜度:O(N*lgN)
空間複雜度:O(1)
穩定性:不穩定

五、冒泡排序

基本思想:

     從元素序列第一個位置開始,進行兩兩比較,根據大小交換位置,直到最後將最大(最小)的數據元素交換到了當前序列的最後一個位置,成爲有序序列的一部分,然後元素個數減1,重複上述過程,直到所有數據元素都排好序。

圖解:

這裏寫圖片描述

代碼實現:
void BubbleSort(int *a, int size)
{
	assert(a);
	for (int i = 0; i < size - 1; i++)
	{
		int flag = 0;
		for (int j = 0; j < size - i - 1; j++) //一趟排序
		{
			if (a[j]>a[j + 1])
			{
				flag = 1;
				Swap(&a[j], &a[j + 1]);
			}
		}
		if (flag == 0) //如果一趟排序後發現沒有一次交換,則說明已經有序
		{
			break;
		}
	}
}
分析:

時間複雜度:

  • 最好情況:O(N)
  • 最壞情況:O(N^2)
  • 平均情況:O(N^2)

空間複雜度:O(1)
穩定性:穩定

六、快速排序

基本思想:

     任取待排列元素序列中的一個元素作爲基準值,通過一趟排序將要排序的序列分割成獨立的兩子序列,其中左子序列的所有元素都比基準值小,右子序列的所有元素都比基準值大,然後左右子序列重複此過程,直到所有元素都排列在相應的位置上爲止。

圖解:

這裏寫圖片描述

將區間按照基準值劃分成左右兩部分的方法有:

(1)左右指針法:
     定義兩個指針begin和end,將基準值放在最右邊,begin從頭開始找比基準值大的值(begin++),end從尾開始找比基準值小的值(end–),若都找到且begin小於end,則兩者值交換,重複上述過程,直到begin>=end時,將begin所對應的值和最右邊的基準值交換,此時整個序列被基準值劃分成左右兩個子序列。
這裏寫圖片描述

int PartSort1(int *a, int left, int right)
{
	int index = GetMid(a, left, right); //三數取中,選取基準值
	Swap(&a[index], &a[right]); //將基準值和最右邊的值交換
	int key = a[right];
	int begin = left;
	int end = right;
	while (begin < end)
	{
		//begin選大
		while (begin < end && a[begin] <= key) //"="可省略
		{
			begin++;
		}
		//end選小
		while (begin < end && a[end] >= key) //"="不可省略
		{
			end--;
		}
		if (begin < end)
		{
			Swap(&a[begin], &a[end]);
		}
	}
	Swap(&a[begin], &a[right]);
	return begin;
}

(2)挖坑法:
     定義兩個指針begin和end,將基準值放在最右邊並保存該值,此時該位置可視爲一個坑。begin從頭開始找比基準值大的值,找到後將begin所對應的值填入到剛纔的坑中,此時begin這個位置成爲新的坑;begin不動,接着end從尾開始找比基準值小的值,找到後將end所對應的值填入到剛纔的新坑中,此時end這個位置成爲新的坑;end不動,begin從上次的位置接着往後找,重複上述過程,直到begin>=end時,將保存的基準值填入到begin所對應的坑中,此時整個序列被基準值劃分成左右兩個子序列。
這裏寫圖片描述

int PartSort2(int *a, int left, int right)
{
	int index = GetMid(a, left, right); //三數取中,選取基準值
	Swap(&a[index], &a[right]); //將基準值和最右邊的值交換
	int key = a[right]; //保存基準值
	int begin = left;
	int end = right;
	while (begin < end)
	{
		//begin選大
		while (begin < end && a[begin] <= key)
		{
			begin++;
		}
		a[end] = a[begin];
		//end選小
		while (begin < end && a[end] >= key)
		{
			end--;
		}
		a[begin] = a[end];
	}
	a[begin] = key;
	return begin;
}

(3)前後指針法:
     定義兩個指針prev和cur,將基準值放在最右邊,prev初始位置在left-1處,cur初始位置在left處。cur從頭開始找比基準值小的值,找到後若此時++prev的位置和cur的位置不在同一處(說明++prev對應的值一定比基準值大),則交換這兩處的值,cur接着剛纔的位置往後找,重複上述過程,直到cur>=right時,將最右邊的基準值和++prev所對應的值進行交換,此時整個序列被基準值劃分成左右兩個子序列。
這裏寫圖片描述

int PartSort3(int *a, int left, int right) 
{
	int index = GetMid(a, left, right); //三數取中,選取基準值
	Swap(&a[index], &a[right]); //將基準值和最右邊的值交換
	int prev = left - 1;
	int cur = left;
	while (cur < right)
	{
		if (a[cur] < a[right] && ++prev != cur)
		{
			Swap(&a[cur], &a[prev]);
		}
		cur++;
	}
	Swap(&a[++prev], &a[right]);
	return prev;
}
三數取中法:

     在選擇基準值時,爲了提高排序效率,我們常常利用三數取中法來選擇基準值,所謂的三數指的是:序列最左端的值、中間位置的值和最右端的值,計算它們的中位數來作爲基準值。

int GetMid(int *a, int left, int right)
{
	int mid = (left + right) >> 1;
	if (a[left] < a[right])
	{
		if (a[mid] < a[left])
		{
			return left;
		}
		else if (a[mid] > a[right])
		{
			return right;
		}
		else
		{
			return mid;
		}
	}
	else
	{
		if (a[mid] > a[left])
		{
			return left;
		}
		else if (a[mid] < a[right])
		{
			return right;
		}
		else
		{
			return mid;
		}
	}
}
代碼實現:

(1)遞歸法:

void QuickSortR(int *a, int left, int right)
{
	assert(a);
	if (left >= right)
	{
		return;
	}

	if (right - left < 10) //優化:在區間較小時,插入排序的效率高
	{
		InsertSort(a, right - left + 1);
	}
	else
	{
		int div = PartSort1(a, left, right);
		QuickSortR(a, left, div - 1);
		QuickSortR(a, div + 1, right);
	}
}

(2)非遞歸法:
     借用棧的結構來模仿遞歸(相關棧的函數定義請查看順序棧

void QuickSort(int *a, int left, int right)
{
	assert(a);
	Stack s;
	StackInit(&s);
	StackPush(&s, left);
	StackPush(&s, right);
	while (!StackEmpty(&s))
	{
		int end = StackTop(&s);
		StackPop(&s);
		int begin = StackTop(&s);
		StackPop(&s);
		int div = PartSort1(a, begin, end);
		if (begin < div - 1)
		{
			StackPush(&s, begin);
			StackPush(&s, div - 1);
		}
		if (div + 1 < end)
		{
			StackPush(&s, div + 1);
			StackPush(&s, end);
		}
	}
}
分析:

時間複雜度:

  • 最好情況:O(N*lgN)
  • 最壞情況:O(N^2)
  • 平均情況:O(N*lgN)

空間複雜度:O(lgN)——遞歸深度
穩定性:不穩定

七、歸併排序

基本思想:

     將待排序的元素序列分成兩個長度相等的子序列,對每一個子序列排序,然後再將它們合併成一個有序序列。

圖解:

這裏寫圖片描述

合併圖解:

這裏寫圖片描述

代碼實現:
//歸併局部遞歸
void _MergeSort(int *a, int left, int right, int *tmp)
{
	if (left >= right)
	{
		return;
	}

	if (right - left < 10) //優化:在區間較小時,插入排序的效率高
	{
		InsertSort(a, right - left + 1);
		return;
	}

	int mid = left + ((right - left) >> 1);
	_MergeSort(a, left, mid, tmp);
	_MergeSort(a, mid + 1, right, tmp);

	int p = left;
	int q = mid + 1;
	int index = 0;
	while (p <= mid&&q <= right)
	{
		if (a[p] <= a[q]) //加上"="可保持穩定性
		{
			tmp[index++] = a[p++];
		}
		else
		{
			tmp[index++] = a[q++];
		}
	}
	while (p <= mid)
	{
		tmp[index++] = a[p++];
	}
	while (q <= right)
	{
		tmp[index++] = a[q++];
	}

	int j = 0;
	for (int i = left; i <= right; i++)
	{
		a[i] = tmp[j++];
	}
}

void MergeSort(int *a, int left, int right)
{
	assert(a);
	int *tmp = (int*)malloc((right - left + 1)*sizeof(int));
	memset(tmp, 0, (right - left + 1)*sizeof(int));
	//或者:int *tmp = (int *)calloc(right - left + 1,sizeof(int));
	_MergeSort(a, left, right, tmp);
	free(tmp);
}
分析:

時間複雜度:O(N*lgN)
空間複雜度:O(N)——臨時數組
穩定性:穩定

八、計數排序

基本思想:

     統計待排序序列中每個元素出現的次數,再根據統計的結果重新對元素進行回收。

圖解:

這裏寫圖片描述

代碼實現:
void CountSort(int *a, int size)
{
	assert(a);
	//(1)找出最值,確定範圍,以便開闢空間
	int max = a[0];
	int min = a[0];
	int index = 0;
	for (index = 1; index < size; index++)
	{
		if (a[index]>max)
		{
			max = a[index];
		}
		if (a[index] < min)
		{
			min = a[index];
		}
	}
	int range= max - min + 1;
	//(2)統計出現次數
	int *tmp = (int*)calloc(range, sizeof(int));
	for (index = 0; index < size; index++)
	{
		tmp[a[index] - min]++;
	}
	//(3)回收
	int i = 0;
	for (index = 0; index < range; index++)
	{
		while (tmp[index])
		{
			a[i++] = index + min;
			tmp[index]--;
		}
	}
	free(tmp);
	tmp = NULL;
}
分析:

時間複雜度:O(N+range)
空間複雜度:O(range)
穩定性:穩定

九、總結:

這裏寫圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章