如何让小白写出归并排序求逆序对

博主分享归并排序求逆序对的算法思想。先通过程序分析递归函数调用前后代码对结果的影响,接着分析二路归并排序问题,给出O(n)复杂度方案。然后探讨逆序对问题,利用二路归并统计逆序对个数,采用递归缩小问题规模,最终实现排序并统计逆序对数量。

昨日和小白们分享了一个归并排序求逆序对的算法思想,对于此算法的学习,一般小白学不明白。包括曾经的小白的我,理解这个算法可是费了老大劲了。那么我希望我的小白们能学起来轻松且水到渠成些。所以,在此记录下,供以后用。(当然以后还用得到么,我也不清楚,据说自招的改革会让很多二三流学校的孩子们学不下去的,以后我可能退役了)。
  【学情分析】已具备对递归函数的执行过程的理解,而且对分析递归能解决的问题,基本会从两个方面去思考如何写递归:递归边界和递归式。
  【分享过程】
  1.写了一个简短的程序分析递归函数调用之前的代码和递归之后写的代码对程序结果的影响。
程序一:

#include<bits/stdc++.h>
using namespace std;
void fun(int n){
	if(n <= 0) return;
	cout << n << endl;
	fun(n-1);
}
int main(){
	fun(5);
	reuturn 0;
}

程序二

#include<bits/stdc++.h>
using namespace std;
void fun(int n){
	if(n <= 0) return;	
	fun(n-1);
	cout << n << endl;
}
int main(){
	fun(5);
	reuturn 0;
}

程序一的输出是在每次递归调用下一个函数之前输出,是在进入一次新的递归前做的事。
  程序二的输出是在每次递归调用回来之后再进行输出,是在递归回来后做的事。
  从以上两个程序帮助理解递归过程中要做的事的先后在程序表达时该放置的位置。
2.尝试分析解决问题二路归并排序问题。
  给定两个非降的数字序列,要求合并后依然是非降的数字序列。其中每个序列的个数是3000000的样子,每个数值在-109~109之间,从这个数据量来看,将数据加入到一个数组中,在sort一遍,nlogn的时间复杂度会比较高,面临超时的危险;根据每个数值的范围来看,桶排序不现实。那么需要尝试O(n)的时间复杂度的方案。
抓住给定的两个有序序列的有序这个关键点,我们可以二路归并来解决。在这里插入图片描述给予大约40分钟左右的时间写出代码并AC

#include<bits/stdc++.h>
using namespace std;
const int MAXN = 3000000+1;
int a[MAXN],b[MAXN],c[MAXN*2];
int main(){
	int n,m;
	cin >> n >> m;
	for(int i = 1;i<= n; i++)scanf("%d",&a[i]);
	for(int i = 1;i<= m; i++)scanf("%d",&b[i]);
	for(int k= 1,i = 1,j = 1;k<= n + m; k++){
		if((a[i] <= b[j]&& i<= n)|| j > m ){
			c[k] = a[i];
			i++;
		}else{
			c[k] = b[j];
			j++;
		}
		printf("%d ",c[k]);
	}
	return 0;
}

3.分析逆序对问题。
  在二路归并中,假设两个序列是并列一排放着,a数组在前,b数组在后在这样的序列中存在多少个逆序对呢?分析当放b[j]时,是因为a[i]比它大,才会轮到b[j]放入c[k]中的,那么目前a数组中从i ~n为下标的数就都是大于目前b[j]的,因此可以与其产生逆序对n-i+1对。依次下去,对于每个b[j]放入c数组时,都去统计目前能产生的逆序对个数,这样累计下来便是这个前后拼接后的数组所存在的逆序对的个数,同时也二路归并为有序的状态了。
  但是不是每个数组给定时能左右两部分有序,因此也就不能如此操作。那么如何能创造条件让其有序并满足此条件,并进行逆序对计算呢?
  假设需要求逆序对的数据是a[1]至a[n],那么需要求解决a[1]~a[(1+n)/2]序列的有序状态和逆序对问题,及a[(1+n)/2]至a[n]序列的有序状态和逆序对问题。我们会发现在这个过程中,问题没有变化,但是问题的规模在缩小,对于这类问题,我们可以采用递归来写。当递归到只有一个数时就停止继续分,当左右只有一个数时便可以看做两个有序序列在合并了,因此在分完回来,就开始对目前的序列的两部分进行二路归并,并顺带统计逆序对个数。因此递归式是:
在这里插入图片描述
然后用一组数模拟分和合的过程。
在这里插入图片描述  其中蓝色笔记为在递归分的过程,红色笔记处为递归回来在做的并的过程。仔细观察为了原来的数组有序,在二路归并时有序的内容是被归并到一个辅助数组中的,因此在归并完后,需要再将辅助数组中的有序数值覆盖回原来数组对应的空间内。
  详细见代码:

void mergesort(int left,int right){
	if(left == right) return;
	int mid = (left + right) / 2;
	mergesort(left, mid);    //分
	mergesort(mid+1 , right);//分
	// 二路归并
	int i = left,j = mid+1;
	for(int k = left ;k <= right; k++){
		if((a[i] <= a[j] && i<=mid) || j>right){
			tmp[k] = a[i];
			i++;
		} 
		else{
			ans += mid - i + 1;  //统计逆序对的个数。
			tmp[k] = a[j];
			j++;
		}
	} 
	for(int k = left; k <= right; k++){   //将辅助数组中的有序数值放回原始数组a中。
		a[k] = tmp[k];
	}
}

这样,就既排好了序,同时也统计了逆序对的数量。

<think>嗯,用户让我介绍一下归并排序逆序对的代码。首先,我需要回忆一下归并排序的基本原理,以及逆序对的定义。归并排序是分治算法,把数组分成两半,分别排序后再合并。而逆序对指的是数组中前面的元素大于后面的元素的情况。例如,在数组[3,1,2]中,逆序对有(3,1)和(3,2),所以总共有2个。 那归并排序过程中如何计算逆序对呢?在合并两个已排序的子数组时,如果左边的元素大于右边的元素,那么左边该元素及其后面的元素都会与右边的这个元素构成逆序对。例如,合并时左边是[3,4],右边是[1,2],当比较3和1时,3>1,所以逆序对的数量应该是左边剩余元素的数量(这里左边此时有2个元素),所以逆序对增加2。这一步是关键,需要正确统计这个数量。 接下来,我需要考虑如何修改归并排序的代码来统计逆序对的数量。通常归并排序的合并过程需要临时数组,然后在合并时进行比较。这时候,当右边的元素被选中放入临时数组时,说明左边的当前元素比右边的大,这时候逆序对的数量需要加上左边剩余元素的个数,也就是mid - i + 1,其中i是左边子数组的当前指针,mid是左边子数组的末尾。 可能的代码结构应该是这样的:主函数调用一个递归函数,该函数返回逆序对的数量。递归函数先分割数组到单个元素,然后合并并统计逆序对的数量。合并过程中,每次从左右子数组中取较小的元素,当取右子数组的元素时,累加逆序对的数量。 需要注意的点包括:数组的索引是否正确,例如mid的计算,以及临时数组的处理。另外,逆序对的累加应该在合并过程中正确计算。例如,当左边元素大于右边元素时,逆序对的数量增加左边剩余元素的数目。 举个例子,比如数组是[5,3,2,4,1]。用归并排序分割到最小单位后,合并时计算逆序对。比如在合并[5]和[3]的时候,5>3,所以逆序对加1。合并后的数组是[3,5],然后再合并更大的部分,每一步都要统计这些情况。 可能还要考虑大数情况下的性能,因为如果数组很大,递归可能导致栈溢出,但通常归并排序在处理大规模数据时还是可以的,因为时间复杂度是O(n log n),而逆序对的计算不会增加复杂度,只是常数时间上的增加。 另外,边界条件需要注意,比如数组长度为0或1时,逆序对数量为0。测试用例方面,可以测试已知的情况,比如数组倒序排列时逆序对数量最大,为n(n-1)/2。例如,数组[4,3,2,1]的逆序对数是6。 在代码实现时,可能需要注意数组的索引是否正确,比如在分割时左半部分是left到mid,右半部分是mid+1到right。合并过程中,i指向左半部分的起始,j指向右半部分的起始,当nums[i] > nums[j]时,逆序对数目加上mid - i + 1。因为此时左半部分从i到mid的所有元素都会比nums[j]大,所以贡献mid - i +1个逆序对。 所以,总结下来,归并排序逆序对的代码结构大致如下: 定义一个全局或传递的计数器,或者在递归过程中返回逆序对数目。合并函数返回本次合并中的逆序对数目,然后递归地将左右子数组的数目相加,再加上合并过程中的数目。 具体的代码实现可能需要使用辅助数组来存放合并后的结果,同时在合并过程中统计逆序对的数量。递归函数的结构可能类似: def merge_sort(nums, left, right): if left >= right: return 0 mid = (left + right) // 2 count = merge_sort(nums, left, mid) + merge_sort(nums, mid+1, right) count += merge(nums, left, mid, right) return count 其中merge函数负责合并并返回该次合并中的逆序对数目。merge函数中,当左半部分的元素大于右半部分时,累加mid - i + 1到count中。 编写merge函数时,需要创建临时数组,然后比较左右两部分的元素,按顺序放入临时数组,同时统计逆序对的数量。例如: def merge(nums, left, mid, right): temp = [] i = left j = mid + 1 count = 0 while i <= mid and j <= right: if nums[i] <= nums[j]: temp.append(nums[i]) i += 1 else: temp.append(nums[j]) count += mid - i + 1 j += 1 # 处理剩余元素 temp.extend(nums[i:mid+1]) temp.extend(nums[j:right+1]) # 将临时数组复制回原数组 nums[left:right+1] = temp return count 这样,在每次合并的时候,当右半部分的元素被选中时,累加对应的逆序对数目。整个归并排序完成后,累计的count就是总的逆序对数目。 需要注意的地方是原数组会被修改,所以如果需要保留原数组,可能需要先复制一份。例如,在调用的时候,可以复制数组,然后对复制的数组进行排序和计算。 例如,主函数可能是: def count_inversions(nums): # 复制原数组,避免修改原数组 temp_nums = nums.copy() return merge_sort(temp_nums, 0, len(temp_nums)-1) 测试的时候,比如输入[3,1,2],应该返回2。输入[1,2,3,4]返回0,输入[4,3,2,1]返回6。 可能的错误点包括: 1. 索引计算错误,比如mid的计算是否正确,是否导致分割错误。 2. 在合并时,i的范围是left到mid,j是mid+1到right,所以在处理剩余元素的时候,应该用nums[i:mid+1],而不是到mid,因为切片是左闭右开的,所以mid+1作为结束索引可以包含到mid的元素。 3. 修改原数组的部分是否正确,即nums[left:right+1] = temp,因为right是包含的,所以切片结束是right+1。 另外,关于空间复杂度,归并排序需要O(n)的额外空间,这在处理大数组时可能需要注意。 现在,综合这些思考,可以编写出正确的归并排序逆序对的代码。</think>以下是使用归并排序计算逆序对的中文代码实现及详细解释: ### 代码实现 ```python def count_inversions(nums): def merge_sort(arr, left, right): if left >= right: return 0 mid = (left + right) // 2 # 分治递归计算左右子数组的逆序对 count = merge_sort(arr, left, mid) + merge_sort(arr, mid+1, right) # 合并过程并统计逆序对 count += merge(arr, left, mid, right) return count def merge(arr, left, mid, right): temp = [] # 临时合并数组 i, j = left, mid+1 count = 0 # 当前合并步骤的逆序对计数 # 左右子数组均未遍历完时比较 while i <= mid and j <= right: if arr[i] <= arr[j]: temp.append(arr[i]) i += 1 else: temp.append(arr[j]) # 关键统计逻辑:左半数组剩余元素都与arr[j]构成逆序对 count += mid - i + 1 j += 1 # 处理剩余元素 temp.extend(arr[i:mid+1]) # 左半剩余 temp.extend(arr[j:right+1]) # 右半剩余 # 将合并结果写回原数组 arr[left:right+1] = temp return count # 创建副本避免修改原数组 return merge_sort(nums.copy(), 0, len(nums)-1) # 测试示例 print(count_inversions([3, 1, 2])) # 输出: 2 print(count_inversions([5, 3, 2, 4, 1])) # 输出: 7 print(count_inversions([4, 3, 2, 1])) # 输出: 6 ``` --- ### 核心原理 1. **分治策略** - 将数组递归拆分为左右两半,分别计算各自的逆序对数量。 - 合并时统计跨越左右子数组的逆序对。 2. **关键合并逻辑** - 当右子数组元素被放入临时数组时,左子数组剩余的所有元素均与该元素构成逆序对。 - 计算公式:`逆序对增量 = mid - 当前左指针 + 1` --- ### 复杂度分析 - **时间复杂度**:O(n log n) 与归并排序相同,合并过程总计执行 log n 层,每层处理 n 个元素。 - **空间复杂度**:O(n) 临时数组存储合并结果。 --- ### 关键点说明 - **原数组保护**:通过`nums.copy()`避免修改输入数据。 - **索引计算**:`mid`值为`(left + right)//2`确保分割正确。 - **剩余元素处理**:使用`extend()`直接添加未遍历完的子数组。 此算法高效且优雅地利用归并排序的特性,在排序过程中同步完成逆序对统计。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值