數據結構與算法分析之排序算法總結

十大常用排序算法總結

1.交換排序

交換排序是通過元素間的比較和交換來完成,分爲冒泡排序和快速排序兩種。

1.1冒泡排序

冒泡排序是最簡單的一種排序方法。其排序過程是類似冒泡一樣,通過相鄰元素之間的比較和交換將小的元素逐漸交換到最前面或者將大的元素逐漸交換到最後面。

時間複雜度
最壞情況:O(n^2)
平均情況:O(n^2)
最好情況:O(n)
空間複雜度:O(n)
輔助存儲:O(1)

穩定性:穩定排序

代碼實現
#include <stdio.h>
void bubble_sort(int A[], int N)
{
	int i, j, tmp;

	for (i = 0; i < N; i++) {
		for (j = N - 1; j > i; j--) {
			if (A[j] < A[j - 1]) {
				tmp = A[j - 1];
				A[j - 1] = A[j];
				A[j] = tmp;
			}
		}
	}
}

int main()
{
	int A[] = {9, 8, 7, 6, 5, 4, 3, 2, 1};
	int size = sizeof(A) / sizeof(int);

	bubble_sort(A, size);

	int i;
	for (i = 0; i < size; i++)
		printf("A[%d] = %d\n", i, A[i]);
}

1.2快速排序

快速排序是典型的分治遞歸算法,它的最壞情形仍然是O(n^2), 通過精煉和高度優化的內部循環來避免這種情況,使其平均情形能夠達到O(nlogn)。

快速排序基本算法由以下4步組成:
1.如果數組S中元素個數是0或1,則直接返回
2.在數組S中選取其中一個元素,稱之爲樞紐元(pivot)
3.將數組S中所有小於pivot的元素放入S1中,將所有大於pivot的元素放入S2中
4.繼續對S1進行快排序,中間元素時pivot,然後繼續對S2繼續快速排序

相比歸併排序,快速排序更快的原因在於,第3步中如果pivot選取恰當,會高效的彌補大小不等的遞歸調用帶來的缺憾。

選取樞紐元pivot的策略時,一種不好的做法是直接將第一個元素做爲樞紐元,一般安全的做法是隨機選取樞紐元。實際中一般使用下面兩種方法選定樞紐元:
1.中位數法,即pivot = A[N/2]
2.三數中值分割法

三數中值分割法:精確的中值很難算出,也會明顯的減慢快速排序的速度,一般做法是使用左右兩端和中心位置上的三個元素的中值
具體算法執行過程如下:
1.選定樞紐元,將樞紐元與最後一個元素交換
2.遊標i從最左端開始移動,j從最右端倒數第二個元素移動
3.當i遇到大於樞紐元的元素時停止移動,同理,當j遇到小於樞紐元的元素時停止移動
4.當i和j停止時交換他們的元素,後繼續執行第2和第3步,直到i和j的位置交錯
5.當i和j交錯時,將pivot與i交換;繼續對S1(S[0]...pivot)以及S2(pivot...S[end])進行遞歸快速排序


時間複雜度:
最壞情況:O(n^2)
平均情況:O(nlogn)
最好情況:O(nlogn)

空間複雜度:O(n)
輔助存儲O(1),網上很多的技術博客都說快速排序的輔助存儲是O(nlogn)或O(logn),想不明白爲什麼,無論是中位數分割或者三數中值分割實現的快速排序都不需要額外開闢空間。所以個人還是堅持自己的想法認爲快速排序算法的輔助存儲複雜度是O(1)。

穩定性:不穩定排序

代碼實現
#include <stdio.h>

typedef int ElementType;

#define Cutoff (3)

void InsertionSort(ElementType A[], int N)
{
	int i, j;
	ElementType Tmp;

	/* 外層循環從1 到 N */
	for (i = 1; i < N; i++) {
		/* 爲第i個元素找到合適的位置插入 */
		Tmp = A[i];
		/* 內層循環從i 到 0查找,當發現有大於Tmp的地方j - 1則
		 * A[j] = A[j - 1],直到退出循環。A[j]就是Tmp要插入的位置
		 * */
		for (j = i; j > 0 && A[j - 1] > Tmp; j--)
			A[j] = A[j - 1];
		A[j] = Tmp;
	}
}

void Swap(ElementType *Lhs, ElementType *Rhs)
{
	ElementType Tmp = *Lhs;
	*Lhs = *Rhs;
	*Rhs = Tmp;
}

ElementType Median3(ElementType A[], int Left, int Right)
{
	int Center = (Left + Right) / 2;

	if (A[Left] > A[Center])
		Swap(&A[Left], &A[Center]);
	if (A[Left] > A[Right])
		Swap(&A[Left], &A[Right]);
	if (A[Center] > A[Right])
		Swap(&A[Center], &A[Right]);

	/**/
	Swap(&A[Center], &A[Right - 1]);
	return A[Right - 1];
}

/* 三數中值分割法 */
void Qsort(ElementType A[], int Left, int Right)
{
	int i, j;
	ElementType Pivot;

	if (Left + Cutoff <= Right) {
		/* 分割元被隱藏在A[Right - 1]的位置,這樣做是爲了少做一次比較,
		 * 因爲在樞紐元被選定後A[Right] > Pivot */
		Pivot = Median3(A, Left, Right);
		//經過Median3執行後,A[Left] < Pivot,所以從++i開始
		i = Left;
		//經過Median3執行後,A[Right - 1] = Pivot,所以從--j開始
		j = Right - 1;

		for (;;) {
			while(A[++i] < Pivot){};
			while(A[--j] > Pivot){};

			if (i < j)
				Swap(&A[i], &A[j]);
			else
				break;
		}

		/* 由於Pivot隱藏在Right-1的位置,此時i>= j
		 * 即A[i] > Pivot,所以選擇i的位置與Pivot交換*/
		Swap(&A[i], &A[Right - 1]);
		Qsort(A, Left, i - 1);
		Qsort(A, i + 1, Right);
	} else//三數中值法要特殊處理Right-Left < 3的情況
		InsertionSort(A + Left, Right - Left + 1);
}

/* 中位數法 */
void Qsort1(ElementType A[], int Left, int Right)
{
	int i, j, PivotIdx;
	ElementType Pivot;
	
	/* Left < Right成立才適用中位數法 */
	if (Left < Right) {
		PivotIdx = (Left + Right) / 2;
		Pivot = A[PivotIdx];
		/* 隱藏分割元到最左邊的位置 */
		Swap(&A[Left], &A[PivotIdx]);
		i = Left;
		j = Right + 1;
		
		for (;;) {
			/* 選取++i/--j而不是i++/j--是爲了下面的Swap 方便 */
			while (A[++i] < Pivot){}
			while (A[--j] > Pivot){}

			if (i < j)
				Swap(&A[i], &A[j]);
			else
				break;
		}

		/* 此時滿足條件i >= j, 所以選用A[j]與樞紐元交換 */
		Swap(&A[Left], &A[j]);
		/* 繼續對分割的兩個部分進行遞歸快排序 */
		Qsort1(A, Left, j - 1);
		Qsort1(A, j + 1, Right);
	}
}

void QuickSort(ElementType A[], int N)
{
	Qsort(A, 0, N - 1);
	//Qsort1(A, 0, N - 1);
}

int main(){
	ElementType A[9] = {9, 8, 7, 6, 5, 4, 3, 2, 1};
	QuickSort(A, 9);

	int i;
	for (i = 0; i < 9; i++){
		printf("A[%d] = %d\n", i, A[i]);
	}
}


2.選擇排序

選擇排序也分爲兩類,一種是直接選擇排序,另一種是堆排序。與交換排序不同的是,選擇排序是通過從整體中選擇一個最大或最小的放入一個合適的位置。當所有元素被選擇後,排序也就完成。選擇排序可以看作是對交換排序的一種優化,只有在最小數或最大數確定的前提下才進行交換,這樣大大減少了交換的次數。

2.1直接選擇排序

其排序過程是,第一次循環通過比較確定最小的那個數,並和第0個元素交換。第二次循環確定下一個最小的數並和第1個元素交換.....

時間複雜度:
最壞情況:O(n^2)
平均情況:O(n^2)
最好情況:O(n^2)

空間複雜度:O(n)
輔助存儲O(1)

穩定性:不穩定排序

代碼實現
#include <stdio.h>

void select_sort(int A[], int N)
{
	int i, j, tmp;
	for (i = 0; i < N; i++) {
		for(j = N - 1; j > i; j--) {
			if (A[j] < A[i]) {
				tmp = A[i];
				A[i] = A[j];
				A[j] = tmp;
			}
		}
	}
}

int main()
{
	int A[] = {9, 8, 7, 6, 5, 4, 3, 2, 1};
	int size = sizeof(A)/sizeof(int);
	select_sort(A, size);
	int i;
	for (i = 0; i < size; i++)
		printf("A[%d] = %d\n", i, A[i]);
}

2.2堆排序

堆排序是利用堆的特性,即最大值堆的根節點是最大值,最小值堆的根節點是最小值來實現的選擇排序。

堆排序的實現過程是
1.對無序數組S構建有序堆
2.刪除根,並將根放入數組的最後一個位置,並對剩下的數組重新構造堆
3.反覆執行2,直到所有的元素都被選擇完畢

時間複雜度
最壞情況:O(nlogn)
平均情況:O(nlogn)
最好情況:O(nlogn)

空間複雜度:O(n)
輔助存儲:O(1)

穩定性:不穩定排序

代碼實現如下
typedef int ElementType;
#define LeftChild( i ) (2 * (i) + 1)

void Swap(ElementType *Lhs, ElementType *Rhs)
{
	ElementType Tmp = *Lhs;
	*Lhs = *Rhs;
	*Rhs = Tmp;
}

/*
 * 爲A[i]找到合適的位置,將i下濾 
 * */
void PercDown(ElementType A[], int i, int N)
{
	int Child;
	ElementType Tmp;

	for (Tmp = A[i]; LeftChild(i) < N; i = Child) {
	
		Child = LeftChild(i);
		/* 當i的左兒子和右兒子同時存在時,選定值較大的那個 */
		if (Child != N - 1 && A[Child + 1] > A[Child])
			Child++;
		if (Tmp < A[Child])
			A[i] = A[Child];/* 大值堆 */
		else /* 當Tmp的值已經大於其左右兒子時退出,此時不再需要繼續下濾 */
			break;
	}

	/* 已爲Tmp找到了合適的位置 */
	A[i] = Tmp;
}

void HeapSort(ElementType A[], int N)
{
	int i;

	/* 先對原始數組重構堆(最大值堆),注意下標是從0開始 */
	for (i = N / 2; i >= 0; i--)
		PercDown(A, i, N);

	for (i = N - 1; i > 0; i--)
	{
		/* 刪除最大值A[0],並和A[i]交換 */
		Swap(&A[0], &A[i]);
		/* 對數組A從0到N-2重新構建堆 */
		PercDown(A, 0, i);
	}

	/* 循環執行上述操作後,數組A中的數據從0到N-1按照升序排列 */
}

int main()
{
}

3.插入排序

和交換及選擇排序不同的是,插入排序是通過比較找到一個合適的位置插入元素來完成排序。插入排序也分爲兩類:一種是直接插入排序,另一種是希爾排序。

3.1直接插入排序

算法執行過程:
1.假定第一個元素位置正確,將第二個元素在0,1的位置中選取合適的位置插入
2.取第n個元素在0,1,...n中選取一個合適的位置插入,直到所有元素都插入完成。

時間複雜度:
最壞情況:O(n^2)
平均情況:O(n^2)
最好情況:O(n)

空間複雜度:O(n)
輔助存儲O(1)

穩定性:穩定排序

代碼實現如下

#include <stdio.h>

typedef int ElementType;

void InsertionSort(ElementType A[], int N)
{
	int i, j;
	ElementType Tmp;

	/* 外層循環從1 到 N */
	for (i = 1; i < N; i++) {
		/* 爲第i個元素找到合適的位置插入 */
		Tmp = A[i];
		/* 內層循環從i 到 0查找,當發現有大於Tmp的地方j - 1則
		 * A[j] = A[j - 1],直到退出循環。A[j]就是Tmp要插入的位置
		 * */
		for (j = i; j > 0 && A[j - 1] > Tmp; j--)
			A[j] = A[j - 1];
		A[j] = Tmp;
	}
}

int main()
{
	ElementType A[9] = {9, 8, 7, 6, 5, 4, 3, 2, 1};
	InsertionSort(A, 9);
	int i;
	for (i = 0; i < 9; i++)
		printf("A[%d]=%d\n", i, A[i]);
}


3.2希爾排序

希爾排序是對插入排序的一種高效優化,也叫縮小增量排序。它利用序列是基本有序的這一特點,通過比較相距一定間隔的元素來排序,最後將所有相鄰的元素做一次直接插入排序。希爾排序的運行時間依賴於增量序列的選擇,流行但不是很好的增量序列是Hk = N/2和Hk = H(k+1)/2。一般實現時仍然採取Hk = N / 2;

時間複雜度
最壞情況:O(n^2)
平均情況:O(n^1.3)
最好情況:O(n)

空間複雜度:O(n)
輔助存儲:O(1)

穩定性:不穩定

代碼實現如下
#include <stdio.h>

typedef int ElementType;

void ShellSort(ElementType A[], int N)
{
	int i, j, Increment;
	ElementType Tmp;

	/* 排序增量序列爲k = N/2 */
	for (Increment = N / 2; Increment > 0; Increment /= 2) {
	
		/* 對於Increment, 檢查從Increment,N之間每一個元素,
		 * 使其間隔Increment的所有元素都是有序的
		 * */
		for (i = Increment; i < N; i++) {
			Tmp = A[i];
			/* 對於每個Increment到N之間的元素,需要遍歷Increment到i之間間隔Increment的元素
			 * 爲A[i]找到一個合適的位置(此過程實際上是插入過程)
			 * */
			for (j = i; j >= Increment; j -= Increment) {
				if (Tmp < A[j - Increment])
					A[j] = A[j - Increment];
				else
					break;
			}
			A[j] = Tmp;
		}
	}
}

int main()
{
	ElementType A[9] = {9, 8, 7, 6, 5, 4, 3, 2, 1};
	ShellSort(A, 9);
	int i;
	for (i = 0; i < 9; i++)
		printf("A[%d]=%d\n", i, A[i]);
}

4.歸併排序

歸併排序採用了遞歸分治的思想,其基本思想是:合併兩個有序序列,所以基本過程是先遞歸劃分子序列,然後再合併結果。

該算法基本過程如下
1.將數組S一分爲2,遞歸進行合併
2.繼續將1中的子序列數組一分爲2,並進行有序合併
3.有序合併是將兩個子序列數組按照順序分別拷貝到臨時數組,再拷回的過程

時間複雜度:
最壞情況:O(nlogn)
平均情況:O(nlogn)
最好情況:O(nogn)

空間複雜度:O(n+n)
輔助存儲:O(n),網上大多數技術博客中此處是O(1)是不對的,歸併排序需要開闢臨時數組來保存有序合併的操作結果,所以輔助存儲是O(n)

穩定性:穩定排序

雖然歸併排序的運行時間是O(nlogn),但它很難用於主存排序。主要問題在於合併兩個有序序列需要線性附件內存,並且在整個算法中還要花費將數據合併到臨時數組再拷回這種附加操作。這樣會嚴重放慢排序的速度。歸併的例程是多數外部排序算法的基石。

代碼實現:
#include <stdlib.h>
#include <stdio.h>

typedef int ElementType;

void Merge(ElementType A[], ElementType TmpArray[],
		int Lpos, int Rpos, int RightEnd)
{
	int i, LeftEnd, NumElements, TmpPos;
	LeftEnd = Rpos - 1;/* 左序列的右邊界 */
	TmpPos = Lpos;
	NumElements = RightEnd - Lpos + 1;

	/* 在兩個序列上同時移動,將較小值拷貝到臨時數組中,
	 * 直到其中一個序列拷貝完成
	 * */
	while (Lpos <= LeftEnd && Rpos <= RightEnd)
		if (A[Lpos] <= A[Rpos])
			TmpArray[TmpPos++] = A[Lpos++];
		else
			TmpArray[TmpPos++] = A[Rpos++];

	/* 左序列有剩餘的情況 */
	while (Lpos <= LeftEnd)
		TmpArray[TmpPos++] = A[Lpos++];

	/* 右序列有剩餘的情況,兩種剩餘情況只會存在一種 */
	while (Rpos <= RightEnd)
		TmpArray[TmpPos++] = A[Rpos++];

	/* 經過上面的合併操作後,Lpos,Rpos,TmpPos都已移動過,
	 * RightEnd沒有改變過,所以從RighEnd位置往前拷貝
	 * NumElements個元素到原數組中*/
	for (i = 0; i < NumElements; i++, RightEnd--)
		A[RightEnd] = TmpArray[RightEnd];
}

/* 
 * 歸併排序要先將兩個子序列排序再合併
 * 遞歸的將整個數組一分爲二,分別排序併合並
 * */
void Msort(ElementType A[], ElementType TmpArray[],
		int Left, int Right)
{
	int Center;
	if (Left < Right) {
		Center = (Left + Right) / 2;
		Msort(A, TmpArray, Left, Center);
		Msort(A, TmpArray, Center + 1, Right);
		Merge(A, TmpArray, Left, Center + 1, Right);
	}
}

void MergeSort(ElementType A[], int N)
{
	ElementType *TmpArray;

	/* 開闢臨時數組 */
	TmpArray = malloc(N * sizeof(ElementType));
	if (TmpArray != NULL) {
		Msort(A, TmpArray, 0, N - 1);
		free(TmpArray);
	}
}

int main()
{
	ElementType A[7] = {8,7,6,5,4,3,2};
	MergeSort(A, 7);

	int i = 0;
	for (i = 0; i < 7; i++)
		printf("A[%d] = %d\n", i, A[i]);
}

5.線性排序

基於比較的排序運行時間有一個下限就是O(nlogn),但現實中仍然存在線性的排序,只不過需要付出額外的代價,那就是較多的輔助空間。都是通過空間換時間的做法實現線性排序。線性排序可以分爲3種:計數排序;桶排序;基數排序

5.1計數排序

計數排序的基本思想是,用待排序的數作爲計數數組的下標,然後統計每個數組中存在的數據來完成有序輸出。

時間複雜度:
最壞情況:O(n+k)
平均情況:O(n+k)
最好情況:O(n)

  其中n是數據元素個數,k是數組中數據元素的範圍,細想一下k的範圍實際上滿足k>=n,當k越大,所需空間越多,遍歷輸出的用時也就越多。理想情況是k=n,即數組中數據有序且連續分佈。計數排序實際上適用於那些比較均勻分佈的序列。

空間複雜度:O(n+k)
輔助空間O(k)

穩定性:穩定排序

代碼實現如下
#include <stdio.h>
#include <stdlib.h>

typedef int ElementType;

/* 獲取數組A中最大元,作爲計數數組的長度 */
ElementType get_max_val(ElementType A[], int N)
{
	int i, pos = 0;

	for (i = 1; i < N; i++)
		if (A[i] > A[pos])
			pos = i;

	return A[pos];
}

void CountSort(ElementType A[], int N)
{
	int i, j, CntArrLen;
	
	CntArrLen = get_max_val(A, N) + 1;

	ElementType *CntArr = malloc(CntArrLen * sizeof(ElementType));
	if (!CntArr) {
		printf("Out of space\n");
		return;
	}

	/* 重置計數數組 */
	for (i = 0; i < CntArrLen; i++)
		CntArr[i] = 0;

	/* 計數 */
	for (i = 0; i < N; i++)
		CntArr[A[i]]++;

	for (i = 0, j = 0; i < CntArrLen; i++) {
		while (CntArr[i]) {
			/* j++是爲了將重複數據放入到下一個位置上 */
			A[j++] = i;
			CntArr[i]--;
		}
	} 
}

int main()
{
	ElementType A[9] = {9, 1, 7, 5, 2, 2, 8, 3, 6};
	CountSort(A, 9);
	int i = 0;

	for (i = 0; i < 9; i++)
		printf("A[%d] = %d\n", i, A[i]);
}


5.2桶排序

桶排序雖然思想上採用的就是計數排序,但是二者不能混爲一談(網上很多都將計數排序當成桶排序)。桶排序比計數排序要複雜,是計數排序的一種優化改進。

桶排序的基本思想是:
假設長度爲N的待排序列A[1....n]均勻分佈在M個子區間上。首先構造M個桶,然後按照映射關係將較連續均勻分佈的子序列分配到對應的子桶中。然後對每個子桶上的數據進行比較排序(插入排序等)。

桶排序與計數排序的區別
1.計數排序使用最大數作爲桶數,需要的輔助空間較多
2.桶排序利用數據的均勻分佈在M個子區間上的事實,劃分M個桶,這樣需要的輔助空間較少,但是需要對桶上數據進行排序。這一排序在分配桶的過程中也是線性的(參考代碼)。

舉個例子:
                                                                                                                           
假如待排序列K={49,38,35,97,76,73,27,49},這些數據全部分佈在1-100之間,我們可以使用10個桶(而不是計數排序中的100個桶)。然後確定映射關係F(k)=k/10。這樣第一個關鍵字49將會定位到第4個桶中(49/10=4)。按照映射關係,依次將所有關鍵字全部放入桶中,並對非空的桶上所有元素進行比較排序,最後順序輸出每個桶上的數據就是有序序列。

時間複雜度:
最壞情況:O(N + N*logN - N*logM)
平均情況:O(N + N * logN - N*logM)
最好情況:O(N)

空間複雜度:O(N+M)
輔助空間是O(M)

穩定性:不穩定排序

桶排序的性能分析:
桶排序利用映射關係,減少了幾乎所有的比較工作。實際上桶排序中的f(k)值得計算,其作用相當於快排序中的劃分,希爾排序中的增量序列,歸併排序中的子序列。也就是將大量數據分割成基本有序的數據塊(桶),只對桶中少量數據做比較排序即可。

對N個關鍵字進行桶排序的時間複雜度分爲兩個部分:
1.循環計算每個關鍵字的桶映射函數,是O(N)
2.利用比較排序對每個桶內所有的數據進行排序,其時間複雜度是O(sum(Ni*logNi))。其中Ni爲第i個桶的數據量。

很顯然第2步決定了桶排序性能的好壞,爲儘量減少桶內數據的數量,可以從以下兩個點着手:
1.映射函數F(k)最好能夠將N個數據平均分配到M個桶中,假設實際情況就是這樣,這樣的話每個桶就有N/M個數據量
2.儘量增大桶的數量。極限情況是每個桶只有一個數據,這樣就完全避開了桶內數據的比較排序。
綜上,對於N個待排數據,M個桶,平均每個桶的[N/M]個數據的桶,平均的時間複雜度爲:
O(N) + O(M*(N/M)*log(N/M)) = O(N + N*(logN-logM)) = O(N + N*logN - N*logM)
當M=N時,即極限情況下每個桶只有一個數據時,桶排序的運行時間最小O(N)

代碼實現如下:
#include <stdio.h>
#include <stdlib.h>

typedef int ElementType;

/* 桶的鏈表節點 */
typedef struct node {
	ElementType key;
	struct node * next;
} KeyNode;

void bucket_sort(ElementType A[], int N, int bucket_size)
{
	int i, idx;
	/* bucket_size個桶數組分配 */
	KeyNode **bucket_table = (KeyNode **)malloc(bucket_size * sizeof(KeyNode *));
	if (!bucket_table) {
		printf("Out of space\n");
		return;
	}

	for (i = 0; i < bucket_size; i++) {
		/* 桶分配並重置,桶的第一個節點的key用來標記桶內分配的元素個數 */
		bucket_table[i] = (KeyNode *)malloc(sizeof(KeyNode));
		bucket_table[i]->key = 0;
		bucket_table[i]->next = NULL;
	}

	for (i = 0; i < N; i++) {
		/* 排序元素節點分配,並最終掛在對應的桶上 */
		KeyNode *p, *node = (KeyNode*)malloc(sizeof(KeyNode));
		node->key = A[i];
		node->next = NULL;
		idx = A[i] / 10;
		p = bucket_table[idx];

		if (p->key == 0) {
			/* 第一次往此桶上分配 */
			p->next = node;
			p->key++;
		} else {
			/* 桶上有多個元素時,在合適的位置插入(完成桶內排序) */
			while(p->next && p->next->key <= node->key)
				p = p->next;
			node->next = p->next;
			p->next = node;
			bucket_table[idx]->key++;
		}
	}

	/* 桶排序後輸出 */
	KeyNode *p;
	for (i = 0; i < bucket_size; i++) {
		p = bucket_table[i];
		if (p->key == 0)
			continue;
		while(p->next) {
			printf("%d\n", p->next->key);
			p = p->next;
		}
	}
}

int main()
{
	ElementType A[] = {49, 38, 65, 97, 76, 13, 27, 49};
	int size = sizeof(A)/sizeof(ElementType);
	/* 桶的大小爲10 */
	bucket_sort(A, size, 10);
}

5.3基數排序

基數排序也是線性排序的一種。和計數以及桶排序不同的是,計數和桶排序只對一個關鍵字進行排序,而基數排序是一種藉助多關鍵字排序思想對單邏輯關鍵字進行排序的方法。所謂多關鍵字排序就是有多個優先級不同的關鍵字。比如學生成績管理中,如果兩個人總分相同,則按照語文分高的排在前面,語文成績也相同的按照數據成績高的排序。對應於數字的排序,可以按照個位,十位,百位等不同優先級的位進行排序。基數排序是通過多次的分配以及收集來實現的。

基數排序按照優先位可分爲MSD(Most Significant Dight)和LSD(Least Significant Dight)。
MSD:先高位排序在低位排序
LSD:先低位排序在高位排序

時間複雜度
最壞情況:O(d(n+r))
平均情況:O(d(n+r))
最好情況:O(d(n+r))
其中d是長度,即比較的位數(涉及d次分配和收集);n是數據個數;r是基數,涉及到桶的個數。

空間複雜度:O(n + r)
輔助存儲:O(r)

穩定性:穩定排序

代碼實現
#include <stdio.h>
#include <stdlib.h>

/* 獲取數組內最大數 */
int find_max_num(int A[], int N)
{
	int i, max = A[0];
	for (i = 1; i < N; i++) {
		if (A[i] > max)
			max = A[i];
	}

	return max;
}

/* 獲取整數的位數*/
int get_bit_num(int maxval)
{
	int i = 0;
	while(maxval /= 10)
		i++;
	return i;
}

/* 根據數據大小和比較位獲得所在桶的索引 */
int get_buckets_idx(int data, int pos)
{
	int multi = 1;

	while(pos--)
		multi *= 10;
	
	return (data / multi) % 10;
}

void show_arr(int A[], int N)
{
	int i;
	for (i = 0; i < N; i++)
		printf("A[%d] = %d\n", i, A[i]);
}

void radix_sort(int A[], int N, int buckets)
{
	/* 桶數組空間分配 */
	int i, **bucket_table = (int **)malloc(buckets * sizeof(int *));

	/* 各桶內元素空間分配 */
	for (i = 0; i < buckets; i++) {
		bucket_table[i] = (int *)malloc(N * sizeof(int));
		bucket_table[i][0] = 0;/* 每個桶的0位置記錄分配到該桶內的元素個數 */
	}

	int *p, j, idx, n, bitn;
	bitn = get_bit_num(find_max_num(A, N));
	/* 從最低優先位排序LSD(Least Significant Digit first) */
	for (i = 1; i <= bitn; i++) {
		/* 按照位i進行分配 */
		for (j = 0; j < N; j++) {
			idx = get_buckets_idx(A[j], i);
			p = bucket_table[idx];
			n = ++p[0];
			p[n] = A[j];
		}

		idx = 0;
		/* 收集過程 */
		for (j = 0; j < buckets; j++) {
			p = bucket_table[j];
			for (n = 1; n <= p[0]; n++)
				A[idx++] = p[n];
			p[0] = 0;
		}
	}
}

int main()
{
	int A[] = {326, 453, 608, 835, 751, 435, 704, 690, 88, 79, 79};
	
	int size = sizeof(A)/sizeof(int);

	radix_sort(A, size, 10);

	show_arr(A, size);
}

6.總結

6.1算法穩定性

算法穩定性:若待排序序列中,存在多個相同的關鍵字記錄,經過排序後,相同關鍵字之間的相對次序保持不變,則該算法是穩定的,否則就是不穩定的。

穩定的排序算法:基數排序,直接插入排序,冒泡排序,歸併排序,計數排序(與實現相關)

不穩定的排序算法:快速排序,直接選擇排序,堆排序,希爾排序,桶排序

6.2排序算法的選擇

假設待排序元素個數爲N,一般的
1.當N較大時,但內存空間有限時,應採取外部排序
2.當N較大,但內存空間允許時,應選擇時間複雜度較低的方法,如快排序,堆排序,歸併排序等
3.當N較小,可採用直接插入排序或者選擇排序
4.一般不建議使用冒泡排序
5.線性排序適用於待排序列按範圍分佈較均勻時,基數排序適用於按多個關鍵排序場景。

6.3算法複雜度及穩定性速查表



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