前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
社区首页 >专栏 >【初探数据结构】归并排序与计数排序的序曲

【初探数据结构】归并排序与计数排序的序曲

作者头像
我想吃余
发布于 2025-03-31 10:22:08
发布于 2025-03-31 10:22:08
9300
代码可运行
举报
运行总次数:0
代码可运行

前言

本文主要内容是归并的递归和非递归以及计数排序的实现方法。文章会提及很多容易忽视的易错点(大多是我自己踩过的坑😂),这是我在学习这块内容时获取的教训和宝贵经验。

因为自己淋过雨,希望能为你们撑把伞!共勉!😁


一、归并排序(Merge Sort)

1. 算法原理

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

归并排序核心步骤:

  1. 分解:将数组递归分成两半,直到子数组长度为1。
  2. 合并:将两个有序子数组合并为一个有序数组。

从步骤可以看出,这似乎就是二叉树的后序遍历

不知道你们在学习C语言的时候有没有写过这样一道题 合并两个有序数组 我建议没写过的可以去写一下,这里可以直接抄作业了,O(∩_∩)O哈哈~

2. 递归实现
  1. 首先我们需要开辟一个临时数组,来存储合并的序列
  2. 递归结束条件:数组长度为1时结束 if (left >= right) return;
  3. mid不要写成(right - left) / 2了,再加上left才能在正确位置(因为这是在计算下标),或者直接用(right+left)/2。——易错点
  4. 确定每次拆分的区间[left,mid] [mid + 1,right]
  5. 递归(后序遍历),
  6. 归并:在tmp上正确的位置进行赋值,不能是cur = 0,否则会覆盖值——易错点
  7. 拷贝,将tmp拷贝到a
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
void Merger(int* a, int* tmp, int left, int right)
{
	//递归结束条件
	if (left >= right) {
		return;
	}
	int mid = left + (right - left) / 2;//易错:加上left才能在正确的位置
	//递归(后序遍历)
	//[left,mid] [mid+1,right]
	Merger(a, tmp, left, mid);
	Merger(a, tmp, mid+1, right);
	//归并
 	int begin1 = left, end1 = mid;
	int begin2 = mid + 1, end2 = right;
	int cur = left;//易错:在tmp上正确的位置进行赋值,不能是cur = 0,否则会覆盖值
	while (begin1 <= end1 && begin2 <= end2) {
		if (a[begin1] > a[begin2]) {
			tmp[cur++] = a[begin2++];
		}
		else {
			tmp[cur++] = a[begin1++];
		}
	}
	while (begin1 <= end1) {
		tmp[cur++] = a[begin1++];
	}
	while (begin2 <= end2) {
		tmp[cur++] = a[begin2++];
	}
	//将tmp拷贝到a
	memcpy(a + left, tmp + left, sizeof(int) * (right - left + 1));
}

void MergerSort(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	int begin = 0, end = n - 1;
	Merger(a, tmp, begin, end);
	free(tmp);
}

特点

  • 时间复杂度:O(n log n)
  • 空间复杂度:O(n)
  • 稳定排序,适合处理大数据量。

3. 非递归实现

利用迭代(循环)模拟递归过程:

当元素个数不是gap的整数时,会发生越界问题: 设归并的两部分分别为[begin1,end1][begin2,end2] 那么,end1、begin2、end2都可能会越界,因此我们就需要修正边界。

  1. end1越界时,第一部分不完整且第二部分不存在,没必要归并了,直接拷贝即可
  2. begin2越界时,第二部分不存在,没必要归并了,直接拷贝即可
  3. end2越界时,第二部分不完整,将end2修正到数组末尾即可

可以发现,1,2是一类情况,可以一起处理了。

我们需要来抉择一个问题:

  1. 整体归并结束后拷贝
  2. 归并一部分拷贝一部分

这两个问题看似不起眼,实则不然,它会影响你控制边界的难度。可以试试两种方式都写写,会特别爽(狗头)

  1. 归并结束后再拷贝,需精密控制边界越界情况,容易出错。——不推荐该写法
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
void MergerSortNonR1(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) {
			//归并
			//[i,i+gap-1] [i+gap,i+2*gap-1]
			int begin1 = i, end1 = i + gap - 1;
			int begin2 = i + gap, end2 = i + 2 * gap - 1;
			int cur = i;
			if (end1 >= n) {
				//修正end1
				end1 = n - 1;
				//使得begin2 > end2,终止归并
				begin2 = n;
				end2 = n - 1;
			}
			else if (begin2 >= n) {
				//使得begin2 > end2,终止归并
				begin2 = n;
				end2 = n - 1;
			}
			else if (end2 >= n) {
				//修正end2,继续归并
				end2 = n - 1;
			}
			while (begin1 <= end1 && begin2 <= end2) {
				if (a[begin1] > a[begin2]) {
					tmp[cur++] = a[begin2++];
				}
				else {
					tmp[cur++] = a[begin1++];
				}
			}
			while (begin1 <= end1) {
				tmp[cur++] = a[begin1++];
			}
			while (begin2 <= end2) {
				tmp[cur++] = a[begin2++];
			}
		}
		//归并结束后再打印
		memcpy(a, tmp, sizeof(int) * n);
		gap *= 2;
	}
	free(tmp);
}

  1. 归并一次拷贝一次,若end1begin2有越界情况,直接跳出循环(退出归并)即可无需控制边界情况,操作简单易理解
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
void MergerSortNonR(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) {
			//归并
			//[i,i+gap-1] [i+gap,i+2*gap-1]
			int begin1 = i, end1 = i + gap - 1;
			int begin2 = i + gap, end2 = i + 2 * gap - 1;
			int cur = i;
			if (end1 >= n || begin2 >= n) {
				break;//直接终止归并
			}
			else if (end2 >= n) {
				//修正end2
				end2 = n - 1;
			}
			while (begin1 <= end1 && begin2 <= end2) {
				if (a[begin1] > a[begin2]) {
					tmp[cur++] = a[begin2++];
				}
				else {
					tmp[cur++] = a[begin1++];
				}
			}
			while (begin1 <= end1) {
				tmp[cur++] = a[begin1++];
			}
			while (begin2 <= end2) {
				tmp[cur++] = a[begin2++];
			}
			//归并一次打印一次
			memcpy(a + i, tmp + i, sizeof(int) * (end2 - i + 1));
		}
		//printf("\n");
		gap *= 2;
	}
	free(tmp);
}

关键点

  • gap 控制合并步长,从1开始逐步扩大。
  • 边界处理:若子数组越界,直接终止合并。

二、计数排序(Count Sort)

思想:计数排序又称为鸽巢原理,是对哈希直接定址法的变形应用。 操作步骤:

  1. 统计相同元素出现次数
  2. 根据统计的结果将序列回收到原来的序列中
1. 算法原理
  1. 确定范围:找出数组的最小值 min 和最大值 max
  2. 统计频率:创建计数数组 countA,统计每个元素出现的次数。
  3. 重建数组:根据计数数组将元素按顺序写回原数组。
2. 代码实现
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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* countA = (int*)calloc(range, sizeof(int));  // 分配计数数组
    for (int i = 0; i < n; i++) countA[a[i] - min]++; // 统计频率
    // 重建数组
    int cur = 0;
    for (int i = 0; i < range; i++) {
        while (countA[i]--) a[cur++] = i + min;
    }
    free(countA);
}

特点

  • 时间复杂度:O(n + k),k 为数据范围。
  • 空间复杂度:O(k)
  • 非比较排序,适合数据范围小且为整数的情况。

三、对比与总结

算法

时间复杂度

空间复杂度

稳定性

适用场景

归并排序

O(n log n)

O(n)

稳定

大数据量、需稳定排序

计数排序

O(n + k)

O(k)

稳定

小范围整数、非比较排序

希望这篇文章对你有所帮助🌹🌹🌹

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2025-03-27,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
【数据结构】排序算法篇二
任取待排序元素序列中的某元素作为基准值key(把它直接排在它最终要排的那个位置),按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。
六点半就起.
2024/10/16
840
【数据结构】排序算法篇二
【数据结构】排序算法——Lesson2
本文将继续介绍两种高效的排序算法——归并排序、计算排序。 归并排序在一些场合下(如外部排序)非常有效,当数据量非常大且无法全部加载到内存时,可以将其分块处理。 而计数排序是一种非比较排序算法,适用于特定范围内的整数排序,在许多数情况下计算排序可以秒杀我们介绍过的所有排序。
_小羊_
2024/10/16
1130
【数据结构】排序算法——Lesson2
【C语言/数据结构】排序(归并排序|计数排序|排序算法复杂度)
计数排序需要我们新创建一个统计数组,按理来说,数组下标就可以用来当作统计的数,该位置就来存放该数出现的次数。但是,如果要排序的数是从一千多开始的,这样前面的空间就全部浪费了。所以我们采用相对映射的方法。即用待排序的数中,最大的数-最小的数+1就可以得到范围,从而减少空间浪费。接着用原数组的数减去最小值,将该值作为count数组的下标,即相对映射。最后进行排序,记得加回最小值min,这样数据才不会被改变。
秦jh
2024/01/31
1520
【C语言/数据结构】排序(归并排序|计数排序|排序算法复杂度)
(超清晰)数据结构--排序实现--C语言
特性总结: 2. 希尔排序是对直接插入排序的优化。 3. 当gap > 1时都是预排序,目的是让数组更接近于有序。当gap == 1时,数组已经接近有序的了,这样就会很快。这样整体而言,可以达到优化的效果。我们实现后可以进行性能测试的对比。 4. 希尔排序的时间复杂度不好计算,因为gap的取值方法很多,导致很难去计算,因此在好些树中给出的希尔排序的时间复杂度都不固定。 5. 稳定性:不稳定。
小志biubiu
2025/02/27
810
(超清晰)数据结构--排序实现--C语言
【初阶数据结构】归并排序 - 分而治之的排序魔法
本文讲解的排序算法是归并排序,作为归并算法,其有着快速排序算法没有的特性,也是面试比较常考的算法之一。本文会重点讲解思路以及代码的实现。
埋头编程
2024/10/18
820
【初阶数据结构】归并排序 - 分而治之的排序魔法
【数据结构】排序算法系列——归并排序(附源码+图解)
归并排序从字面上来看,它的大致核心应与归并有关——归并拆分开来,变成归类和合并,归类则是将数组进行有序化,合并则是将两个有序的数组进行合并变成一个有序的数组。
Skrrapper
2024/09/25
2040
【数据结构】排序算法系列——归并排序(附源码+图解)
【初阶数据结构与算法】——手撕八大经典排序算法
那如果前面的元素都比要插入的数据大呢? 那就一直比,直到比完第一个元素,然后end- -之后变成-1,还是放到end位置的后面,即让它成为新的第一个元素。
YIN_尹
2024/01/23
3340
【初阶数据结构与算法】——手撕八大经典排序算法
【数据结构与算法】归并排序:从理论到实践的深度剖析
归并排序的非递归实现(也称为迭代实现)主要依赖于循环而非递归调用来分解和合并数组。与递归实现相比,非递归实现避免了递归调用栈的开销,但需要更复杂的索引和边界处理。
倔强的石头
2024/12/06
2210
【数据结构与算法】归并排序:从理论到实践的深度剖析
数据结构(C语言)之对归并排序的介绍与理解
首先,归并排序可以理解为用分治策略的一种排序算法,这里可以用递归的思想去理解,对一个数组进行不断分割,每次分为两个子数组,直到最后剩下的是一个数据也就是不可再分割,那么就开始对末两个子数组进行归并,然后归回去,在原数组得到有序的数组。(也就是说再每次归并的两个数组一定要是有序的)。
羑悻的小杀马特.
2025/01/23
560
数据结构(C语言)之对归并排序的介绍与理解
【初阶数据结构】星河中的光影 “排” 象:排序(下)
接上一篇,解决掉剩余的排序方法,本篇有些许难度,建议巩固好上一篇再来进行本篇的学习
DARLING Zero two
2025/02/26
550
【初阶数据结构】星河中的光影 “排” 象:排序(下)
归并排序含非递归版
至此,归并排序的递归版和非递归版讲解完成,感谢各位友友的来访,祝各位友友前程似锦O(∩_∩)O
大海里的番茄
2024/01/19
1700
归并排序含非递归版
【初阶数据结构篇】归并排序、计数排序
归并排序(MERGE-SORT)是建⽴在归并操作上的⼀种有效的排序算法,该算法是采⽤分治法(Divide andConquer)的⼀个⾮常典型的应⽤。将已有序的⼦序列合并,得到完全有序的序列;即先使每个 ⼦序列有序,再使⼦序列段间有序。若将两个有序表合并成⼀个有序表,称为⼆路归并。归并排序核 ⼼步骤:
熬夜学编程的小王
2024/11/20
690
【初阶数据结构篇】归并排序、计数排序
【初阶数据结构篇】归并排序和计数排序(总结篇)
gitee 前篇:【初阶数据结构篇】插入、希尔、选择、堆排序介绍 中篇:【初阶数据结构篇】冒泡排序和快速排序
半截诗
2024/10/09
870
【初阶数据结构篇】归并排序和计数排序(总结篇)
八大排序(二)堆排序,快速排序,归并排序,计数排序
堆排序其实就是利用堆的第二个特点:任一结点的值都是其子树所有结点的最大值或最小值。
小灵蛇
2024/06/06
1100
八大排序(二)堆排序,快速排序,归并排序,计数排序
【数据结构】八大排序之归并排序算法
表示不小于x的最小整数)个长度为2或1的有序子序列;再两两归并,......,如此重复,直至得到一个长度为n的有序序列为止,这种排序方法称为2路归并排序.
修修修也
2024/04/01
1420
【数据结构】八大排序之归并排序算法
归并排序深度剖析
归并排序是建立在归并操作上的一种有效,稳定 的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。 ————百度百科
用户11029129
2024/06/04
1300
归并排序深度剖析
数据结构——lesson12排序之归并排序
前面我们学习过六种排序——直接插入排序、希尔排序、直接选择排序、堆排序、冒泡排序和快速排序,今天我们就来学习归并排序🥳🎉🎉🎉
大耳朵土土垚
2024/04/04
1300
数据结构——lesson12排序之归并排序
归并排序详解
可以看到这种结构很像一棵完全二叉树,分阶段可以理解为就是递归拆分子序列的过程,递归深度为log2n。
P_M_P
2024/01/20
1110
归并排序详解
《数据结构》八大排序算法 必读!
本文将介绍常见八大排序,包括直接插入排序、希尔排序、选择排序、堆排序、冒泡排序、快速排序、归并排序以及计数排序(计数排序和桶排序面试基本不涉及,本文将简要介绍),本内容是重点中的重点,请务必全部掌握!
码神联盟
2021/10/27
1.2K0
《数据结构》八大排序算法 必读!
【数据结构】排序之归并排序与计数排序
在前面的文章中介绍了 插入排序和交换排序,今天来分享的是归并排序和计数排序。 话不多说,正文开始。
zxctscl
2024/01/23
1400
【数据结构】排序之归并排序与计数排序
推荐阅读
相关推荐
【数据结构】排序算法篇二
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验