剑指offer之逆序对的个数(使用归并排序)

题目

题目:在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数P。并将P对1000000007取模的结果输出。 即输出P%1000000007。

输入

描述

题目保证输入的数组中没有的相同的数字
数据范围:
对于%50的数据,size<=10^4
对于%75的数据,size<=10^5
对于%100的数据,size<=2*10^5

示例

1,2,3,4,5,6,7,0 结果是 7

思路

一看到这个题我就感觉似曾相识,知道需要使用归并排序的思想来做,却早已忘记了归并排序的思想到底是什么。。。于是特地整理了一遍,见博客归并排序算法

在归并排序的归并步骤时,可以顺便统计逆序对的个数。

图一.png

假设正在归并上面的数组,左侧的2,3,6,8和右侧的1,4,5,7已经排好序了,左侧和右侧内部都没有逆序对,而从左侧取一个数,从右侧取一个数,则有可能形成逆序对。

例如,开始左侧拿出2,右侧拿出1,可知2>1,形成了逆序对。此时逆序对只是加1吗?并不是,因为2右边的数都是大于2的,所以可以判断左边的数和右边的1可以形成4对逆序对((2,1)、(3,1)、(6,1)、(8,1))。

接下来比24,不会形成逆序对。再比34,不会形成逆序对。

当比较到64的时候,形成了逆序对,个数为2((6,4)、(8,4))。

图二.png

归纳一下,也就是在归并的时候,如果右侧的元素小于左侧的元素,这个时候开始统计逆序对就行了,如果左侧的索引为i,左侧的末尾元素的索引为mid,逆序对个数就为mid-i+1

这样并没有结束,前面的假设是左侧和右侧是有序的,事实上并不是,左侧和右侧也进行了归并的过程才能变得有序,而在归并过程中,也能计算出逆序对的个数。

所以:

总的逆序对的个数=左侧归并时求得的逆序对个数 + 右侧归并时求得的逆序对个数 + 对整体进行归并时的逆序对个数。

可能会怀疑这三种情况会有重复,但是并没有。左侧归并找到的逆序对相当于从左侧数组中取2个数,而整体归并的时候是分别从左右数组中取1个数,不可能发生重复!

代码

知道上面的思路后,可以很容易的将归并排序代码进行修改。

#include <iostream>
#include <stdlib.h>

using namespace std;

int const N = 6;
//下面注释掉的是标准的归并排序,供对比使用
/*void merge(int* a,int left,int mid,int right)
{
	int* b=(int*)malloc(N*sizeof(int));
	int i, j, k;
	i = left;
	j = mid+1;
	k = left;
	for (int i = 0; i < N; i++)
	{
		b[i] = a[i];
	}
	for (; i <= mid && j <= right; k++)
	{
		if (b[i] <= b[j])
		{
			a[k] = b[i];
			i++;
		}
		if (b[i] > b[j])
		{
			a[k] = b[j];
			j++;
		}
	}
	if (i <= mid)
	{
		for (; i <= mid;)
		{
			a[k] = b[i];
			i++;
			k++;
		}
	}
	if (j <= right)
	{
		for (; j <= right;)
		{
			a[k] = b[j];
			j++;
			k++;
		}
	}
	free(b);
	b = NULL;
	return;
}

void merge_sort(int* a, int left, int right)
{
	if(left<right)
	{
		int mid = (left + right) / 2;
		merge_sort(a, left, mid);
		merge_sort(a, mid+1, right);
		merge(a, left, mid, right);
	}
}
*/

int merge2(int* a, int left, int mid, int right)//合并有序数组
{
	int* b = (int*)malloc(N * sizeof(int));
	int i = left;
	int j = mid + 1;
	int k = left;
	int count = 0;//用于计数合并时逆序对数量
	for (int i = 0; i < N; i++)//拷贝一份数组,用于对比
	{
		b[i] = a[i];
	}
	for (; i <= mid && j <= right; k++)
	{
		if (b[i] <= b[j])
		{
			a[k] = b[i];
			i++;
		}
		if (b[i] > b[j])
		{
			a[k] = b[j];
			j++;
			count += mid - i + 1;//计数合并时的逆序对数量,注意不是count+=1!具体原因在上面有详细解释
		}
	}
	if (i <= mid)
	{
		for (; i <= mid;)
		{
			a[k] = b[i];
			i++;
			k++;
		}
	}
	if (j <= right)
	{
		for (; j <= right;)
		{
			a[k] = b[j];
			j++;
			k++;
		}
	}
	free(b);
	b = NULL;
	return count;
}

int merge_sort2(int* a, int left, int right)
{
	if (left < right)
	{
		int res = 0;//用于记录结果
		int mid = (left + right) / 2;
		int lres = merge_sort2(a, left, mid);//左半边的逆序对数量
		int rres = merge_sort2(a, mid + 1, right);//右半边的逆序对数量
		int mres = merge2(a, left, mid, right);//合并过程中的逆序对数量
		res = lres + rres + mres;//总数量是上面三者相加
		return res;
	}
	return 0;//如果只有一个元素,则不可能产生逆序对,逆序对至少要2个元素
}



int main()
{
	int a[6] = {2,3,4,5,6,1};
	int res=merge_sort2(a, 0, 5);
	cout << res<<endl;
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值