归并排序详解以及其洛谷p1908逆序对题解

文章目录

    • 一.题目描述
      • 输入格式
      • 输出格式
      • 样例 #1
      • 样例输入 #1
      • 样例输出 #1
      • 提示
    • 二.解题思路
    • 三.归并排序
      • (1)分解
      • (2)合并
      • (3)将临时数组中已经排序后的部分覆盖数组的对应部分
    • 四.在该题中使用归并排序解题
    • 五.题解代码:
    • 总结

一.题目描述

  猫猫 TOM 和小老鼠 JERRY 最近又较量上了,但是毕竟都是成年人,他们已经不喜欢再玩那种你追我赶的游戏,现在他们喜欢玩统计。

  最近,TOM 老猫查阅到一个人类称之为“逆序对”的东西,这东西是这样定义的:对于给定的一段正整数序列,逆序对就是序列中 a i > a j a_i>a_j ai>aj i < j ii<j 的有序对。知道这概念后,他们就比赛谁先算出给定的一段正整数序列中逆序对的数目。注意序列中可能有重复数字。

Update:数据已加强。

输入格式

第一行,一个数 n n n,表示序列中有 n n n个数。

第二行 n n n 个数,表示给定的序列。序列中每个数字不超过 1 0 9 10^9 109

输出格式

输出序列中逆序对的数目。

样例 #1

样例输入 #1

6
5 4 2 6 3 1

样例输出 #1

11

提示

对于 25 % 25\% 25% 的数据, n ≤ 2500 n \leq 2500 n2500

对于 50 % 50\% 50% 的数据, n ≤ 4 × 1 0 4 n \leq 4 \times 10^4 n4×104

对于所有数据, n ≤ 5 × 1 0 5 n \leq 5 \times 10^5 n5×105

请使用较快的输入输出

应该不会 O ( n 2 ) O(n^2) O(n2) 过 50 万吧

题目链接:
洛谷 - 逆序对

二.解题思路

 首先,如果我们并没有了解过归并排序,还算是个算法小白的话,我们可能想到冒泡排序这样 O ( n 2 ) O(n^2) O(n2)的方法,通过判断前面一个数是否比后面的数大,如果前面的数更大,就是一对逆序对,这样将其排序下来就可以得到最终的答案,但是这种方法只能过小范围的数据,如果数据量太大,那么 O ( n 2 ) O(n^2) O(n2)的算法是绝对会超出时间限制的,所以我们需要用到归并排序的思想。

三.归并排序

 归并排序是建立在归并操作上的一种有效,稳定的排序算法,该算法是采用分治法的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
时间复杂度: O ( n l o g 2 n ) O(n log_2n) O(nlog2n)
空间复杂度: O ( n ) O(n) O(n)

 我们如何能做到更好的理解归并排序呢?这里先引入另一种题目,如何合并两个有序数组?
例如:假设有两个数组arr1[3] = {1,4,6}, arr2[3] = {2,3,5} ,要求将两个数组合并成一个新的有序数组。

 首先,由于两个数组都是有序的,那么我们就可以用两个索引分别指向两个数组的首段,然后从左往右遍历,将两个索引指向的值中的较小值放入新创建的数组中,然后被使用的那个索引指向该数组的下一项继续进行比较,直到把某一个数组中的值全部遍历完,再把另一个数组中的剩下值按顺序放入数组中即可,如下图所示:
归并排序详解以及其洛谷p1908逆序对题解_第1张图片
 这就是合并两个有序数组的思路了,那么有了这个基础后,我们就够更好的理解归并排序了。
归并排序详解以及其洛谷p1908逆序对题解_第2张图片

首先,归并排序其实是分为三部分,分解大数组,合并,以及数据搬迁。

(1)分解

 运用二分的思想,将一个大数组不断地分解直到每个需要处理的部分只含有一个元素(只含有一个元素的部分一定有序),也就是上图中的分解部分,这里运用了递归的思想,代码如下:
arr:需要进行排序的数组
tmp:中间变量数组(用于暂时存放已经合并的数组)
begin:需要处理部分的开始索引
end:需要处理部分的结束索引

void merger_sort(int * arr,int * tmp,int begin,int end)
{
	if(begin==end)//表示数组中仅有一个元素
		return;
	int mid = (left+right)/2;
	merger_sort(arr,tmp,begin,mid);//处理前半段
	merger_sort(arr,tmp,mid+1,end);//处理后半段

(2)合并

 当我们已经将数组分解到最小时,就需要一步步向上合并了,上面讲了如何合并有序数组,那么其实归并排序在合并过程就可以看作合并数组的两个有序部分,同样是通过双索引的方式,我们先把排完序的结果按照对应索引的位置放在临时使用的数组中,避免出现覆盖导致数据丢失的现象。合并的方法和合并两个有序数组类似,所以就不再详细描述了,直接上代码!

	int i = begin;//待合并数组的前半部分的索引
	int j = mid + 1;//待合并数组后半部分索引
	int t = begin;//合并完之后将结果存放在临时数组中,临时数组的索引
	while (i <= mid && j <= end)
	//比较两索引指向部分的大小,并将排序之后的结果放到临时数组里面
	//直到有一部分数据被遍历完结束该过程
	{
		if (arr[i] > arr[j])
			tmp[t++] = arr[j++];
		else
			tmp[t++] = arr[i++];
	}
	//将剩下一部分的所有数都放进临时数组中
	while (i <= mid)
		tmp[t++] = arr[i++];
	while (j <= end)
		tmp[t++] = arr[j++];

(3)将临时数组中已经排序后的部分覆盖数组的对应部分

由于排序后的部分时放在临时数组中的,所以要重新将其放进数组中。

	for (int s = begin; s <= end; s++)
		arr[s] = tmp[s];

以上就是归并排序的三步骤啦!接下来附上完整代码:

#define _CRT_SECURE_NO_WARNINGS 1
#include
#include

void merger_sort(int begin, int end,int* arr,int * tmp)
{
	if (begin >= end)
		return;
	int mid = (begin + end) / 2;
	find_reverse_pair(begin, mid, arr,tmp);
	find_reverse_pair(mid + 1, end,arr, tmp);
	int i = begin;
	int j = mid + 1;
	int t = begin;
	while (i <= mid && j <= end)
	{
		if (arr[i] > arr[j])
		{
			tmp[t++] = arr[j++];
		}
		else
			tmp[t++] = arr[i++];
	}
	while (i <= mid)
		tmp[t++] = arr[i++];
	while (j <= end)
		tmp[t++] = arr[j++];
	for (int s = begin; s <= end; s++)
		arr[s] = tmp[s];
}

到这里,归并排序应该是讲的很明白了(不知道哪来的自信哈哈!),如果写的有什么问题或者有疑惑欢迎评论区提出。

四.在该题中使用归并排序解题

 由于在归并排序的过程中每次划分后合并时左右子区间都是从小到大排好序的,并且虽然左右区间中元素的位置有变化,但是都是在内部变化,左区间的元素不会跑到右区间(也就是左区间的元素在原数组中的下标一定小于右数组中元素的下标),我们只需要统计右边区间每一个数分别会与左边区间产生多少逆序对即可(也就是有几个左区间元素比右区间的元素大),那如何计数呢?

 我们发现,在归并排序的合并阶段不就是有一个我比较左右数组索引值的过程吗,我们又有一个前提条件就是数组左右区间有序,那么当判断到数组左区间索引对应的值更大是,整个左区间在这个索引以及之后的所有数都能与右区间当前索引的组成一组逆序对,那么逆序对的数量就是mid - i + 1(由上文知mid是左区间的最后一个元素),这里引用洛谷社区一位大佬举的例子便于理解(自己懒得码字了(doge))

在某个时候,左区间:  5 6 7  下标为i
           右区间:  1 2 9  下标为j
          
这个时候我们进行合并:
step 1:由于 5>1,所以产生了逆序对,这里,我们发现,左区间所有还没有被合并的数
都比 1 大,所以1与左区间所有元素共产生了 3 个逆序对(即tot_numleft-i+1),
统计答案并合并 1 
step 2:由于 5>2,由上产生了3对逆序对,统计答案并合并 2
step 3:由于 5<9, 没有逆序对产生,右区间下标 j++
step 4:由于 6<9, 没有逆序对产生,右区间下标 j++
step 5:由于 7<9, 没有逆序对产生,右区间下标 j++
step 6:由于右区间已经结束,正常执行合并左区间剩余,结束

PS: tot_numleft=3,即左区间总元素个数

 那么,我们就可以对归并排序过程稍加改进就可以得到本题答案,也就是在合并过程中增加一部计数即可。

		if (arr[i] > arr[j])
		{
		//这里的ans是存放答案的地址
			(*ans)+=mid - i + 1;
			tmp[t++] = arr[j++];
		}

五.题解代码:

当然,还有最后一些细节要注意:

  • 这题的数据大小是n<=500000,如果直接在栈中创建数组,那一定会爆栈,有的人喜欢用全局变量定义这种大数组,但是建议不要养成习惯,或许这样做算法题会方便很多,但是使用全局变量在大工程中非常危险,因为一不小心就会错误的修改全局变量中的值,所以这里推荐使用动态内存分配的代码,也就是使用malloc或者calloc分配内存,并且记住在程序结束后要记得free()释放分配的内存,养成好习惯!
  • 由于数据的范围很大,所以答案要存在long long类型的变量中,放在int类型变量会数据溢出。
#include
#include

void find_reverse_pair(int begin, int end,int* arr,int * tmp, long long* ans)
{
	if (begin >= end)
		return;
	int mid = (begin + end) / 2;
	find_reverse_pair(begin, mid, arr,tmp, ans);
	find_reverse_pair(mid + 1, end,arr, tmp, ans);
	int i = begin;
	int j = mid + 1;
	int t = begin;
	while (i <= mid && j <= end)
	{
		if (arr[i] > arr[j])
		{
			(*ans)+=mid - i + 1;
			tmp[t++] = arr[j++];
		}
		else
			tmp[t++] = arr[i++];
	}
	while (i <= mid)
		tmp[t++] = arr[i++];
	while (j <= end)
		tmp[t++] = arr[j++];
	for (int s = begin; s <= end; s++)
		arr[s] = tmp[s];
}

int main()
{
    int * tmp = malloc(500000*sizeof(int));
    int * arr = malloc(500000*sizeof(int));
	int n = 0;
	scanf("%d", &n);
	for (int i = 0; i < n; i++)
		scanf("%d", &arr[i]);
	long long ans = 0;
	find_reverse_pair(0,n-1, arr, tmp,&ans);
	printf("%lld", ans);
    free(tmp);
    free(arr);
	return 0;
}

归并排序详解以及其洛谷p1908逆序对题解_第3张图片

总结

 以上就是对归并排序的深入了解以及使用归并排序后的解法了,大家在理解完整个代码后一定要自己再写一遍整理思路,看还有没有什么疑惑的地方进行查缺补漏,觉得有帮助的记得点赞哦哈哈!如果有什么错误的地方还请评论区提出。
归并排序详解以及其洛谷p1908逆序对题解_第4张图片

你可能感兴趣的:(C语言,算法,算法,排序算法,c语言)