数组中的逆序对:归并算法
数组中的逆序对:归并算法
在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数P。并将P对1000000007取模的结果输出。 即输出P%1000000007
输入描述:
题目保证输入的数组中没有的相同的数字数据范围: 对于%50的数据,size<=10^4 对于%75的数据,size<=10^5 对于%100的数据,size<=2*10^5
示例1
输入
[复制](javascript:void(0)😉
1,2,3,4,5,6,7,0
输出
[复制](javascript:void(0)😉
7
本题使用归并算法解决
归并排序使用分治策略,序列一分为二 (O(1)) 后,将子序列递归排序(2 * T(n / 2)),最后合并有序子序列(O(n)),T(n) = 2 * T(n / 2) + O(n) = O(n * logn)。
一、归并排序
1、归并排序的实现
写递归函数就像开车,先系上安全带即先写出递归基。
public static void mergeSortCore(int[] arr, int lo, int hi) {
if (lo == hi) { // <----- 安全带
return;
}
int mid = ((hi - lo) >> 1) + lo;
mergeSortCore(arr, lo, mid);
mergeSortCore(arr, mid + 1, hi);
merge(arr, lo, mid, hi);
}
2、二路归并的实现
将两个有序序列合并为一个有序序列,如下 (还有一个结构紧凑的写法,效率不高 (merge2))
public static void merge(int[] arr, int lo,int mid, int hi) {
int[] temp = new int[hi - lo + 1];
int i = 0, p1 = lo, p2 = mid + 1;
while (p1 <= mid && p2 <= hi) {
temp[i++] = arr[p1] <= arr[p2] ? arr[p1++] : arr[p2++];
}
while (p1 <= mid) {
temp[i++] = arr[p1++];
}
while (p2 <= hi) {
temp[i++] = arr[p2++];
}
for (i = 0; i < temp.length; i++) {
arr[lo + i] = temp[i];
}
}
3、源码
二、剑指 offer[51]
数组中的逆序对 题解
在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组, 求出这个数组中的逆序对的总数 P。并将 P 对 1000000007 取模的结果输出。即输出 P%1000000007
二路归并即 merge,是将两个有序的序列合并为一个有序的序列,在两个子序列 left、right 合并过程中,当 left 中当前元素 A 小于 right 中当前元素 B 时,因为 right 序列已经有序,所以不用比较,A 一定是 left、right 两个子序列当前剩余元素中最小的元素,这省去了 A 与 B 后其他元素比较的操作。
对于本题,在两个子序列 left、right 合并过程中,当 left 中当前元素 A 大于 right 中当前元素 B 时,因为 right 序列已经有序,所以不用比较,A 一定大于 right 序列当前所有剩余元素,其全部可以与 A 组成逆序对,即通过一次比较可到一批逆序对,加速统计过程。
public class Solution {
private int cnt;//逆序对数量
private void MergeSort(int[] array, int start, int end){
if(start>=end)return;
int mid = (start+end)/2;
MergeSort(array, start, mid);
MergeSort(array, mid+1, end);
MergeOne(array, start, mid, end);
}
private void MergeOne(int[] array, int start, int mid, int end){
//temp是用来储存归并结果的数组
int[] temp = new int[end-start+1];
int k=0,i=start,j=mid+1;
while(i<=mid && j<= end){
//如果前面的元素小于后面的不能构成逆序对
if(array[i] <= array[j])
temp[k++] = array[i++];
else{
//如果前面的元素大于后面的,那么在前面元素之后的元素都能和后面的元素构成逆序对
temp[k++] = array[j++];
//因为cnt定义是int,所以每一次计算都要对1e7取模以防止超出Int范围
//当 left 中当前元素 A 小于 right 中当前元素 B 时,因为 right 序列已经有序,所以不用比较,A 一定是 left、right 两个子序列当前剩余元素中最小的元素
cnt = (cnt + (mid-i+1))%1000000007;
}
}
/*
这里为什么就不需要计算cnt了,因为前面left给right的“让步”时就已经把所有的逆序对算完了
*/
while(i<= mid)
temp[k++] = array[i++];
while(j<=end)
temp[k++] = array[j++];
for(int l=0; l<k; l++){
array[start+l] = temp[l];
}
}
/*
入口函数
*/
public int InversePairs(int [] array) {
MergeSort(array, 0, array.length-1);
return cnt;
}
}
下面是我的代码,和解析中的不同,我采用了[left,right)这样的左闭右开的形式
public class Solution {
int cnt=0;
/*
[left,right)
*/
private void mergeSort(int[]array,int left,int right){
if(left>=right-1){
return;
}
int middle=(right+left)/2;
mergeSort(array,left,middle);
mergeSort(array,middle,right);
merge(array,left,middle,right);
}
private void merge(int[]array,int left,int middle,int right){
int i=left,j=middle,index=0;
int[]temp=new int[right-left];
while(i<middle&&j<right){
if(array[i]<=array[j]){
temp[index]=array[i];
index++;
i++;
}else{
temp[index]=array[j];
cnt=(cnt+middle-i)%1000000007;
index++;
j++;
}
}
while(i<middle){
temp[index]=array[i];
index++;
i++;
}
while(j<right){
temp[index]=array[j];
index++;
j++;
}
for(int t=left;t<right;t++){
array[t]=temp[t-left];
}
return;
}
public int InversePairs(int [] array) {
int length=array.length;
mergeSort(array,0,length);
return cnt;
}
}
关键的地方是:
if(left>=right-1){
return;
}
int middle=(right+left)/2;
因为right是开区间,left无论怎样都是取不到right的,所以应该是right-1
另外,一开始这里错写成了int middle=(right-left)/2;,不要犯这种错误
-
为什么要对1e7取模
其实不止1e9+7,还有1e9+9和998244353。这三个数都是一个质数,同时小于
。所以有什么好处呢?
\1. 所有模过之后的数在加法操作在int范围内不会溢出,即
。
\2. 在乘法操作后在long long范围内不会溢出,即
。
有很多题目的答案是很大的,然而出题人的本意也不是让选手写高精度或者Java,所以势必要让答案落在整型的范围内。另外,对质数取模的话,能尽可能地避免模数相同的数之间具备公因数,来达到减少冲突的目的。那么有个很大的且好记的质数1e9+7(包括它的孪生素数1e9+9)。

浙公网安备 33010602011771号