排序【归并排序和计数排序】

1.归并排序

1.1 基本思想

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

1.2 递归法:

也就是说,我们在得到有序列之前,要保证俩个子集有序(左区间和右区间),但子集又要分成俩个小子集来使子集有序,小子集又能分出俩个更小的子集,直到子集无法在分,也就是子集仅剩一个元素。

由于我们不能太早的修改原数组的内容,所以我们需要开辟一块新空间(tmp)来暂时存放内容,等内容全部排好序,再将tmp拷贝给原数组。

由于是递归,如果在开辟空间的函数递归,就会一直开辟空间、释放空间,这样会有效率低下的问题,所以我们用一个子函数来完成排序的部分。

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
void _MergeSort(int* a, int* tmp, int begin, int end);
//归并排序
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);

	free(tmp);
	tmp = NULL;
}

void TestMergeSort()
{
	int a[] = { 9,8,6,2,3,4,5,1,7,10 };
	MergeSort(a, sizeof(a) / sizeof(int));
	PrintArray(a, sizeof(a) / sizeof(int));
}

1.2.1 子函数

void _MergeSort(int* a, int* tmp, int begin, int end);

我们先将数组进行分割,分割点就为中间点(mid),分割出 [ b e g i n , m i d ] [ m i d + 1 , e n d ] [begin, mid][mid+1, end] [begin,mid][mid+1,end],然后当集合只剩下一个元素的时候就跳出递归

void _MergeSort(int* a, int* tmp, int begin, int end)
{
	if (begin >= end)
	{
		return;
	}
	int mid = (begin + end) / 2;

	_MergeSort(a, tmp, begin, mid);
	_MergeSort(a, tmp, mid + 1, end);
}

1.2.2 排序主体

然后我们就来完成排序的主体部分,因为我们是用区间来分割数组,所以我们需要四个变量来记录俩个区间的开始和结束。
除此之外,我们还需要一个下标变量来记录元素的所在位置,那么我们给下标初始化就不能给0,而是要给相对位置begin

我们判断俩个区间哪个值更小(更大),将较小的值存放到tmp数组里,然后让较小的begin++,直到俩个区间中的任意一个区间没有值(begin == end),就跳出循环,然后将有值区间内的剩余值放到tmp数组,最后将tmp数组的值拷贝给原数组。

void _MergeSort(int* a, int* tmp, int begin, int end)
{
	if (begin >= end)
	{
		return;
	}
	int mid = (begin + end) / 2;

	_MergeSort(a, tmp, begin, mid);
	_MergeSort(a, tmp, mid + 1, end);
	
	//左区间
	int begin1 = begin, end1 = mid;
	
	//右区间
	int begin2 = mid+1, end2 = end;
	
	//下标
	int i = begin;
	while (begin1 <= end1 && begin2 <= end2)
	{
		if (a[begin1] < a[begin2])
		{
			tmp[i++] = a[begin1++];
		}

		else
		{
			tmp[i++] = a[begin2++];
		}

	}

	while (begin1 <= end1)
	{
		tmp[i++] = a[begin1++];
	}

	while (begin2 <= end2)
	{
		tmp[i++] = a[begin2++];
	}
	
	//拷贝
	memcpy(a+begin, tmp+begin, (end - begin + 1) * sizeof(int));
}

这个方法与二叉树的后序遍历很相似,先排序后左区间,在排序右区间,最后在将左右区间结合。

完整代码:

用例:int a1[] = { 9, 8, 6, 2, 11, 3, 4, 5, 1, 10, 7 }; 长度为11

#include<stdio.h>
#include<string.h>
#include<stdlib.h>

void PrintArray(int* a, size_t n)
{
	for (int i = 0; i < n; i++)
	{
		printf("%d ", a[i]);
	}
	printf("\n");
}

void _MergeSort(int* a, int* tmp, int begin, int end)
{
	if (begin >= end)
	{
		return;
	}
	int mid = (begin + end) / 2;

	_MergeSort(a, tmp, begin, mid);
	_MergeSort(a, tmp, mid + 1, end);
	
	//左区间
	int begin1 = begin, end1 = mid;
	
	//右区间
	int begin2 = mid+1, end2 = end;
	
	//下标
	int i = begin;
	while (begin1 <= end1 && begin2 <= end2)
	{
		if (a[begin1] < a[begin2])
		{
			tmp[i++] = a[begin1++];
		}

		else
		{
			tmp[i++] = a[begin2++];
		}

	}

	while (begin1 <= end1)
	{
		tmp[i++] = a[begin1++];
	}

	while (begin2 <= end2)
	{
		tmp[i++] = a[begin2++];
	}
	
	//拷贝
	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);

	free(tmp);
	tmp = NULL;
}

在这里插入图片描述

1.3 非递归方法:

出发点并不是先排序一个区间再排序另一个区间这样的深度优先遍历(DFS),而是一层一层排序的广度优先遍历(BFS)

类似下图👇:
在这里插入图片描述

1.3.1 初版

由于我们不是递归,所以我们不会用分割,而是用分组,确定每组的元素个数,再进行排序。

我们使用gap来代表每组的个数,然后两组两组的归并,归并一轮后增加gap的数量,直到gap大于大于数组长度,当然也是要将tmp的值拷贝给原数组。

//循环版
void MergeSort2(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		perror("malloc fail");
		return;
	}
	//分组 , gap为每组有多少个元素
	int gap = 1;
	while (gap < n)
	{
		for (int i = 0; i < n; i += gap * 2)
		{
			//左区间
			int begin1 = i, end1 = i + gap - 1;

			//右区间
			int begin2 = i + gap, end2 = i + gap * 2 - 1;

			//下标
			int j = i;
			while (begin1 <= end1 && begin2 <= end2)
			{
				if (a[begin1] < a[begin2])
				{
					tmp[j++] = a[begin1++];
				}

				else
				{
					tmp[j++] = a[begin2++];
				}

			}

			while (begin1 <= end1)
			{
				tmp[j++] = a[begin1++];
			}

			while (begin2 <= end2)
			{
				tmp[j++] = a[begin2++];
			}
			memcpy(a+i, tmp+i, sizeof(int) * (end2-i+1));
		}
		gap *= 2;
	}
	
	free(tmp);
	tmp = NULL;
}

在这里插入图片描述
可以看到运行成功了,但是,这个排序算法仅仅支持长度为2的次方倍的数组,因为gap是以2的倍数增长,那么当这个数组长度不是2的次方倍的话,就一定会越界访问。

举例:int a2[] = { 9,8,6,2,3,4,5,1,10,7 };
在这里插入图片描述
可以看到报错了

1.3.2 优化

我们先来看看左右区间的下标(原数组的长度是10,最后一个元素的下标为9)
在这里插入图片描述
我们可以看到画红线的就是越界了的
我们可以这样解决:当begin2越界,就代表end2肯定也是越界的,那么这次就不排序,直接退出,如果只有end2越界,那我们就将end2修正成 n − 1 n-1 n1,也就是最后一个元素的下标。
这样可以不需要管end1越界的情况,因为end1越界了,begin2肯定也越界了,就会直接退出。

			//左区间
			int begin1 = i, end1 = i + gap - 1;

			//右区间
			int begin2 = i + gap, end2 = i + gap * 2 - 1;

			if (begin2 > n)
			{
				break;
			}
			if (end2 > n)
			{
				end2 = n - 1;
			}

这样我们就不用管这个数组是不是2的倍数,是不是偶数,因为会将end2修正为n-1,归并并没有规定排序规定两个子集的长度是一样的,所以我们可以把一部分已经排好序但是begin2越界的组留下来,等到最后排序(中途也有可能排序)

完整代码:

用例:int a2[] = { 9,8,6,2,11,3,4,5,1,10,7 }; 长度为11

//循环版
void MergeSort2(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		perror("malloc fail");
		return;
	}
	//分组 , gap为每组有多少个元素
	int gap = 1;
	while (gap < n)
	{
		for (int i = 0; i < n; i += gap * 2)
		{
			//左区间
			int begin1 = i, end1 = i + gap - 1;

			//右区间
			int begin2 = i + gap, end2 = i + gap * 2 - 1;
			//printf("[%d][%d] [%d][%d]  ", begin1, end1, begin2, end2);
			
			//begin2越界,就代表这个区间不存在
			if (begin2 >= n)
			{
				break;
			}
			
			//只有end2越界,修正
			if (end2 >= n)
			{
				end2 = n - 1;
			}
			
			//下标
			int j = i;
			while (begin1 <= end1 && begin2 <= end2)
			{
				if (a[begin1] < a[begin2])
				{
					tmp[j++] = a[begin1++];
				}

				else
				{
					tmp[j++] = a[begin2++];
				}

			}

			while (begin1 <= end1)
			{
				tmp[j++] = a[begin1++];
			}

			while (begin2 <= end2)
			{
				tmp[j++] = a[begin2++];
			}
			memcpy(a+i, tmp+i, sizeof(int) * (end2-i+1));
		}

		gap *= 2;
	}
	
	free(tmp);
	tmp = NULL;
}

在这里插入图片描述

1.4 特性总结

  1. 归并的缺点就在于需要 O ( N ) O(N) O(N)的空间复杂度,归并排序主要是解决数据在磁盘(固态硬盘)中的外排序问题。
  2. 时间复杂度: O ( N ∗ l o g N ) O(N*logN) O(NlogN)
  3. 空间复杂度: O ( N ) O(N) O(N)
  4. 稳定性:稳定

2. 计数排序

计数排序其实是一种非比较排序,是不需要进行数据之间比较大小的排序。
操作步骤:

  1. 统计相同元素出现的次数
  2. 根据统计的结果,将序列回收到原来的序列中

2.1 操作讲解:

我们既然要统计次数,那么就需要建立一个tmp数组(要将tmp数组里的元素都初始化成0)。

在这里插入图片描述
然后我们遍历一遍a数组,计算a数组下标元素出现的次数,拿a[0]也就是9来举例。
a数组遍历到9的时候,那么tmp数组中下标为9的元素就+1,换成代码就是++tmp[a[i]],将a[i]作为tmp的下标。

在这里插入图片描述
这样依次类推,直到遍历完a数值,然后再遍历一遍tmp数组,将tmp数组中非零值的下标依次拷贝到原数组中。
在这里插入图片描述
但是,我们这里使用的绝对位置,如果这些数是从100开头,然后只排序10个数(101到110),那么我们真正会利用到的也就是一百以后的空间,前100个空间就就浪费了,所以我们要用相对位置。

2.2 相对位置

我们要在上文的遍历a数组开辟tmp前,先遍历一遍a数组,找到其中的最大值和最小值,然后让他们相减再加一,就是我们需要的范围了(int range = max - min + 1;),就拿上文中a数组的0~9来说吧,先遍历一遍,找到min = 0, max = 9,那么我们再根据range的公式得出 range = 10,再用range来开辟tmp数组。

void CountSort(int* a, int n)
{
	int min = a[0];
	int max = a[0];
	
	//通过遍历来获取max和min的值
	for (int i = 0; i < n; i++)
	{
		if (a[i] < min)
		{
			min = a[i];
		}
		if (a[i] > max)
		{
			max = a[i];
		}
	}
	
	//计算得出range的值并开空间
	int range = max - min + 1;
	int* count = (int*)calloc(range, sizeof(int));
	if (count == NULL)
	{
		perror("calloc fail");
		return;
	}
	//排序……
}

然后再进行上文的操作,但到了回收的时候就要注意了,这时就不能是直接让tmp下标赋值给a数组,而是a[i++] = j + min(i为a数组的下标,j为tmp数组的下标)。

2.3 完整代码

用例:int a1[] = { 9, 8, 6, -2, -11, 3, 4, 5, 1, 10, 7,10,7 ,9, 8, 6, 3, 4, 5, 1, 7, 7, 8, 6, -2, -11, 5, 1 };

void CountSort(int* a, int n)
{
	int min = a[0];
	int max = a[0];

	//通过遍历来获取max和min的值
	for (int i = 0; i < n; i++)
	{
		if (a[i] < min)
		{
			min = a[i];
		}
		if (a[i] > max)
		{
			max = a[i];
		}
	}
	//计算得出range的值并开空间
	int range = max - min + 1;
	int* count = (int*)calloc(range, sizeof(int));
	if (count == NULL)
	{
		perror("calloc fail");
		return;
	}
	
	//排序主体
	//计数
	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;
			--count[i];
		}
	}

	free(count);
	count = NULL;
}

在这里插入图片描述

可以看到这个计数排序的思想也是可以解决负数的问题。

2.4 特性总结

  1. 计数排序在数据范围集中时,效率很高,但是适用范围及场景有限。
  2. 时间复杂度: O ( M A X ( N , r a n g e ) ) O(MAX(N,range)) O(MAX(N,range))
  3. 空间复杂度: O ( r a n g e ) O(range) O(range)
  4. 稳定性:稳定

3.总结

本文主要讲解了归并排序和计数,归并排序讲解了递归与非递归的版本,递归版本是采用了二叉树遍历中后序遍历的思想(先遍历左右孩子,再遍历自己),而非递归的重点是对于区间的掌控。

计数排序其实是非比较排序的一种,还有俩个比较出名的非比较排序(基数排序和桶排序),由于逻辑复杂且几乎没有实践意义,在本文就没有讲解,计数排序的重点在于开辟空间的时候要使用相对位置,且回收时是用tmp下标加min来回收。

最后感谢您能阅读完此片文章,如果有任何建议或纠正欢迎在评论区留言,也可以前往我的主页看更多好文哦(点击此处跳转到主页)。
如果您认为这篇文章对您有所收获,点一个小小的赞就是我创作的巨大动力,谢谢!!!

  • 22
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,我可以为您解答这个问题。 首先,堆排序、快速排序归并排序计数排序都是常见的排序算法,它们的实现方式各不相同。下面分别介绍它们的C++实现。 1. 堆排序(Heap Sort) 堆排序是一种选择排序,它利用堆的性质进行排序。堆是一个完全二叉树,其中每个节点的值都大于或等于其子节点的值(最大堆),或者小于或等于其子节点的值(最小堆)。在堆排序中,我们将待排序数组构建成一个最大堆,然后将堆顶元素(即最大值)与数组最后一个元素交换,然后重新调整堆,重复执行此操作,直到整个数组有序。 下面是堆排序的C++实现: ```c++ void heapify(int arr[], int n, int i) { int largest = i; // 初始化最大值为根节点 int l = 2 * i + 1; // 左子节点索引 int r = 2 * i + 2; // 右子节点索引 // 如果左子节点比最大值大,则更新最大值索引 if (l < n && arr[l] > arr[largest]) largest = l; // 如果右子节点比最大值大,则更新最大值索引 if (r < n && arr[r] > arr[largest]) largest = r; // 如果最大值不是根节点,则交换根节点和最大值,然后递归调整堆 if (largest != i) { swap(arr[i], arr[largest]); heapify(arr, n, largest); } } void heapSort(int arr[], int n) { // 构建最大堆 for (int i = n / 2 - 1; i >= 0; i--) heapify(arr, n, i); // 依次取出堆顶元素,放到数组末尾,并重新调整堆 for (int i = n - 1; i >= 0; i--) { swap(arr[0], arr[i]); heapify(arr, i, 0); } } ``` 2. 快速排序(Quick Sort) 快速排序是一种分治排序,它利用分治的思想将大问题分解成小问题,然后递归地解决小问题。在快速排序中,我们选择一个枢轴元素(一般选择第一个元素),将数组分为两个部分,左边部分的元素均小于枢轴元素,右边部分的元素均大于枢轴元素。然后分别对左右两部分递归执行此操作,直到整个数组有序。 下面是快速排序的C++实现: ```c++ int partition(int arr[], int low, int high) { int pivot = arr[low]; // 枢轴元素 int i = low, j = high; while (i < j) { // 从右往左找第一个小于枢轴元素的位置 while (i < j && arr[j] >= pivot) j--; // 从左往右找第一个大于枢轴元素的位置 while (i < j && arr[i] <= pivot) i++; // 交换左右两个元素 if (i < j) swap(arr[i], arr[j]); } // 将枢轴元素放到正确的位置上 arr[low] = arr[i]; arr[i] = pivot; return i; } void quickSort(int arr[], int low, int high) { if (low < high) { int pivot = partition(arr, low, high); quickSort(arr, low, pivot - 1); quickSort(arr, pivot + 1, high); } } ``` 3. 归并排序(Merge Sort) 归并排序也是一种分治排序,它将待排序数组分成两部分,分别排序,然后合并成一个有序数组。在归并排序中,我们先将数组分为两半,分别对左半部分和右半部分递归执行归并排序,然后将两个有序子数组合并成一个有序数组。 下面是归并排序的C++实现: ```c++ void merge(int arr[], int l, int m, int r) { int n1 = m - l + 1; int n2 = r - m; // 创建临时数组 int L[n1], R[n2]; // 将左半部分复制到临时数组 L for (int i = 0; i < n1; i++) L[i] = arr[l + i]; // 将右半部分复制到临时数组 R for (int i = 0; i < n2; i++) R[i] = arr[m + 1 + i]; // 合并左右两个有序子数组 int i = 0, j = 0, k = l; while (i < n1 && j < n2) { if (L[i] <= R[j]) { arr[k] = L[i]; i++; } else { arr[k] = R[j]; j++; } k++; } // 将剩余的元素复制到数组中 while (i < n1) { arr[k] = L[i]; i++; k++; } while (j < n2) { arr[k] = R[j]; j++; k++; } } void mergeSort(int arr[], int l, int r) { if (l < r) { int m = l + (r - l) / 2; mergeSort(arr, l, m); mergeSort(arr, m + 1, r); merge(arr, l, m, r); } } ``` 4. 计数排序(Counting Sort) 计数排序是一种比较排序,它利用元素出现的次数来确定每个元素在有序数组中的位置。在计数排序中,我们先统计每个元素出现的次数,然后计算每个元素在有序数组中的位置,最后将元素放到正确的位置上。 下面是计数排序的C++实现: ```c++ void countingSort(int arr[], int n) { int max_val = 0; for (int i = 0; i < n; i++) { if (arr[i] > max_val) max_val = arr[i]; } // 创建计数数组 int count[max_val + 1] = {0}; // 统计每个元素出现的次数 for (int i = 0; i < n; i++) count[arr[i]]++; // 计算每个元素在有序数组中的位置 for (int i = 1; i <= max_val; i++) count[i] += count[i - 1]; // 将元素放到正确的位置上 int output[n]; for (int i = n - 1; i >= 0; i--) { output[count[arr[i]] - 1] = arr[i]; count[arr[i]]--; } // 将有序数组复制到原数组中 for (int i = 0; i < n; i++) arr[i] = output[i]; } ``` 以上就是堆排序、快速排序归并排序计数排序的C++实现,希望能对您有所帮助!
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值