数组中的逆序对

  基础的排序算法,尤其是常见的几种特殊的排序算法,一定要深刻理解,记住特性,甚至要能写出代码。

题目描述

  在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数P。例如{7,8,5,1},这个数组中8在7的后面。不产生逆序对,5在7,8的后面,产生两个逆序对,1在7,8,5的后面,产生三个逆序对,共5个逆序对。结果需要将P对1000000007取模的结果输出。 即输出P%1000000007。

算法分析

暴力法 O ( n 2 ) O(n^2) O(n2)

  我们在题目描述中,已经把{7,8,5,1}的例子讲解的比较细致了,这个描述本来就是求解这个问题的一种思路,我们只需要把每个数都和自己前面的数字挨个比较就可以了。找出每个数字产生的逆序对数目,然后求和即可。
  这个算法的代码很好实现(可以使用python一行代码实现),类似于普通的插入排序代码,但是在比较过程中发现逆序需要记录。因为每个元素都要和自己前面的元素进行比较,和自己前面的元素比较的复杂度是 O ( n ) O(n) O(n),然后需要进行 O ( n ) O(n) O(n)趟,总的时间复杂度是 O ( n 2 ) O(n^2) O(n2)
  其实这里还有一种方法,可以用上快排的partition函数然后来找每个数字产生的逆序对数目,但是由于复杂度依然是 O ( n 2 ) O(n^2) O(n2),所以在这里就不赘述。这里给出一个结论,阅读完下面内容有助于对这个结论进行理解。稳定排序稍作改动都可以求解这个题目

基于归并排序的巧妙解法

  首先我们来看一些简单的基本分析,如果这个数组本来就是一个升序序列,那么显然这个数组的逆序对数目就是0。如果这个数组可以切开分成两个序列,两个序列都是升序序列,这个时候逆序对数是多少呢。显然两个序列内部的逆序对数目显然是0。合并在一起研究,第一个序列没有逆序对。但是第一个序列可能存在数字比第二个序列中的数字大,因为第二个序列是在第一个序列后面的,这个时候就会产生逆序对。我们来算一下数目,显然每一个数大于后序列多少个数字,则这个数字就会对后面的数字产生多少逆序对。此时我们可以考虑对两个序列执行有序合并,根据前面序列的数字比后面的序列有多少个数字大来判断这个时候的逆序对是多少,合并的同时是为了已经计算过逆序对在下次合并的时候不会继续计算
  我们在两个序列进行合并的时候,我们此时需要分析一个问题,上次合并的时候得到的逆序对会不会和下一次合并的时候相互叠加或者相互抵消。答案是不会,因为我们合并的时候,只统计前面的数字对后面的数字产生的逆序对,也就是说在合并的时候前移和和后移不管怎么一起操作,我们都是看对后面的数字产生的逆序对。所以不会产生抵消或者叠加的情况。
  我们已经会处理两个有序序列的逆序对情况,那我们可以把一个长的数组划分成两个有序序列,然后去计算两个子序列的逆序对。我们在统计逆序对的时候还做了一件事就是把两个有序序列合并成一个有序序列,这样我们的有序序列就得到了,就可以继续向下进行了。
  我们此时来做个梳理,我们首先要对问题进行分解,需要得到整个序列的逆序对,则需要将序列一分为二,先统计两个序列的逆序对,然后将两个序列变成有序序列,再最终进行合并,得到这一批次的逆序对数目。只要序列还是很长,我们就一直进行划分,划分到什么时候我们能够容易处理,显然是序列长度为1或者0的时候,此时可以确定,序列一定是有序的,产生的逆序对是0。所以整个过程就是分治的过程。
  整个过程其实也是归并排序的过程,因为我们在合并的过程中也做了有序合并。算法是基于分治的,只要序列长度大于2就一直划分,合并过程就是将两个有序序列合并为一个有序序列的过程,不过本题中需要统计前面序列中的元素比后面序列中多少个元素大。这里我们可以采取一个策略就是,归并合并从大往小合并,那么要合并的元素一定是两个序列剩余元素中最大的,如果要合并的元素是前面序列的元素,则后面的序列没有被合并的元素一定是要比当前要合并的元素要小,后面序列还剩多少个元素,逆序对就是多少。
  我们来举个直观的例子,{7,8,5,1}被划分成四个序列,每个序列的元素个数都是1,因为1我们可以直接处理。
  划分结束我们进入治的环节,{7},{8}从后往前合并,先合并8,不产生逆序对,合并8之后第二个序列为空,则直接将第一个序列进行复制。该过程不产生逆序对。我们再合并{5},{1},显然先合并的元素是5,是前面的序列,则会产生后面序列长度个逆序对,也就是1个。第一轮归并之后,我们的序列是{7,8,1,5},共一个逆序对。
  第二趟,我们合并两个上次合并出来的序列,分别是{7,8}和{1,5},因为8>5,我们需要合并8,8是前面的序列,产生逆序对为2,接着合并{7}和{1,5},因为7>5,我们需要合并7,7是前面的序列,产生逆序对为2,第一个序列合并完毕,直接复制第二个序列,不产生逆序对。第二轮归并结束,整个归并结束,序列变为{1,5,7,8},共四个逆序对。
  归并结束,一共五个逆序对。

C++代码如下

class Solution {
public:
	static const int MOD = 1000000007;
	using RanIt=vector<int>::iterator;
	int InversePairs(vector<int> data) {
		vector<int> dup(data);
		return mergeSort(data.begin(), data.end(), dup.begin());
	}
	int mergeSort(const RanIt& begin, const RanIt& end, const RanIt& dup) {
		int len = end - begin;
		if (len < 2) return 0;//如果序列长度不为0,1持续划分
		int mid = (len + 1) >> 1;
		auto m1 = begin + mid, m2 = dup + mid;
		auto i = m1, j = end, k = dup + len;
		int ans = (mergeSort(dup, m2, begin) + mergeSort(m2, k, m1)) % MOD;
		for (--i, --j, --k; i >= begin && j >= m1; --k) {
			if (*i > * j) {//如果需要合并前面序列的元素
				*k = *i--;
				(ans += j - m1 + 1) %= MOD;
			}
			else *k = *j--;
		}
		if (i >= begin)copy(begin, i + 1, dup);
		else copy(m1, j + 1, dup);
		return ans;
	}
};

  归并排序是也是分治算法的一个典型案例,归并排序空间复杂度是O(n),是平均复杂度为 O ( n l g n ) O(nlgn) O(nlgn)的排序中唯一是稳定排序的。因为归并排序时间复杂度为 O ( n l g n ) O(nlgn) O(nlgn),所以这个题目的复杂度就是O(nlgn)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值