数据结构与算法分析之排序算法总结

十大常用排序算法总结

1.交换排序

交换排序是通过元素间的比较和交换来完成,分为冒泡排序和快速排序两种。

1.1冒泡排序

冒泡排序是最简单的一种排序方法。其排序过程是类似冒泡一样,通过相邻元素之间的比较和交换将小的元素逐渐交换到最前面或者将大的元素逐渐交换到最后面。

时间复杂度
最坏情况:O(n^2)
平均情况:O(n^2)
最好情况:O(n)
空间复杂度:O(n)
辅助存储:O(1)

稳定性:稳定排序

代码实现
#include 
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 

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 

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 

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 

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 
#include 

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 
#include 

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个桶,这样需要的辅助空间较少,但是需要对桶上数据进行排序。这一排序在分配桶的过程中也是线性的(参考代码)。

举个例子:
                                                                                                                            数据结构与算法分析之排序算法总结_第1张图片
假如待排序列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 
#include 

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 
#include 

/* 获取数组内最大数 */
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算法复杂度及稳定性速查表

数据结构与算法分析之排序算法总结_第2张图片

你可能感兴趣的:(C语言,数据结构与算法分析)