数据结构算法—归并排序

概念

归并排序是一种基于分治思想的高效稳定排序算法,核心思路是将待排序序列递归拆分为若干子序列,直到每个子序列只有一个元素(天然有序),再依次将有序子序列两两合并,最终得到完整有序序列

  • 核心步骤:1. 分解:递归把数组从中间分成左右两部分,直至每个子数组长度为 1;
    1. 合并:从最小有序单元开始,两两比较并按顺序合并,逐层向上归并为完整有序数组

生活中的例子

  • 多人分组整理扑克牌:先把牌分给多人各自排好,再把多组有序牌依次合并成一整副有序牌
  • 多队排队合并:两队已按身高排好的队伍,由工作人员逐个对比排头,按顺序合并成一队
  • 书籍页码整理:先把厚书拆成多叠分别按页码排好,再把多叠有序页码逐叠合并成完整顺序
  • 快递分拣:先按区域分成小堆并各自排序,再把各区域有序包裹合并成整体有序的配送清单

归并排序的思路分析

第一步:拆

想象你手里有一副乱牌:10 6 7 1 3 9 4 2,想把它从小到大理整齐,归并排序就是这么干的

先把牌堆从中间劈成两半,再把每一半继续劈成更小的堆,直到每堆只剩一张牌------ 毕竟单张牌天生就是 "排好序" 的

  • 原堆:10 6 7 1 3 9 4 2
  • 第一次劈:[10 6 7 1][3 9 4 2]
  • 继续劈:[10 6] [7 1][3 9] [4 2]
  • 最后劈到最小单位:[10] [6] [7] [1] [3] [9] [4] [2]

第二步:两两合并

现在从最小的牌堆开始,两两合并成有序小堆,就像两队排好的人,挨个比排头,把小的先拉出来:

  1. 先合并单张牌:
    • 106 → 排成 6 10
    • 71 → 排成 1 7
    • 39 → 排成 3 9
    • 42 → 排成 2 4
  2. 再合并这四个小堆:
    • 6 101 7 → 挨个比排头,排成 1 6 7 10
    • 3 92 4 → 挨个比排头,排成 2 3 4 9
  3. 最后合并两个大堆:
    • 1 6 7 102 3 4 9 → 继续比排头,最终得到 1 2 3 4 6 7 9 10

一句话总结

先把乱牌拆到 "每张都孤单",再两两凑成整齐的小堆,最后把小堆一步步拼成整副整齐的牌 ------ 这就是归并排序的核心思路

错误写法

复制代码
void _MergeSort(int* arr, int* tmp, int begin, int end)
{
	if (begin == end) 
	{
		return;
	}
	int mid = (begin + end) / 2;
	//将数组区域划分为:[begin, mid - 1] [mid, end]
	_MergeSort(arr, tmp, begin, mid - 1); 
	_MergeSort(arr, tmp, mid, end);       
 
	////归并
	int begin1 = begin;
	int end1 = mid - 1;
	int begin2 = mid;
	int end2 = end;
	int i = begin; 
 
	while (begin1 <= end1 && begin2 <= end2) 
	{
		if (arr[begin1] < arr[begin2])
		{
			tmp[i++] = arr[begin1++];
		}
		else
		{
			tmp[i++] = arr[begin2++];
		}
	}
	while (begin1 <= end1)
	{
		tmp[i++] = arr[begin1++];
	}
	while (begin2 <= end2)
	{
		tmp[i++] = arr[begin2++];
	}
	memcpy(arr + begin, tmp + begin, (end - begin + 1) * sizeof(int));
}
 
//归并排序
void MergeSort(int* arr, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n); 
	assert(tmp != NULL);
 
	_MergeSort(arr, tmp, 0, n - 1); 
	free(tmp);
	tmp = NULL;
}

这个报错就是栈溢出,为什么会出现这样的情况呢?

我们会发现栈溢出问题的出现就是因为中间值下标mid取的有问题 ,我们知道整型的除法是会取整的,所以奇数的取中是会有问题的;而且当递归只剩下两个数的时候,下标之和是一定为奇数的,这样就会导致如图所示 [mid, end] 这个区间会一直不变导致无限递归的情况,而递归一次就需要在栈区开辟新的空间,无限的递归就会导致栈溢出的问题

正确写法

复制代码
void _MergeSort(int* arr, int* tmp, int begin, int end)
{
	if (begin == end) 
	{
		return;
	}
	int mid = (begin + end) / 2+1;
	//将数组区域划分为:[begin, mid - 1] [mid, end]
	_MergeSort(arr, tmp, begin, mid - 1); 
	_MergeSort(arr, tmp, mid, end);       
 
	////归并
	int begin1 = begin;
	int end1 = mid - 1;
	int begin2 = mid;
	int end2 = end;
	int i = begin; 
 
	while (begin1 <= end1 && begin2 <= end2) 
	{
		if (arr[begin1] < arr[begin2])
		{
			tmp[i++] = arr[begin1++];
		}
		else
		{
			tmp[i++] = arr[begin2++];
		}
	}
	while (begin1 <= end1)
	{
		tmp[i++] = arr[begin1++];
	}
	while (begin2 <= end2)
	{
		tmp[i++] = arr[begin2++];
	}
	memcpy(arr + begin, tmp + begin, (end - begin + 1) * sizeof(int));
}
 
//归并排序
void MergeSort(int* arr, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n); 
	assert(tmp != NULL);
 
	_MergeSort(arr, tmp, 0, n - 1); 
	free(tmp);
	tmp = NULL;
}

现在我们来测试一下

归并排序非递归版本

概念

归并排序非递归实现(自底向上归并排序),不使用递归调用与系统栈,直接从最小有序子序列开始,通过迭代逐层合并,最终得到完整有序序列

生活例子

假设你有8本散乱的书,要按书名首字母(A-Z)排序,用归并排序非递归的思路就是:

  1. 初始gap=1(单本书为一个"有序子序列"):每本书单独放,此时每一本都是"有序"的(只有1个元素,天然有序);

  2. gap=2(合并相邻2本):把第1和第2本对比排序、第3和第4本对比排序、第5和第6本对比排序、第7和第8本对比排序,得到4组"2本有序的书";

  3. gap=4(合并相邻4本):把前4本(两组2本有序的书)合并成1组4本有序的书,后4本同理,得到2组"4本有序的书";

  4. gap=8(合并相邻8本):把两组4本有序的书合并,最终得到8本按首字母排序的完整有序书本,排序完成

所以此时我们不再把一个数组看成一个整体,由于一次归并排序是需要两个有序数组,所以我们将第一排两两为一组进行归并排序,第一排也叫做11归并;当第一排完成归并排序后就如第二排所示,此时由于一个数组此时存放两个数,则相当于4个数为一组进行归并排序,所以第二排也叫做22归并;同理下面的逻辑也是如此,则我们就能不用递归来实现归并排序。

我们会发现每完成一排的归并后数组的数据量都进行了翻倍,但是我们知道归并排序是需要两个头指针同时进行比较排序,所以这两个头指针之间的距离是会发生变化的,也就是说我们需要一个变量来控制这个距离,我们就定义为 gap 。

gap指的就是当前一排中一个数组所存放的数据个数。当 gap = 1时相当于第一排,此时每两个数组为一组进行归并排序,全部完成后则gap = 2相当于第二排,此时再每两个数组为一组进行归并排序,以此类推。

2、数组中数据个数为2^n的代码实现

为什么我们先实现个数为2^n的数组排序呢?就由上面的图所示,我们能保证每两个数组都能成为一组进行归并排序,如果数组个数不是2^n,比如假设为10个数据,则到第二排的数组个数为5个,无法做到每两个数组为一组进行归并排序

归并排序非递归代码

复制代码
void MergeSortNonR(int* a, int n)
{
	int* tmp = malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		perror("malloc");
		return;
	}
	int gap = 1;//gap每组归并的个数
	while (gap < n)
	{
		for (int i = 0; i < n; i += 2 * gap)
		{
			//[begin1,end1][begin2,end2]
			int begin1 = i, end1 = i + gap - 1;
			int begin2 = i + gap, end2 = i + 2 * gap - 1;
			if (end1 >= n)
			{
				end1 = n - 1;
			}
			if (begin2 >= n)
			{
				break;
			}
			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 小时前
机器学习线性回归算法是入门机器学习理解人工智能模型很好示例
人工智能·算法·机器学习
Trouvaille ~1 小时前
【贪心算法】专题(三):排序、博弈与区间的贪婪法则
c++·算法·leetcode·青少年编程·面试·贪心算法·蓝桥杯
Sakinol#2 小时前
Leetcode Hot 100 —— 二叉树 part02
算法·leetcode
N1_WEB2 小时前
HDU:杭电 2017 复试真题汇总
算法
努力学算法的蒟蒻2 小时前
day111(3.13)——leetcode面试经典150
算法·leetcode·面试
爱学习的小囧2 小时前
VCF 9.0 操作对象与指标报告自动化教程
运维·服务器·算法·自动化·vmware·虚拟化
嫂子开门我是_我哥2 小时前
心电域泛化研究从0入门系列 | 第四篇:域泛化核心理论与主流方法——破解心电AI跨域失效难题
人工智能·算法·机器学习
Olivia_su2 小时前
数据分析及可视化Tableau自学入门
算法·数据分析·tableau
亓才孓2 小时前
【Stream】讲常见数据结构转为map<String,Long>
数据结构·windows·python