归并排序与计数排序详解

前言

本章博客将针对递归归并排序、非递归归并排序、计数排序进行讲解,最后还会整理一下包括前两篇博客写的排序的稳定性

归并排序

如果你的快速排序基础扎实的话,那么归并排序应该对你来说不是啥太大的问题,因为他们都是分治的思想,先看一张图片

注意点:

这个在分解的过程中还是再原数组里,不要看这张图片好像是把数组里的元素分开了,其实只是和快速排序一样划分了一下有效区间,但是在划分的方式上还是有区别的

归并排序的划分是平均分,他无关中间的值是多少,他只关心划分出来的左右区间内的元素个数是否是1,如果是1说明该区间就有序了,就可以归并了

**归并逻辑:**划分出来的两个有序区间,从头开始比较,小的插入一个新的数组,最后两个区间如果都归并完了,那么再把新数组的数据拷贝回原数组

代码实现:

创建一个用来归并的数组

递归函数要判断当前的区间左右值是否有效(左区间不能大于右区间)

每次调用递归函数的时候,传入的值应该是左区间和中间值,中间值+1和左区间

如果已经符合归并条件了(最后一层递归是1 1归并,倒数第二层是 2 2归并,倒数第三层 4 4归并...以此类推)(1 1归并就是俩个有序的一个元素区间归并,2 2归并就是俩个有序的2个元素区间归并)那么就开始合并,创建一个循环,循环处理的就是俩个有序区间的排序,俩俩比较小的入新数组,最后入完再把新数组里的元素拷贝会原数组的原位置

cpp 复制代码
//归并排序
//递归部分
void _MergeSort(int* a, int * tmp , int begin, int end)
{
    //保证区间有效性
	if (begin >= end)
	{
		return;
	}
	//中间值
	int mid = begin + (end - begin) / 2;
	//每次折半递归
	_MergeSort(a, tmp, begin, mid);
	_MergeSort(a, tmp, mid + 1, end);
    //当函数执行到这里的时候说明,数组已经划分完区间了
    //分别拿到两个区间的首地址和结束地址
	int begin1 = begin, end1 = mid;
	int begin2 = mid + 1, end2 = end;
    //新数组的下标开始处
    //这里的下标和原数组保持一致,方便后面往回拷贝数据
	int index = begin;
    //如果俩个区间有一个录完了,那就结束循环
	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++];
	}
    //将新数组的数据拷贝回原数组的相应位置
    //为了保证拷贝回去的位置是原来的位置
    //end -begin + 1是两区间归并完后数组的元素个数
	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("tmp fail");
		exit(-1);
	}
	_MergeSort(a, tmp, 0, n - 1);
    free(tmp);
    tmp = NULL;
	
}

这里我在解释一下a+begin,tmp+begin,还记的tmp归并时的下标是index = begin嘛,应为我们的tmp数组是按照原数组的大小来创建的,然后我们为了保证最后在拷贝回原数组的时候可以方便一点,所以我们选择了俩个区间的元素在原数组是什么下标,在tmp里就保持一致

非递归版本的归并排序

非递归版本的归并排序思路:

可以从递归的思路下手,递归的目的就是为了筛选出最后的只有一个元素区间(因为这样这个区间就是有序的),而且递归版本是1 1归,2 2归,4 4归...所以,我们是不是也可以从1 1归开始,只要控制每次归并的区间只有一个元素就可以了,这就是1 1归,那么2 2 归就是在数组1 1 归以后,在执行,因为1 1归以后数组就是俩俩有序的

第一步还是要有一个tmp数组

第二步要有一个gap来确保1 1、2 2、3 3..归

第三步就是归并的代码

非递归就是这样的流程(其实递归也是这个流程)

代码实现:

cpp 复制代码
void MergeSortNonR(int* a, int n)//这里的n是元素个数
{
	int* tmp = (int*)malloc(sizeof(int) * (n + 1));
	if (tmp == NULL)
	{
		perror("tmp fail");
		exit(-1);
	}
    //控制数组归并步长
	int gap = 1;
    //当gap的长度大于等于数组的长度时就结束
	while (gap < n)
	{
		//归并区间
        //为什么i+=gap*2?
        //你可以套一下值看看
		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;
            //防止越界
		    if (begin2 > n)
			{
				break;
			}
			if (end2 > n)
			{
				end2 = n;
			}
            //tmp的下标
			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++];
			}
            //此时的i就是俩区间的起始下标
			memcpy(a + i, tmp + i, (end2 - i + 1) * sizeof(int));
		}
        //1 1归,结束 2 2归...
		gap *= 2;
	}
	
}

if (begin2 > n)

{

break;

}

if (end2 > n)

{

end2 = n;

}

这里再解释一下为什么要加这个条件,来防止越界,正常上面的我们8个元素是不会越界的但是9、10、11呢

1里面有begin1、end2

2里面有begin2、end2

我们可以观察到其实会越界的位置都是end1、begin2、end2这些位置

所以我在归并的循环里面加了限制条件,如果end1和begin2都大于n了那么就不需要归并了,此时就只有一个有序区间

如果是end2大于n了,那么此时还是有俩个有序区间的还是需要归并的,我们只需要把end2修正一下就可以了,end2 = n即可,这样end2就不会越界了

归并排序的时间复杂度

归并排序的代码逻辑

第一步将数据分成若干个区间,每个区间只有一个元素,保证了每个区间是有序的

第二步,在第一部的基础上,现在数组两两有序,那就两两归并

直到全部归并完毕

所以这个过程中的最大层数是logn层

然后交换的次数也是n次(这里的n是根据大O的渐进表示法得来的),所以归并排序的时间复杂度就是O(nlogn)

计数排序

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

操作步骤:

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

先创建一个新数组tmp,对于原数组的每一个元素都能在tmp数组中的下标找到

原数组第一个是6,那么在tmp的数组中下标为6的位置就++

原数组第一个是1,那么在tmp的数组中下标为1的位置就++

原数组第一个是2,那么在tmp的数组中下标为2的位置就++

原数组第一个是9,那么在tmp的数组中下标为9的位置就++

原数组第一个是4,那么在tmp的数组中下标为4的位置就++

...

直到遍历完原数组中的所有值,接着就是根据tmp数组的的每个下标的值来对原数组排序即可

tmp数组0下标是0,那么说明原数组中没有0这个元素

tmp数组1下标是2,那么说明原数组中有2个1,此时我们就对原数组的0、1下标覆盖为1

tmp数组2下标是2,那么说明原数组中有2个2,此时我对就对原数组的2、3下标覆盖为2

...以此类推,直到tmp数组访问完最后一个下标

以上这种是计数排序的绝对下标


这种写法对于数据量很大的时候就会很乏力,应为要开辟的空间很多,而且很程度上都是浪费的,就比如:101 102 103 104 105这五个数据,按照绝对下标来说的话,我们要开辟105个空间,但是前101都是没用的浪费的,为了杜绝浪费,我们一般使用相对下标来写代码

相对下标:

先遍历找出原数组中的最大值和最小值,然后用最大值减最小值+1算出原数组的数据值范围

比如,刚刚那个数据最大的是105,最小的是101,那么105 -101 + 1就是 5,那么我们只需要创建5个数据的数组就可以,101就可以存在0下标,102就存在1下标,103就存在2下标,104就存在3下标,105就存在4下标,然后往外拿的时候在加上最小值在放回原数组就可以了

代码实现:

cpp 复制代码
// 计数排序
void CountSort(int* a, int n)
{
    //先选出最大最小值
	int max = a[0];
	int min = a[0];
	for (int i = 0; i < n; i++)
	{
		if (a[i] > max)
		{
			max = a[i];
		}
		if (a[i] < min)
		{
			min = a[i];
		}
	}
    //先创建一个新数组
	int* tmp = (int*)calloc(max-min+1, sizeof(int) * (max - min + 1));
	if (tmp == NULL)
	{
		perror("tmp fail");
		exit(-1);
	}
    //先根据a数组里的数据,往新数组中记录
	int i = 0;
	while (i < n)
	{
		tmp[a[i++] - min]++;
	}
    //在根据tmp数组中的数据,往原数组覆盖数据
	int j = 0;
	for (i = 0; i < max - min + 1; i++)
	{
		
		while (tmp[i])
		{
			a[j++] = i + min;
			tmp[i]--;
		}
  	}
    free(tmp);
    tmp = NULL;
}

计数排序适合那种数据中出现大规模重复数据,且数据的大小值区间比较集中的,对于这一类数据,计数排序会有奇效

相关推荐
油泼辣子多加2 小时前
【信创】算法开发适配
人工智能·深度学习·算法·机器学习
一路往蓝-Anbo2 小时前
【第20期】延时的艺术:HAL_Delay vs vTaskDelay
c语言·数据结构·stm32·单片机·嵌入式硬件
Aaron15883 小时前
AD9084和Versal RF系列具体应用案例对比分析
嵌入式硬件·算法·fpga开发·硬件架构·硬件工程·信号处理·基带工程
laocooon5238578863 小时前
插入法排序 python
开发语言·python·算法
wuhen_n3 小时前
LeetCode -- 1:两数之和(简单)
javascript·算法·leetcode·职场和发展
林shir4 小时前
Java基础1.7-数组
java·算法
Jeremy爱编码5 小时前
leetcode课程表
算法·leetcode·职场和发展
甄心爱学习5 小时前
SVD求解最小二乘(手写推导)
线性代数·算法·svd
努力学算法的蒟蒻5 小时前
day46(12.27)——leetcode面试经典150
算法·leetcode·面试