C语言实现八大排序算法

目录

1.插入排序

1.1 直接插入排序

 1.2 希尔排序

2. 选择排序

2.1 直接选择排序

2.2 堆排序 

*TopK问题:

3. 交换排序

3.1 冒泡排序

 3.2 快速排序

1. Hoare版本

2. 挖坑法

3. 前后指针法

4. 快速排序优化

5. 非递归快速排序

4.归并排序 

1.递归式归并排序

2. 非递归式归并排序

5.计数排序

排序算法性能总结


1.插入排序

1.1 直接插入排序

直接插入排序是一种简单的插入排序法,其基本思想是:

把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列 。

比如我们玩扑克牌的时候就选择了直接插入排序:

3743fab8a47a41caa27d4ac848b31a49.png

上面举了我们玩扑克牌的例子,我们在每次拿新牌之前,手里的牌就已经是一个有序数组了,新来的这张牌再插入这个有序数组中,保证新的数组还是有序的。

还拿扑克牌的例子,扑克牌游戏刚开始,我们手里的牌是“有序数组”(空手也可视为有序数组),牌堆里的牌是一个“无序数组”,每来一张牌我们都会拿这张牌与手中的牌比较(从后往前比),找到既不小于前者又不大于后者的位置插入。

那么直接插入排序的实现为:

//直接插入排序
void InsertSort(int* a, int n)
{
	//[0,end]有序,把end+1位置的数据插入到前序序列
	//控制[0,end+1]有序
	for (size_t i = 0; i < n - 1; i++) {
		int end = i;//有序数组的最后一个
		int tmp = a[end + 1];//无序数组的第一个

		while (end >= 0)//end = -1时,一张牌(一个数据)的排序完成
		{
			if (tmp < a[end]) {
				a[end + 1] = a[end];
			}
			else {
				break;
			}
 
			--end;
		}
		a[end + 1] = tmp;
	}
}

直接插入排序特性总结:

1. 元素集合越接近有序,直接插入排序算法的时间效率越高;

2. 时间复杂度:O(N²);

3. 空间复杂度:O(1),它是一种稳定的排序算法;

4. 稳定性:稳定。

 1.2 希尔排序

希尔排序是在直接插入排序之前,先进行预排序,使数组达到“相对有序”的状态再进行直接插入排序。

即:

①将数组中每隔gap距离的数归为一组,将每组进行一次直接插入排序,然后将gap变小,继续对每组数据进行直接插入排序;

②直到gap=1时,进行直接插入排序。

图解: 

1032c2e659d1466089ded9496e4bec12.png

代码如下:

其实:希尔排序就相当于:在1.2中的代码中,将int tmp = a[end + 1];改为int tmp = a[end + gap];再加以修改:

//希尔排序
void ShellSort(int* a, int n)
{
	int gap = n;
	while (gap > 1)
	{
		gap = gap / 2;//先①预排序;gap总会变为1,变为1就是②
		for (size_t i = 0; i < n - gap; i++)
		{
			int end = i;
			int tmp = a[end + gap];
			while (end >= 0)
			{
				if (tmp < a[end]) {
					a[end + gap] = a[end];
					end -= gap;
				}
				else {
					break;
				}
				a[end + gap] = tmp;
			}
		}
	}
}

希尔排序特性总结:

1. 希尔排序是对直接插入排序的优化;

2. 当gap > 1时都是预排序,目的是让数组更接近于有序。当gap == 1时,数组已经接近有序的了,这样就会很快。这样整体而言,可以达到优化的效果;

3. 希尔排序的时间复杂度不好计算,因为gap的取值方法很多,导致很难去计算,因此在一些书中给出的希尔排序的时间复杂度都不固定:如:《数据结构-用面向对象方法与C++描述》--- 殷人昆:

d28e707c296c40039e43108d408b073e.png

4. 稳定性:不稳定。

2. 选择排序

2.1 直接选择排序

直接选择排序是从待排序数据中每次选出最小或最大的元素,存放在序列的起始位置,直到全部数据排序完成为止;为了增加效率,在此基础上每次都选出序列中最大和最小的元素,将其放在序列的起始或末尾位置,直到全部数据排完为止。

eb98dc096f8f4707840b8aa74be0b12c.png

代码如下:

void Swap(int* x, int* y)
{
	int tmp = *x;
	*x = *y;
	*y = tmp;
}

void SelectSort(int* a, int n)
{
	int begin = 0, end = n - 1;
	while (begin < end)
	{
		//[begin,end]
		int mi = begin, ma = begin;
		for (int i = begin + 1; i <= end; i++)
		{
			if (a[i] > a[ma]) {
				ma = i;
			}
			if (a[i] < a[mi]) {
				mi = i;
			}
		}

		Swap(&a[begin], &a[mi]);
		if (ma == begin) {//避免此时最大值已经换到了最小值的位置造成的错误
			ma = mi;
		}
		Swap(&a[end], &a[ma]);
		++begin;
		--end;
	}
}

直接选择排序特性总结:

1. 直接选择排序思考非常好理解,但是效率不是很好。实际中很少使用;

2. 时间复杂度:O(N^2);

3. 空间复杂度:O(1);

4. 稳定性:不稳定。

2.2 堆排序 

堆排序(Heapsort)是指利用堆积树(堆)这种数据结构所设计的一种排序算法,它是选择排序的一种。它是通过堆来进行选择数据。需要注意的是排升序要建大堆,排降序建小堆。

关于堆的概念,我们在数据结构7——二叉树的顺序结构以及堆的实现已经讲过,详情可以点击链接跳转,此处不再详细赘述。

关于堆排序的实现,我们同样在上述文章中已经实现过,只需将其AdjustDown(向下调整)函数引用过来即可,不过在那篇文章中我们实现的是小堆,我希望排序得到一个升序序列,那么我就需要建大堆,只需稍微修改即可;

第一次调用AdjustDown函数使数组变成大根堆,第二次调用AdjustDown函数完成数组的排序:

图解:

e7f8e3601b094f73b98b77b0ea49e8eb.png

代码如下: 

//向下调整
void AdjustDown(int* a, int n, int parent)
{
	int child = parent * 2 + 1;
	while (child < n)
	{
		//找出大的那个孩子
		if (child + 1 < n && a[child + 1] > a[child])//a[child + 1] < a[child]是建小堆
		{
			++child;
		}

		if (a[child] > a[parent])//(a[child] < a[parent])是建小堆
		{
			Swap(&a[child], &a[parent]);
			//继续往下调整
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}

//堆排序
void HeapSort(int* a, int n)
{
	// 向下调整建堆
	// O(N)
	for (int i = (n - 1 - 1) / 2; i >= 0; i--)//i表示从最后一个非叶子节点开始向下调整
	{
		AdjustDown(a, n, i);
	}

	//此时只是完成了大根堆,数组现在还并不是有序的
	// O(N*logN)
	int end = n - 1;
	while (end > 0)
	{
		//交换数组的首尾元素
		//使--end之后的末尾元素一直都是最大的
		Swap(&a[0], &a[end]);
		AdjustDown(a, end, 0);
		--end;
	}
}

堆排序特性总结:

1. 堆排序使用堆来选数,效率就高了很多;

2. 时间复杂度:O(N*logN);

3. 空间复杂度:O(1);

4. 稳定性:不稳定 。

*TopK问题:

TopK问题,可能你要问了,这篇文章到现在已经讲了四种排序了,我排好序,找出前K个或后K个数据不就好了吗? 这种方法当然可以,可是当数据足够多时,有没有效率更高的算法呢?——堆排序就为我们提供了解决这种问题效率极高的思路:

例如:找出n个数中的k个最小的数?

我们知道,堆顶元素就是这个堆中的最大值或最小值。

在这n个数中取k个数建大堆,再将剩下的n-k个数依次与堆顶元素进行比较,大于堆顶元素的数据直接舍弃,小于堆顶元素的数据与堆顶元素进行交换,再重新进行堆排序(保持堆序性),将n-k个数遍历完毕,此时堆中的数据就是k个最小的数。

(为什么要建大堆?可以思考一下,如果建了小堆,此时的堆顶元素就是这k个数中的最小值,但如果恰巧此时的堆顶元素就是n个数中的最小值呢?那么剩下的n-k个数都无法进堆了;所以要明白:找最小的k个数建大堆,找最大的k个数建小堆

具体实现如下:

void PrintTopK(int* a, int n, int k)
{
	//①先将数组a的前k个数建堆
	int* Heap = (int*)malloc(k * sizeof(int));
	if (Heap == NULL) {
		perror("malloc:");
		exit(-1);
	}
	for (int i = 0; i < k; i++)
	{
		Heap[i] = a[i];
	}
	//②建大堆
	AdjustDown(Heap, k, 0);
	//③
	for (int i = k; i < n; i++)
	{
		if (a[i] < Heap[0]) {
			Heap[0] = a[i];
			AdjustDown(Heap, k, 0);
		}
		
	}
	for (int i = 0; i < k; i++)
	{
		printf("%d ", Heap[i]);
	}
	free(Heap);
	Heap = NULL;
}

3. 交换排序

3.1 冒泡排序

冒泡排序是我们最熟悉的排序。所谓交换,就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置,交换排序的特点是:将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动。

图解: 

9adf7c6e3b2c45fbbef9b2ad9e9776fd.png

代码如下: 

//冒泡排序
void BubbleSort(int* a, int n)
{
	for (int i = 0; i < n; i++)
	{
		int exchange = 0;
		for (int j = 1; j < n - i; j++)
		{
			if (a[j - 1] > a[j]) {
				Swap(&a[j - 1], &a[j]);
				exchange = 1;
			}
		}
		if (exchange == 0) {//如果在第一轮排序中exchange的值未变化,说明数组已经是有序的了,此时可以提前结束,提高效率
			break;
		}
	}
}

冒泡排序特性总结:

1. 冒泡排序是一种非常容易理解的排序;

2. 时间复杂度:O(N^2);

3. 空间复杂度:O(1);

4. 稳定性:稳定 。

 3.2 快速排序

快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法,其基本思想为:

任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。

(为了方便,基准值一般选在序列的最左边或最右边)

1. Hoare版本

基本步骤为:

①选择序列最左边的值作为基准值keyi;

②确定left与right分别为序列的初始值和末尾值,令二者往序列中间走;

③让right先走,遇到比keyi小的值就停下(找小),令left后走,遇到比keyi大的值就停下(找大);

④二者都停下之后交换数据;

⑤重复③和④,直到right与left相遇;

⑥交换keyi与left的值。

此时被交换的keyi的值,即现在left的值已经到达了它该到达的位置。

(注:当left与right相遇时,相遇时刻的值一定比keyi小,这是③我们令right先走决定的,而right先走又是①我们令最左边的值作为基准值所决定的。

当然,当我们令序列最右边的值作为基准值时,就应该令left先走了,理所应当此时left与right相遇时的值也一定比keyi大。)

图解:

 070c81ac046543c29bed8e1c2b4004e7.png

上述的基本步骤和图解只完成了一趟的排序工作,若想完成全部数据的排序工作,又该如何设计代码呢?

仔细观察上述的序列,在6到达指定位置之后,6就不需要再参与排序工作了,那么剩下的就是6的左边序列和右边序列分别继续进行单趟排列,这种方法与二叉树的先序遍历及其相似,所以模仿二叉树的先序遍历,我们采用递归的方法,对左右序列继续进行排序工作。

具体代码如下:

//①Hoare版本
int PartSort1(int* a, int left, int right)
{
	int 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[keyi], &a[left]);
	return left;
};

void QuickSort(int* a, int begin, int end)
{
	if (begin >= end) {
		return;//递归终止条件
	}
	int keyi = PartSort1(a, begin, end);
	QuickSort(a, begin, keyi - 1);//左序列
	QuickSort(a, keyi + 1, end);//右序列
}

2. 挖坑法

挖坑法与Hoare版本类似,具体步骤为:

①将序列最左边的值取出作为基准值key,值取出后,此时序列最左边留下“坑位”;

②确定left与right分别为序列的初始值和末尾值,令二者往序列中间走;

③让right先走,遇到比key小的值就停下(找小),停下后将right位置的值取出去填“坑位”,此时right位置就留下“坑位”;

④令left后走,遇到比key大的值就停下(找大),停下后将left位置的值取出去填“坑位”,此时left位置就留下“坑位”;

⑤重复③和④,直到right与left相遇,相遇的地点必定是“坑位”,此时用key去填“坑位”。

图解:

1ec64472ec144f8699b2ff504b3c5fd0.png

具体代码如下:

//②挖坑法
int PartSort2(int* a, int left, int right)
{
	int key = a[left];
	// 保存key值以后,左边形成第一个坑
	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;
	}

	a[hole] = key;
	return hole;
}

void QuickSort(int* a, int begin, int end)
{
	if (begin >= end) {
		return;//递归终止条件
	}
	int keyi = PartSort2(a, begin, end);
	QuickSort(a, begin, keyi - 1);//左序列
	QuickSort(a, keyi + 1, end);//右序列
}

3. 前后指针法

前后指针法具体步骤为:

① 将序列最左边的值确定为基准值key,创建指针prev和cur(cur=prev+1);

② 先比较此时cur位置的值与key的大小,

    若小于key,则prev++,然后交换prev与cur位置的值;

    若cur不小于key,则prev保持不动;

③ cur++;

④ 重复②和③,直到cur超出序列范围为止;

⑤ 此时交换prev与key的值。

(应该注意的点:步骤笼统来说是:先prev++,再交换prev与cur,最后cur++)

图解:

95b716b1a0244a558b9b88219338aa28.png

 具体代码如下:

//③前后指针法
int PartSort3(int* a, int left, int right)
{
	int prev = left;
	int cur = prev + 1;

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

		++cur;
	}

	Swap(&a[prev], &a[keyi]);
	return prev;
}
void QuickSort(int* a, int begin, int end)
{
	if (begin >= end) {
		return;//递归终止条件
	}
	int keyi = PartSort3(a, begin, end);
	QuickSort(a, begin, keyi - 1);//左序列
	QuickSort(a, keyi + 1, end);//右序列
}

4. 快速排序优化

在介绍快速排序刚开始时我们就提到:“快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法”,这种排序算法类似于二叉树的先序遍历:以基准值为界,分左右两边进行递归。

可现在问题来了:

如果选择的基准值恰巧是序列中的最大值或最小值呢?

这时就会进行不必要的递归,更是在排序大量有序数据或者接近有序数据时,效率会比较低,甚至可能会出现程序崩溃的情况;

这是因为在排序有序数据时,快速排序的递归调用次数过多,会导致栈溢出的情况。

为了解决这种问题,有两种优化方法:

①三数取中选key值:

即在序列的初始位置、中间位置、末尾位置的三个数中选出中间值作为key值;

②递归到小的子区间时,可以考虑使用插入排序。

具体代码如下:

// 三数取中
int GetMidi(int* a, int left, int right)
{
	int mid = (left + right) / 2;
	if (a[left] < a[mid]) {
		if (a[mid] < a[right]) {
			return mid;
		}
		else if (a[left] > a[right]) {// mid是最大值
			return left;
		}
		else {
			return right;
		}
	}
	else {// a[left] > a[mid]
		if (a[mid] > a[right]) {
			return mid;
		}
		else if (a[left] < a[right]) {// mid是最小
			return left;
		}
		else {
			return right;
		}
	}
}

 以Hoare版本为例,那么快速排序的优化版本代码为:

//①Hoare版本
int PartSort1(int* a, int left, int right)
{
	int mid = GetMidi(a, left, right);
	Swap(&a[left], &a[mid]);

	int 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[keyi], &a[left]);
	return left;
};
void QuickSort(int* a, int begin, int end)
{
	if (begin >= end) {
		return;//递归终止条件
	}

	if (end - begin + 1 < 10) {//区间长度小于10时采用插入排序
		InsertSort(a + begin, end - begin + 1);
	}
	else {
		int keyi = PartSort1(a, begin, end);
		QuickSort(a, begin, keyi - 1);//左序列
		QuickSort(a, keyi + 1, end);//右序列
	}
}

5. 非递归快速排序

在4中,我们优化了快速排序,为的就是减少快速排序的递归次数,可是尽管减少了递归次数,可是当数据足够足够大时,又不得不进行足够多次递归了。

递归次数足够多就有可能造成“爆栈”——栈溢出!的风险,以上四种方法都是递归式的快速排序,接下来介绍一种非递归的快速排序。

其实非递归快速排序基本思想同以上三种方法大体相同:

① 取数据序列初和序列末的下标入栈;

② 两次取出栈顶元素;

③ 调用一次Hoare版本的排序算法(挖坑法和前后指针法当然也可以),以获得“基准值”处的坐标;

④ 此时基准值已到达正确的位置,在此处将序列一分为左右两个序列;

⑤ 若左右两个序列有元素,则将左右序列的初始和末尾下标分别继续入栈;

⑥ 重复②③④⑤,直到栈为空停止。

图解:

b3908633978f4cf8af71b3e0aab804d2.png

具体代码如下:

void QuickSortNonR(int* a, int begin, int end)
{
	ST st;
	STInit(&st);
	STPush(&st, end);
	STPush(&st, begin);
	while (!STEmpty(&st))
	{
		int left = STTop(&st);
		STPop(&st);

		int right = STTop(&st);
		STPop(&st);

		int keyi = PartSort1(a, left, right);
		// [lefy,keyi-1] keyi [keyi+1, right]
		if (keyi + 1 < right)
		{
			STPush(&st, right);
			STPush(&st, keyi + 1);
		}

		if (left < keyi - 1)
		{
			STPush(&st, keyi - 1);
			STPush(&st, left);
		}
	}

	STDestroy(&st);
}

(对于栈的知识和栈的实现,具体可参考往期文章数据结构4——栈) 

快速排序特性总结:

1. 快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫快速排序;

2. 时间复杂度:O(N*logN);

3. 空间复杂度:O(logN);

4. 稳定性:不稳定。

4.归并排序 

归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。 

aa36a8e76c0a458a9d32881cb921830d.png

1.递归式归并排序

按照这种思想,是将一个序列分解为若干个有序的子序列,再将这若干个有序的子序列合并成一个有序的整序列,为了方便判断,直接将子序列分解到只剩一个元素,一个元素当然可以算作有序呀!

所以归并排序的基本步骤为:

① 将整个序列平分(一分二,二分四......),直到序列只剩一个元素为止;

② 将“同时分家”的两个子序列进行排序,排好序后,再“放回到他们的父亲家里”;

(这里就可以创建一个临时数组tmp,按照两个子序列谁的元素小谁先入数组的原则排序,将排好序的tmp再拷贝回“父序列”)

 图解:

aea556ccdd184020a1056878ac734c45.png

具体代码如下:

//归并排序
void _MergeSort(int* a, int* tmp, int begin, int end)
{
	if (end <= begin)
		return;

	int mid = (end + begin) / 2;
	// [begin, mid][mid+1, end]
	_MergeSort(a, tmp, begin, mid);
	_MergeSort(a, tmp, mid + 1, end);

	// 归并到tmp数据组,再拷贝回去
	// a->[begin, mid][mid+1, end]->tmp
	int begin1 = begin, end1 = mid;
	int begin2 = mid + 1, end2 = end;
	int index = begin;

	//两个序列谁的初始元素小谁就插入到tmp的末尾
	//只要有一个序列变为空就停止循环
	while (begin1 <= end1 && begin2 <= end2)
	{
		if (a[begin1] < a[begin2])
		{
			tmp[index++] = a[begin1++];
		}
		else
		{
			tmp[index++] = a[begin2++];
		}
	}

	//此时必定还有一个序列不为空
	//这个序列已经有序了,此序列的“头”插入tmp的“尾”
	while (begin1 <= end1)
	{
		tmp[index++] = a[begin1++];
	}
	while (begin2 <= end2)
	{
		tmp[index++] = a[begin2++];
	}

	// tmp完成,拷贝回原数组
	memcpy(a + begin, tmp + begin, (end - begin + 1) * sizeof(int));
}

void MergeSort(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		perror("malloc fail");
		return;
	}

	_MergeSort(a, tmp, 0, n - 1);//增加一个子函数,避免tmp被重复创建和销毁

	free(tmp);
}

2. 非递归式归并排序

在讲非递归快速排序时,我们提到,递归存在一定风险,当数据足够足够多时可能会有爆栈的风险,因此这里就对归并排序提供了非递归的思路:

归并排序的思想就是分治法-先分解再合并,而以上代码发生递归的地方就是出现在分解过程,为了不递归,能不能绕过分解过程直接合并呢?

所以非递归的思想与递归的思想大致相同,不同的是,非递归思想是:

先让每一个元素为一组两两归并,再每两个元素为一组两两归并,再每四个元素为一组两两归并,再每八个......直到归并完所有元素为止。

图解:

35efb0408e954434b874a7da3f231a26.png

 具体代码如下:

//非递归式
void MergeSortNonR(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		perror("malloc fail");
		return;
	}

	int gap = 1;
	while (gap < n)
	{
		for (int i = 0; i < n; i += 2 * gap)
		{
			int begin1 = i, end1 = i + gap - 1;
			int begin2 = i + gap, end2 = i + 2 * gap - 1;
			// [begin1,end1] [begin2,end2] 归并

			//当序列元素个数出现奇数个时,必定会有“落单”的情况,即出现数组越界
			// 增加判断,防止出现这一情况
			// 如果第二组不存在,这一组不用归并了
			if (begin2 >= n)
			{
				break;
			}
			// 如果第二组的右边界越界,修正一下
			if (end2 >= n)
			{
				end2 = n - 1;
			}

			int index = i;
			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++];
			}

			// 拷贝回原数组
			memcpy(a + i, tmp + i, (end2 - i + 1) * sizeof(int));
		}
		gap *= 2;
	}
	free(tmp);
}

归并排序特性总结:

1. 归并的缺点在于需要O(N)的空间复杂度,归并排序的思考更多的是解决在磁盘中的外排序问题;

2. 时间复杂度:O(N*logN);

3. 空间复杂度:O(N);

4. 稳定性:稳定。

5.计数排序

计数排序又称为鸽巢原理,是对哈希直接定址法的变形应用。

操作步骤:

1. 统计相同元素出现次数;

2. 根据统计的结果将序列回收到原来的序列中。

图解:

 54d2ef4c617044b2b666f12a87b24d86.png

具体代码如下:

//计数排序
void CountSort(int* a, int n)
{
	//找出原数组的最小值和最大值
	int min = a[0], max = a[0];
	for (int i = 0; i < n; i++)
	{
		if (a[i] < min)
			min = a[i];

		if (a[i] > max)
			max = a[i];
	}

	int range = max - min + 1;
	int* count = (int*)malloc(sizeof(int) * range);
	//printf("range:%d\n", range);
	if (count == NULL)
	{
		perror("malloc fail");
		return;
	}
	memset(count, 0, sizeof(int) * range);

	// 统计数据出现次数
	for (int i = 0; i < n; i++)
	{
		count[a[i] - min]++;
	}

	// 排序
	int j = 0;
	for (int i = 0; i < range; i++)
	{
		while (count[i]--)
		{
			a[j++] = i + min;
		}
	}
}

(注:计数排序只适用于排列整数,当序列中既有正数又有负数时,则不适合用计数排序;另外,计数排序只适用于数据相对集中的序列,如:就排列三个数,99999999、1、2,数据跨度太大,就要开辟一个长度为99999999的数组,显然这是不合适的)

计数排序特性总结:

1. 计数排序在数据范围集中时,效率很高,但是适用范围及场景有限;

2. 时间复杂度:O(N + range);

3. 空间复杂度:O(range) ;

4. 稳定性:稳定。



排序算法性能总结

79f501b1b1034a37a32b7d5720d02003.png

所谓稳定性,举个例子就明白了:比如将一副扑克牌排序,排序之前红桃十在黑桃十的前面,排序之后红桃十依旧在黑桃十的前面,即序列中相同元素在排序前后相对位置不发生变化,就认为该排序算法稳定。 



最后的最后,写一段程序对以上实现的所有排序进行简单的测试:

void TestOP()
{
	srand(time(0));
	const int N = 10000;
	int* a1 = (int*)malloc(sizeof(int) * N);
	int* a2 = (int*)malloc(sizeof(int) * N);
	int* a3 = (int*)malloc(sizeof(int) * N);
	int* a4 = (int*)malloc(sizeof(int) * N);
	int* a5 = (int*)malloc(sizeof(int) * N);
	int* a6 = (int*)malloc(sizeof(int) * N);
	int* a7 = (int*)malloc(sizeof(int) * N);
	int* a8 = (int*)malloc(sizeof(int) * N);
	int* a9 = (int*)malloc(sizeof(int) * N);
	int* a10 = (int*)malloc(sizeof(int) * N);

	for (int i = N - 1; i >= 0; --i)
	{
		a1[i] = rand() + i;
		a2[i] = a1[i];
		a3[i] = a1[i];
		a4[i] = a1[i];
		a5[i] = a1[i];
		a6[i] = a1[i];
		a7[i] = a1[i];
		a8[i] = a1[i];
		a9[i] = a1[i];
		a10[i] = a1[i];
	}

	int begin1 = clock();
	InsertSort(a1, N);
	int end1 = clock();

	int begin2 = clock();
	ShellSort(a2, N);
	int end2 = clock();

	int begin3 = clock();
	SelectSort(a3, N);
	int end3 = clock();

	int begin4 = clock();
	HeapSort(a4, N);
	int end4 = clock();

	int begin5 = clock();
	BubbleSort(a5, N);
	int end5 = clock();

	int begin6 = clock();
	QuickSort(a6, 0, N - 1);
	int end6 = clock();

	int begin7 = clock();
	QuickSortNonR(a7, 0, N - 1);
	int end7 = clock();

	int begin8 = clock();
	MergeSort(a8, N);
	int end8 = clock();

	int begin9 = clock();
	MergeSortNonR(a9, N);
	int end9 = clock();

	int begin10 = clock();
	CountSort(a10, N);
	int end10 = clock();

	printf("InsertSort:%d\n", end1 - begin1);
	printf("ShellSort:%d\n", end2 - begin2);
	printf("SelectSort:%d\n", end3 - begin3);
	printf("HeapSort:%d\n", end4 - begin4);
	printf("BubbleSort:%d\n", end5 - begin5);
	printf("QuickSort:%d\n", end6 - begin6);
	printf("QuickSortNonR:%d\n", end7 - begin7);
	printf("MergeSort:%d\n", end8 - begin8);
	printf("MergeSortNonR:%d\n", end9 - begin9);
	printf("CountSort:%d\n", end10 - begin10);


	free(a1);
	free(a2);
	free(a3);
	free(a4);
	free(a5);
	free(a6);
	free(a7);
	free(a8);
	free(a9);
	free(a10);
}

测试结果:

2fba409c703448c8b9b14077bebc4768.png

由测试结果可以看出,快排之所以敢叫快排,是因为其确实是有一定实力的,计数排序虽然应用场景比较少,但是在特定场景下确实是比较有优势的。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/938017.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

走进 RAG 技术:一场智能数据交互的奇幻之旅

朋友们&#xff0c;咱身处的这个时代&#xff0c;科技那可是跟开了挂似的往前冲&#xff0c;其中人工智能更是厉害得没话说&#xff0c;宛如一个充满无限可能的魔法领域&#xff0c;时不时就给咱的生活来个大变样。而在这其中&#xff0c;RAG 技术就像是突然冒出来的一颗超亮眼…

商业化大前端在性能优化领域的探索与实践

导读&#xff1a;在业务飞速发展的过程中&#xff0c;用户体验是必不可少的一个环节&#xff0c;而页面性能是直接影响用户体验的重要因素。当页面加载时间过长、交互操作不流畅时&#xff0c;意味着业务可能会出现转化率降低、用户流失等业务问题。在过去一年&#xff0c;为了…

C# 位运算

一、数据大小对应关系 说明&#xff1a; 将一个数据每左移一位&#xff0c;相当于乘以2。因此&#xff0c;左移8位就是乘以2的8次方&#xff0c;即256。 二、转换 1、 10进制转2进制字符串 #region 10进制转2进制字符串int number1 10;string binary Convert.ToString(num…

YOLOv10改进,YOLOv10利用DLKAttention融合DCNv3、DCNv4形成全新的可变形大核注意力,并二次创新C2f结构,全网首发

理论介绍 完成本篇需要参考以下三篇文章,并已添加到YOLOv10代码中 YOLOv10改进,YOLOv10添加DCNv3可变性卷积与C2f结构融合(无需编译)YOLOv10改进,YOLOv10添加DCNv4可变性卷积(windows系统成功编译),全网最详细教程YOLOv10改进,YOLOv10添加DLKA-Attention可变形大核注意力…

Linux高性能服务器编程 | 读书笔记 | 8. 信号

8. 信号 信号是由用户、系统、进程发送给目标进程的信息&#xff0c;以通知目标进程某个状态的改变或系统异常。Linux信号可由以下条件产生&#xff1a; 对于前台进程&#xff0c;用户可通过输入特殊终端字符来给它发送信号&#xff0c;如输入CtrlC通常会给进程发送一个中断信…

记录学习《手动学习深度学习》这本书的笔记(五)

这一章是循环神经网络&#xff0c;太难了太难了&#xff0c;有很多卡壳的地方理解了好久&#xff0c;比如隐藏层和隐状态的区别、代码的含义&#xff08;为此专门另写了一篇【笔记】记录对自主实现一个神经网络的步骤的理解&#xff09;、梯度计算相关&#xff08;【笔记】记录…

【git、gerrit】特性分支合入主分支方法 git rebase 、git cherry-pick、git merge

文章目录 1. 场景描述1.1 分支状态 2. 推荐的操作方式方法 1&#xff1a;git merge&#xff08;保留分支结构&#xff09;方法 2&#xff1a;git rebase&#xff08;线性合并提交历史&#xff09;直接在master分支执行git merge br_feature&#xff0c;再 执行 git pull --reba…

Python-基于Pygame的小游戏(天空之战)(一)

前言:不久前接触了Python的游戏制作的相关第三方库&#xff0c;于是学习了pygame的相关内容&#xff0c;想制作一款基于pygame的小游戏。因为还不太熟悉游戏制作和pygame&#xff0c;部分内容我参考了《Python-从入门到精通》这本书。那么好&#xff0c;话不多说&#xff0c;我…

探索 Cesium 的未来:3D Tiles Next 标准解析

探索 Cesium 的未来&#xff1a;3D Tiles Next 标准解析 随着地理信息系统&#xff08;GIS&#xff09;和 3D 空间数据的快速发展&#xff0c;Cesium 作为领先的开源 3D 地球可视化平台&#xff0c;已成为展示大规模三维数据和进行实时渲染的强大工具。近年来&#xff0c;随着…

Redis和数据库的一致性(Canal+MQ)

想要保证缓存与数据库的双写一致&#xff0c;一共有4种方式&#xff0c;即4种同步策略&#xff1a; 先更新缓存&#xff0c;再更新数据库&#xff1b;先更新数据库&#xff0c;再更新缓存&#xff1b;先删除缓存&#xff0c;再更新数据库&#xff1b;先更新数据库&#xff0c;再…

CNCF云原生生态版图-分类指南(一)- 观测和分析

CNCF云原生生态版图-分类指南&#xff08;一&#xff09;- 观测和分析 CNCF云原生生态版图-分类指南一、观测和分析&#xff08;Observability and Analysis&#xff09;&#xff08;一&#xff09;可观测性&#xff08;Observablility&#xff09;1. 是什么&#xff1f;2. 解决…

JVM运行时数据区内部结构

VM内部结构 对于jvm来说他的内部结构主要分成三个部分&#xff0c;分别是类加载阶段&#xff0c;运行时数据区&#xff0c;以及垃圾回收区域&#xff0c;类加载我们放到之后来总结&#xff0c;今天先复习一下类运行区域 首先这个区域主要是分成如下几个部分 下面举个例子来解释…

C语言学习day22:URLDownloadToFile函数/开发文件下载工具

简言&#xff1a; 在之前我们去下载某个东西都是用的迅雷之类的软件&#xff0c;但是现在&#xff0c;只要提供一个地址&#xff0c;或者一个链接&#xff0c;我们自己去做一个工具去下载。这就是我们这篇的主要内容。 也就是我们的winAPI&#xff1a;URLDownloadToFile函数 …

购物车案例--分模块存储数据,发送请求数据渲染,底部总计数量和价格

shift鼠标右键&#xff0c;打开powershell&#xff0c;新建项目 自定义 只有一个页面&#xff0c;不涉及路由&#xff0c;勾选vuex,css,babel 无需保存预设 回车项目开始创建 项目用vscode打开 将src里的内容全部清空 将第七天的课程准备代码复制粘贴到src中 刷新页面&…

SQL server学习06-查询数据表中的数据(中)

目录 一&#xff0c;聚合函数 1&#xff0c;常用聚合函数 2&#xff0c;具体使用 二&#xff0c;GROP BY子句分组 1&#xff0c;基础语法 2&#xff0c;具体使用 3&#xff0c;加上HAVING对组进行筛选 4&#xff0c;使WHERE记录查询条件 汇总查询&#xff1a;在对数…

YOLOv5-7.0训练过程中出现报错Example: export GIT_PYTHON_REFRESH=quiet

出现报错&#xff1a; This initial message can be silenced or aggravated in the future by setting the $GIT_PYTHON_REFRESH environment variable. Use one of the following values: - quiet|q|silence|s|silent|none|n|0: for no message or exception - warn…

从0到1实现vue3+vite++elementuiPlus+ts的后台管理系统(一)

前言&#xff1a;从这篇文章开始实现vue3vite的后台管理系统&#xff0c;记录下自己搭建后台系统图的过程。 这篇文章完成项目的初始化和基本配置&#xff0c;这一步可以直接跟着vue3官网进行。整个系列只有前端部分&#xff0c;不涉及后端。 vue3官网&#xff1a;https://cn.…

Spring Boot教程之二十五: 使用 Tomcat 部署项目

Spring Boot – 使用 Tomcat 部署项目 Spring Boot 是一个基于微服务的框架&#xff0c;在其中创建可用于生产的应用程序只需很少的时间。Spring Boot 建立在 Spring 之上&#xff0c;包含 Spring 的所有功能。如今&#xff0c;它正成为开发人员的最爱&#xff0c;因为它是一个…

java中操作线程

文章目录 前言创建与运行线程1. 创建线程①、方法1(直接new)②、方法2(使用Runnable配合Thread进行new操作)③、方法3&#xff08;FutureTask对象实现&#xff09;④、线程创建原理特别注意&#xff01; 2查看与杀死线程①、 Windows下 &#xff1a;②、 Java下 &#xff1a; 3…

【redis】redix在Linux下的环境配置和redis的全局命令

˃͈꒵˂͈꒱ write in front ꒰˃͈꒵˂͈꒱ ʕ̯•͡˔•̯᷅ʔ大家好&#xff0c;我是xiaoxie.希望你看完之后,有不足之处请多多谅解&#xff0c;让我们一起共同进步૮₍❀ᴗ͈ . ᴗ͈ აxiaoxieʕ̯•͡˔•̯᷅ʔ—CSDN博客 本文由xiaoxieʕ̯•͡˔•̯᷅ʔ 原创 CSDN 如…