归并排序原理(洛谷 P1908 逆序对 应用)

一.归并排序原理 

递归划分(递归的出口为子区间元素个数为1)

归并排序运用了 分治 的思想,将一个无序的数组按照中点 mid 划分成左右两个区间:

[ left , mid][ mid+1,  right ] ,直到区间内元素个数为 1 的时候,此时这个子区间一定是有序的(毕竟只有一个元素)

回溯合并(持续地将左右两个较短的有序数组合并为一个较长的有序数组,直至结束

最后再把这些区间合并,这样保证了每一步之后的子区间一定有序

如下图是归并排序的简单实现过程

 根据观察可知,形成了深度为 \log_{2}n的递归树,每次要执行n次操作完成子区间的排序,综上,该算法的时间复杂度为 n*log \, n

 二.归并排序的C++代码实现

#include <bits/stdc++.h>
using namespace std;

int nums[] = {7, 3, 2, 4, 6, 1, 5, 0};

// 递归实现的归并排序
void msort(int l, int r) {
	int mid = (l + r) / 2; // 找到中间点
	if (l >= r) return; // 如果区间内只有一个元素,直接返回
	
	msort(l, mid); // 递归排序左半部分
	msort(mid + 1, r); // 递归排序右半部分
	
	int tempSize = r - l + 1; // 计算临时数组的大小
	int temp[tempSize]; // 创建临时数组存放合并后的有序数组
	
	int i = l, j = mid + 1, t = 0; // 初始化指针
	//i 是指向左半区间的指针
	//j 是指向右半区间的指针
	//t 是指向临时数组的指针
	
	// 合并两个有序数组
	while (i <= mid && j <= r) {
		if (nums[i] <= nums[j]) {
			temp[t++] = nums[i++]; // 将较小值复制到临时数组
		} else {
			temp[t++] = nums[j++]; // 将较小值复制到临时数组
		}
	}
	
	// 复制剩余的元素(如果有的话)
	while (i <= mid) temp[t++] = nums[i++];
	while (j <= r) temp[t++] = nums[j++];
	
	// 将临时数组中的元素复制回原数组
	//相当于在nums中拿出来一段排序
	//再将排序好的那一段放回去
	for (int i = 0; i < tempSize; i++) {
		nums[l + i] = temp[i];
	}
}

int main() {
	int len = sizeof(nums) / sizeof(nums[0]); // 计算数组长度
	msort(0, len - 1); // 调用归并排序函数
	
	// 打印排序后的数组
	for (int i = 0; i < len; i++) {
		cout << nums[i] << " ";
	}
	return 0;
}

三.归并排序的原理在题目中应用(洛谷P1908 逆序对)

P1908 逆序对 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)icon-default.png?t=N7T8https://www.luogu.com.cn/problem/P1908根据归并排序的原理,我们可以发现此算法和求逆序对将有关联

因为当将这些数组在回溯合并的阶段,每一部分在合并成更长的区间前是有序的,所以可以运用这个性质进行求解。

比如这一部分,3 7和2 4都是有序的,对应的指针  i 指向左区间, j 指向右区间,满足 i < j 了,只要再nums[ i ]  > nums [ j ]即可构成一个逆序对。然后 i 指向 3,j 指向 2,此时贡献出了两个逆序对(3和2   7和2),因为3 7是有序的,较小数在前面(3),3可以与右区间构成逆序对,更大的数 7 自然也可以由此得出规律,当 i 所指向的左半边的数大于 j 所指向的右半边的数时,贡献出 (mid - i + 1 )个逆序对(mid是左区间最右侧对应下标,i 是左区间对应的当前元素的下标,+1是表示个数,消除索引偏移),完成这一部分后,将右区间中的 2 存入临时数组中,j 指针指向 4 ,然后进行下一步操作。(这便是归并排序中的回溯合并操作,逐步把最小的数字取出来再合并)

如果对应的不是nums[ i ] >nums[ j ],而是nums[ i ] <= nums[ j ],说明左区间中的最小的数和右区间的数构成不了逆序对,那么需要左区间出来一个更大的数,那么此时 i 指向的这个左区间内的较小的数应该放到临时数组中,i 指针指向左区间的下一位。(我们发现这一步也是把较小的元素存入临时数组,这正是归并排序的操作)

!!!:上述操作是在 i 没超过左区间边界 且 j 没超过右区间边界时进行的(i<=mid&&j<=r)

但是还有时候,当递归划分时,分左右区间,是将一个奇数平分,所以左右区间数不相等,对比之后可能会有剩余,或者就算左右区间数字个数相等时,也会有剩余,此时还需要对剩余的元素进行判断和操作

(依次看 i <=mid 和 j<=r是否成立,因为这个是在i<=mid&&j<=r对应的while循环之后进行的,那么它的作用便是看左右区间内是否有剩下的元素)

AC代码如下

#include <bits/stdc++.h>
using namespace std;
#define ll long long
const int Maxn = 5e5;
int n;
ll nums[Maxn+5];
ll b[Maxn+5];
ll ans = 0;
void msort(int l,int r){
	int mid = (l+r)/2;
	if(l==r) return ;
    //只有一个元素了 此时递归结束
	else{
	msort(l,mid);//递归划分左半边
	msort(mid+1,r);//递归划分右半边
	}
	int i = l ,j = mid+1 , t = l;
	//i指向左半边 j指向右半边
	//t代表临时数组的下标
	while(i<=mid && j<=r){
		if(nums[i]>nums[j]){
			ans+=mid-i+1;
			//因为是排好序再比较的,所以如果
			//nums[i]都比nums[j]大,那么左半边所有元素中
			//比nums[j]大的有 mid(左半边总个数)-i(当前元素下标)+1(索引偏移)
			b[t++] = nums[j++];
		}else{
			//b中存入较小元素
			//最后b中的元素都是有序的,再把b中元素复制到nums中
			//这样就对应上面所说的"排好序再比较"
			b[t++] = nums[i++];
		}
	}
	//当左半边或者右半边都比较完了 另一边还有剩余的
	while(i<=mid)//左半边还有剩余
	{
		b[t++] = nums[i++];
	}	
	//右半边还有剩余
	while(j<=r){
		b[t++] = nums[j++];
	}
	//排序后的结果放到nums中
	for(int i =l;i<=r;i++){
		nums[i] = b[i];
	}

	
	
}
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0);
	cout.tie(0);
	//输入
	cin>>n;
	for(int i=1;i<=n;i++){
		cin>>nums[i];
	}
	msort(1,n);
	cout<<ans;
	return 0;
}

  • 7
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值