数组小和问题:用递归思想和归并排序求数组arr的数组小和
提示:之前讲过的归并排序,可不仅仅是归并排序这么简单,递归思想和归并的思想,它有大用处!
看本文之前,必看之前的基础知识:
(1)10大排序算法之四:归并排序【稳定的】,复杂度中,系统常用归并排序
题目
数组arr中,某个i位置左边所有比[i]小的数的和,叫小和
从i=1–N-1所有i位置的小和的累加和,叫数组小和
请你求数组arr的数组小和是多少?
一、审题
示例:arr=1,3,2,4,5
0位置不看
1位置的3,左边有1小于3,故小和为1
2位置的2,左边只有1小于2,故小和为1
3位置的4,左边有3个都小于4,故小和为1+3+2=6
4位置的5,左边全部都小于5,故小和为6+4=10
所以数组小和为1+1+6+10=18
二、解题
暴力解,面试0分!!
来到i位置,都去索引0–i-1上有几个小于[i],然后将他们都累加好
最后所有位置i的小和累加就是数组小和
这样的时间复杂度
外层n次遍历
内层对比每一个位置也是n次遍历
故复杂度o(n^2)
面试时肯定0分。
暴力解好说:
//暴力方法——这种方法在笔试过程中会被直接AC,不过面试只能得0分,不过我们可以拿来当对数器
public static int smallSum1(int[] arr){
int sum = 0;
int[] num = new int[arr.length];//每一个arri,比它大的数有多少个
for (int i = 0; i < arr.length; i++) {
for(int j = i + 1; j < arr.length; j++){
if(arr[j] > arr[i]) num[i]++;//去i右边暴力找,找到一个num+一次
}
}
//统计完以后直接累加
for (int i = 0; i < arr.length; i++) {
sum += arr[i] * num[i];//值*它出现的次数
}
return sum;
//这种方法:空间复杂度还需要另外申请O(N),时间复杂度是O(N^2),耗时耗空间。
}
当然,你可以在笔试用一下试试,复杂度通不过,即使能AC,也是一部分
当然了,你啥招都想不到,是可以这么做得
最优解:用递归思想和归并的思想
这里不得不说,递归和归并排序的思想,是非常好非常好的基础知识
(1)10大排序算法之四:归并排序【稳定的】,复杂度中,系统常用归并排序
在解大厂的面试题的时候,你要敏感地想到,你学过什么数据结构与算法的知识,咱们能用哪些数据结构解?
咱们能利用哪些见过的题目解决这个问题?
这很难,但是唯独多练习,多思考,多理解,才可能培养出来敏感度。
这个题,求一个位置i的小和,你要看它左边有几个比它小,这事还就真的很难办……
我们很多数据结构与算法中的题目,一定要在原来题目的基础上转化它的意思,才能解!
我们来观察示例:arr=1,3,2,4,5
0位置不看
1位置的3,左边有1小于3,故小和为1
2位置的2,左边只有1小于2,故小和为1
3位置的4,左边有3个都小于4,故小和为1+3+2=6
4位置的5,左边全部都小于5,故小和为1+3+2+4=10
所以数组小和为1+1+6+10=18
在求数组小和的过程中,你看看那个0位置的1,它被加了多少次?
凭什么每一次1能被加入数组小和?
因为右边有数比它大!这就是为什么!
所以,由于0位置1,右边有4个数,比1大,故在求小和的过程中,1被加了4次!!!
那么,同理,我们思考,如果能在每一个位置i,发现它右边有几个数比其大(x个),那我们就可以加多少个[i](x * [i])
这样,我们就能把问题转换出来了,不再局限到题目原来的意思,当然,我们的理解,是数组小和的本质
好,那我们如何能在i位置,快速知道,右边有几个数大于我呢???? ——这就是优化的地方!!!
咱们单独就看上面那个案例:
arr=1,3,2,4,5
曾经我们学习归并排序时,这时候,要把这个数组归并排序
mid=0+4/2=2
——先左边递归排序,右边递归排序
然后再这样的情况下,从0–2,3–4内部已经排序好了为:1,2,3 4,5
——然后该归并为1一个数组了吧,merge:1,2,3 4,5–>1 2 3 4 5的过程如下
我们说从p1和p2开始,谁小copy谁到help数组,此时大家观察一下:
在merge的过程中,我们产生小和,累加到数组小和上
(1)如果[p1]<[p2],自然是满足左边部分的小于右边部分的值,右边部分有几个比p1位置大?
有R-p2+1个:2个
所以数组小和整体就可以累加21了,之后p1++,把1copy到help中。
(2)此时:[p1]<[p2],自然是满足左边部分的小于右边部分的值,右边部分有几个比p1位置大?
有R-p2+1个:2个
所以数组小和整体就可以累加22了,之后p1++,把1copy到help中。
(3)此时:[p1]<[p2],自然是满足左边部分的小于右边部分的值,右边部分有几个比p1位置大?
有R-p2+1个:2个
所以数组小和整体就可以累加23了,之后p1++,把1copy到help中。
(4)此时p1已经越界,还剩下右边部分没有完事,直接copy右边所有元素给help
然后转移到arr中,完成merge操作
注意,如果左边部分有比右边部分大的呢?
比如:1,4 2,5
咱们对比4和2时,左边>=右边,所以不需要产生小和,直接copy右边的2就行,继续merge
总结一下:我们如何利用归并排序的merge来产生x个小和,累加到结果上呢?
数组arr,划分为L–mid–R两部分:
(0)咱们定义一个递归排序的主函数mergeSortAndGetSmallSum:
——递归排序左边部分,会有左边部分的小和smallLeft
——递归排序右边部分,会有右边部分的小和smallRight
——然后咱们merge L–mid–R,使得arr在L–R上整体有序,与此同时产生这个整体上贡献的小和smallCurrent
(1)merge本身常规操作不动,但是,只需要在左边<右边时,产生R-p2+1个数(这个数就是[p1]),然后累加到结果中即可,这可不就是我们要优化的方法吗?o(1)速度知道右边有x个数,大于p1位置,这样直接就把x[p1]累加到小和中。
(2)当左边>=右边时,咱不产生小和,直接完成merge的copy右边p2的数去help
然后我们L–R上整体的小和是多少呢?
smallLeft+smallRight+smallCurrent=ans
返回ans
理解了吗?我们在做啥
我们递归左边,或者递归右边的时候,左右2部分的数,不会乱
所以左边产生左边的小和left,右边产生右边的小和right
最后整体merge,只看右边部分,有多少个大于左边每个位置p1的?
整体产生一个小和current,累加起来就是我们统计的数组小和。
好,手撕代码,看一遍就能理解上面的图:
//复习数组小和
//归并过程:
public static int mergeGenerateSmallSum(int[] arr, int L, int mid, int R){
if (L == R) return 0;//就剩一个数,不需要哦merge,小和也不会有
//至少2个数
int p1 = L;
int p2 = mid + 1;
int i = 0;
int[] help = new int[R - L + 1];
int ans = 0;//merge中产生的小和
while (p1 <= mid && p2 <= R){
//现在只有小于的时候,咱们才copy左边哦,因为,左>右时,我们才copy右边
ans += arr[p1] < arr[p2] ? (R - p2 +1) * arr[p1] : 0;//这一步得放前面哦!!!!
//目的就是这样产生小和,左>=右时不需要产生小和,加0就行。
//正常该merge的merge,但是小和是要产生与左边小于右边的情况下
help[i++] = arr[p1] < arr[p2] ? arr[p1++] : arr[p2++];//注意,之前的merge比较是<=,这样稳定
}
//没copy的继续
while (p1 <= mid) help[i++] = arr[p1++];
while (p2 <= R) help[i++] = arr[p2++];
//然后转移回去
for (int j = 0; j < help.length; j++) {
arr[L + j] = help[j];
}
//返回我们这个过程中收集到的小和
return ans;
}
//归并排序,并产生数组小和left,right,cur
public static int mergeSortAndGetSmallSum(int[] arr, int L, int R){
if (L >= R) return 0;//1个数没法归并
int mid = L + ((R - L) >> 1);
int left = mergeSortAndGetSmallSum(arr, L, mid);
int right = mergeSortAndGetSmallSum(arr, mid + 1, R);
int cur = mergeGenerateSmallSum(arr, L, mid, R);//整体归并产生小和
return left + right + cur;
}
//测试
public static void test2(){
int[] arr = {1,3,2,4,5};
int ans = mergeSortAndGetSmallSum(arr, 0, arr.length - 1);
System.out.println(ans);//18
}
public static void main(String[] args) {
// test();
// checker();
test2();
}
结果的确是:18
之前我们说过了,归并排序时间复杂度也就o(n*log(n))
自然是低于暴力解
如何验证你这个优化的方法对不对呢?
对数器:
//对数器之构建随机数组
public static int[] createArray(int arrSize, int maxValue){
int[] arr = new int[arrSize];
for (int i = 0; i < arrSize; i++) {
arr[i] = (int)(maxValue * Math.random());//0-N-1的随机数
}
return arr;
}
public static void checker(){
//生成检验数组
int[] arr = createArray(100,10);
int[] arr2 = new int[arr.length];//赋值同样一个数组arr
for (int i = 0; i < arr.length; i++) {
arr2[i] = arr[i];//copy即可
}
int[] arr3 = new int[arr.length];//赋值同样一个数组arr
for (int i = 0; i < arr.length; i++) {
arr3[i] = arr[i];//copy即可
}
//绝对的正确方法——暴力方法,或系统函数,操作arr
int sum1 = smallSum1(arr);
//优化方法,操作arr2
int sum2 = smallSum2(arr2);
//复习手撕的
int sum3 = mergeSortAndGetSmallSum(arr3, 0, arr3.length - 1);
System.out.println(sum1);
System.out.println(sum2);
System.out.println(sum3);
//然后校验
System.out.println( (sum2 != sum1 || sum1 != sum3) ? "oops,wrong!" : "right!");
}
public static void main(String[] args) {
// test();
checker();
// test2();
}
结果:
5274
5274
5274
right!
全部通过
总结
提示:重要经验:
1)当要求的题目,暗示,依赖:比某个位置i左边或者右边大,或者小,的那些数,你要敏感地想到,归并的思想
2)因为归并思想,可以统计左边和右边的大小关系,找到有几个数比p1大,有几个数比p1小
3)数组小和问题,相信你见过之后,熟悉了,今后就不在面试场上怕了。