利用归并排序的局部有序性——计算逆序数

我们来看这样一道题目:

给出逆序数的定义:在数列A={a_0,a_1,a_2,...,a_{n-1}}中,如果有一组数(i,j)满足a_i>a_j,且i<j,那么这组数称为一个逆序,数列A中逆序的数量就称为逆序数。

简单来说,逆序的这组数就是其数值大小和其先后顺序相反

现给出数组A与其长度n,要求返回其逆序数的值

解法一

题解要求我们找到所有的逆序,观察题目,我们很容易想到,可以先锚定一个下标较小的数,然后向后遍历数组,每找到一个数值比它小的元素,就相当于是找到了一组逆序。不难发现,这就相当于冒泡排序。我们在冒泡排序的过程中,每交换一次,就相当于是找到一组逆序。也就是说,冒泡排序的总交换次数就是数组的逆序数,这种解法的时间复杂度为O(n^2)

当数组长度较大的时候,冒泡排序的效率就很低了,那么我们能否找到效率更高的算法呢?

解法二

在解法一中,我们仅把目光放在“一个”下标较小的数身上,那么,为了提高效率,我们是否可以一次性把目光锚定在更多下标较小的数上呢?同时,我们是否可以不遍历后面全部的元素,仅遍历部分元素,就把全部数值比我们所锚定元素小(或大)的元素个数给找出来呢?

想要回答第一个问题,我们势必要将数组划分为两个区间;

而对于第二个问题,我们可以想到,若当后面的数组有序,则可以实现部分遍历找全部元素的目的

综合这两个想法,我们可以发现,这和归并排序的方式十分相似

假定给出数组A={2,5,7,3,6,9,8,4,1,5}

将其分为两个子数组:

C={2,5,7,3,6} , D={9,8,4,1,5}

假定我们已求出C与D各自的逆序数

由于C的元素下标肯定比D小,那么对于C的元素而言(假设对7来说),D中存在n个(这里为3个)比它小的数,C的逆序数就再加3

现将其排序:

C={2,3,5,6,7} , D={1,4,5,8,9}

那么对于D的每一个元素(称之为d),我们只需要遍历C,直到找到比d大的元素(称之为c),那么数组C中c及其之后的元素个数即为元素d所对应的逆序数,具体计算方法为:

记数组C的长度为length,则length=mid+1,设元素c的下标为i(其实就是对应指针i),那么逆序数+=length-i(即为元素c及其之后的元素个数)

对于数组A,我们可以将其分为数组C,D进行求解,那么想要求解数组C,D自身的逆序数,我们只需要将其再次划分,直到划分为子数组内只有一个元素为止。这种递归方式也是和归并排序完全相同的

至此,我们就可以利用归并排序中的局部有序性求出数组的逆序数,该算法的时间复杂度为O(nlogn),空间复杂度为O(n)

实现代码如下:

int MergeAndCal(int* nums, int* temp, int left, int mid, int right) {
	int ans = 0;
	int i = left;
	int j = mid + 1;
	int k = left;
	int length = mid + 1;
	while (i < mid + 1 && j < right + 1) {//从这里开始是合并,我们加入计数的代码
		if (nums[i] <= nums[j]) {
			temp[k++] = nums[i++];
		}
		else{
			ans += length - i;     //相比与正常的合并,就加多了这一行代码
			temp[k++] = nums[j++]; 
	    }
	}
	while (i < mid + 1) {
		temp[k++] = nums[i++];
	}
	while (j < right + 1) {
		temp[k++] = nums[j++];
	}
	for (k= left; k < right+1; k++) {
		nums[k] = temp[k];
	}
	return ans;
}
int ReverseNum(int* nums, int* temp, int left, int right) {
	int res1 = 0, res2 = 0, res3 = 0;
	if (left < right) {
		int mid = left + (right - left) / 2;
		res1=ReverseNum(nums, temp, left, mid);//第一个子数组自身的逆序数
		res2=ReverseNum(nums, temp, mid + 1, right);//第二个子数组自身的逆序数
		res3 = MergeAndCal(nums, temp, left, mid, right);//两个子数组合并后新增的逆序数
		return res1 + res2 + res3;//答案为总和
	}
	return 0;//数组只有一个元素时,其逆序数为0
}
int BubbleSort(int* nums, int numsSize) {//开一个冒泡排序来检验结果正确性
	int ans = 0;
	for (int i = 0; i < numsSize-1; i++) {
		for (int j = i+1; j < numsSize ; j++) {
			if (nums[i] > nums[j]) {
				ans++;
			}
		}
	}
	return ans;
}
int main() {
	int nums1[18] = { 4,2,6,8,7,2,54,3,2,15,14,16,98,10,72,35,20,74 };
	int nums2[18] = { 4,2,6,8,7,2,54,3,2,15,14,16,98,10,72,35,20,74 };
	int* temp = (int*)malloc(sizeof(int) * 18);
	int ans1 = ReverseNum(nums1, temp, 0, 17);
	int ans2 = BubbleSort(nums2, 18);
	printf("归并排序计数结果:%d\n冒泡排序计数结果:%d\n", ans1,ans2);
	return 0;
}
  • 5
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值