归并排序与计数排序详解

前言

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

归并排序

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

注意点:

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

归并排序的划分是平均分,他无关中间的值是多少,他只关心划分出来的左右区间内的元素个数是否是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;
}

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

相关推荐
nlpming8 分钟前
OpenClaw 代码解析
算法
学习永无止境@12 分钟前
MATLAB中矩阵转置
算法·matlab·fpga开发·矩阵
七颗糖很甜12 分钟前
雨滴谱数据深度解析——从原始变量到科学产品的Python实现【下篇】
python·算法·pandas
nlpming12 分钟前
OpenClaw system prompt定义
算法
nlpming13 分钟前
OpenClaw安装配置及简介
算法
爱码小白13 分钟前
MySQL 常用数据类型的系统总结
数据库·python·算法
玛丽莲茼蒿20 分钟前
Leetcode hot100 【中等】括号生成
算法·leetcode·职场和发展
小欣加油23 分钟前
leetcode 128 最长连续序列
c++·算法·leetcode·职场和发展
汀、人工智能35 分钟前
[特殊字符] 第94课:删除无效的括号
数据结构·算法·数据库架构·图论·bfs·删除无效的括号
pwn蒸鱼42 分钟前
leetcode:92. 反转链表 II
算法·leetcode·链表