归并排序的扩展
小和问题和逆序对问题
小和问题
在一个数组中,每一个数左边比当前数小的数累加起来,叫做这个数组 的小和。求一个数组 的小和。
问题的转换
* 原问题是找到一个数它左边的比它小的数,然后全部相加
* 例如: 例子:[1,3,4,2,5] 1左边比1小的数,没有; 3左边比3小的数,1; 4左 边比4小的数,1、3; 2左边比2小的数,1; 5左边比5小的数,1、3、4、 2; 所以小和为1+1+3+1+1+3+4+2=16
* 我们可以反过来想,我们可以确定一个元素,找右边比它大的数有多少个,然后用数量乘以这个数,一样等于最小和,只不过这里是事先先将这个数字的最小求出来。
* [1,3,4,2,5],1右边比1大的数有3,4,2,5共四个,所以41。然后3右边有:4,5两个数比3大,共2个数。所以是32=6。对于4,共有:5,一个数大于五,所以:4*1=4。同理还有:2。共:4+6+4+2=16。
如何用归并实现小和
我们想到,归并在合并两个已经排好序的部分是,分开比较大小,放入辅助空间的,我们可以利用这个合并的比较放入辅助空间的过程。实现后一部分中找到比前一部分某一个数大的有多少个。然后用数量乘以该数即可算出包含该数的最小和。
为什么没有重复与多算与漏算
首先我用简洁的语言来描述:在两个部分归并的时候,我们只算后一部分对前一部分的最小和,是因为前一部分已经算了。为什么算了,因为前一部分也可以划一半,那一半也经历过一次归并。在不断重复划分前一部分终究会划的只剩一个数。从这一个数推后就可以知道,是不会重复与多算的。
我们看这几个数,假设有abcdefghij这几个数。对于c来说,在和c归并前,ab两个是经历过一次归并的,他们两个的最小和已经算了,在和c归并的时候abc就已经求好了最小和,然后abc与de归并,对于c来说ab已经和它算了,他接着和de算。同理abcd与fghij的归并也是一样。
三个函数的改动与对比
一开始的调用函数,返回值有了确定的int,不在是void,为了返回最小和
public static int smallsum(int [] arr) {
if(arr == null || arr.length < 2) {
return 0;
}
return mergesort(arr,0,arr.length-1);
}
后面的主递归函数,不在直接调用两个函数,而是返回值两个递归过程和归并过程的直接想加。逻辑上是:左边不分的最小和加上右边部分的最小和加上两个部分合并的最小和
public static int mergesort(int [] arr, int l,int r) {
if(l==r) {
return 0;
}
int mid;
mid = l + ((r-l)>>1);
return mergesort(arr,l,mid)+mergesort(arr,mid+1,r)+merge(arr,l,mid,r);
}
如何的合并过程的函数有了返回值,最后明显出现了返回值。然后在第一个合并的while过程中加入了最小值的运算:
res+=arr[p1]<arr[p2]?(r-p2+1)*arr[p1]:0;
public static int merge(int []arr,int l,int m ,int r) {
int [] help = new int [r-l+1];
int i=0;
int p1=l;
int p2=m+1;
int res=0;
while(p1<=m&&p2<=r) {
res += arr[p1]<arr[p2]?(r-p2+1)*arr[p1]:0;
help[i++]=arr[p1]<arr[p2]?arr[p1++]:arr[p2++];
}
while(p1<=m) {
help[i++]=arr[p1++];
}
while(p2<=r) {
help[i++]=arr[p2++];
}
for(i = 0;i<help.length;i++) {
arr[i+l]=help[i];
}
return res;
}
我的代码
public class Smallsum {
public static int smallsum(int [] arr) {
if(arr == null || arr.length < 2) {
return 0;
}
return mergesort(arr,0,arr.length-1);
}
public static int mergesort(int [] arr, int l,int r) {
if(l==r) {
return 0;
}
int mid;
mid = l + ((r-l)>>1);
return mergesort(arr,l,mid)+mergesort(arr,mid+1,r)+merge(arr,l,mid,r);
}
public static int merge(int []arr,int l,int m ,int r) {
int [] help = new int [r-l+1];
int i=0;
int p1=l;
int p2=m+1;
int res=0;
while(p1<=m&&p2<=r) {
res += arr[p1]<arr[p2]?(r-p2+1)*arr[p1]:0;
help[i++]=arr[p1]<arr[p2]?arr[p1++]:arr[p2++];
}
while(p1<=m) {
help[i++]=arr[p1++];
}
while(p2<=r) {
help[i++]=arr[p2++];
}
for(i = 0;i<help.length;i++) {
arr[i+l]=help[i];
}
return res;
}
public static void main(String[] args) {
int[] arr;
arr = new int[]{1,1,1,4};
int r=smallsum(arr);
System.out.println(r);
return ;
}
}
代码事故现场
我不管如何一开始编译,都是栈溢出,栈溢出。我就想不清,我的递归没有无限调用啊,然后发现问题居然出现在;
int mid = l + (r-l)>>1;上,这个表达式,是最后位移,而不是位移后相加。
最后将加上一个括号int mid = l + ((r - l )>>1);
逆序对问题
在一个数组中,左边的数如果比右边的数大,则折两个数 构成一个逆序对,请打印所有逆序 对。
我的思路
首先降顺归并排序,然后再合并的过程中,像最小和那样,找出所有,右边比左边小的数。然后要答应就遍历一遍,不打印只是找数量的话直接:
r-p2得到数目
总结
对于所有类似的,找一个数组(部分)前面与后面两个元素关系的,可以考虑归并排序的改变,递归方式。进行求解。注意的是,对两部分已经排好序的性质的运用,像最小和,求个数,那直接r-p2+1,得到所有比arr[p1]大的数的个数,原因仅仅是因为已经排好序,大于p2的部分都大于arr【p2】。
还有要注意的是不断的排序将数字放到辅助空间是必要的,这样不会在一次合并中重复计算莫一个元素。