一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第17天,点击查看活动详情。
小和问题
这个题暴力的解法其实很直观,就是每到一个i就左边遍历一遍计算小和,最终是一个N^2的算法。
这里能不能做到NlogN复杂度?
可以的,基本思想和归并排序也是类似的,不过需要底层逻辑改一下
首先我们需要转换一下我们的思维,求小和可以先看左边1,得到有4个1的小和(右侧4个值比1大),接着有2个3的小和....按照这种思路最后拿num * value 累加得到小和数,例如2个3小和其实意思就是右侧有两个数字比3大,算小和时候会带上两个3 - > 2*3
然后上述这个就可以基于归并思想来解决:
一开始1与3处理,得到1个1的小和结果,辅助数组返回为[1 3]
接着处理13与4,两个下标分别比较,首先1<4,辅助数组新增1,且小和新增1个1,然后3<4,辅助数组新增3,且小和新增1个3(左侧小产生小和)...
左侧完成了进行右边的处理,得出两边的小和,最后再进行一轮处理就行了
上述其实不一定要把一开始的思想反过来,也可以让二叉树右侧和左侧比较,大的来计数小和也是可行的,并且之前小和结果信息一样利用
这里主要要思考一下为啥小和数字信息保留了,没有遗漏和重算:
例如3再第一次归并时候和4进行了比较,留下了小和信息
然后下一次归并时候又和右侧的5进行了比较,留下了小和信息 -> 3*2 没有遗漏和重算也复用了结果
为什么没有遗漏和重算也是需要思考的(不遗漏:只有和右组merge时候才会处理,都会merge的所有不遗漏。不重算:计算小河后以就merge了,merge后不会再计算):
为什么递归时需要数组排序
这样能够很快直接知道后续有多少个数字比你自己大,这样其实就是通过排序把原先的遍历过程代替了,这样可以直接知道小数个数
和标准mergeSort的差别
另外还要提一下如果左右相同,必须先拷贝右组的并且不产生小和,这样才能知道有多少个准确的小和数量,下图现在1明确小于2,然后可以直接得到5个1的小和,然后连着3个5个1的小和,然后左侧到2继续...
上面辅助数组最后是7个1....
代码实现
public static int smallSum(int[] arr){
if(arr == null || arr.length < 2){
return;
}
return process(arr,0,arr.length-1);
}
// 既要排好序,也要求小和
public static int process(int[] arr,int L,int R){
// 越界判断
if(L==R){
return 0;
}
// 获取中点
int mid = L+ ((L+R) >> 1);
// 注意:这里是左侧的排序和小和数量 + 右侧的排序小和数量 + 左右侧排好后merge小和数量 = 小和的总数
return process(arr,L,mid)+process(arr,mid+1,R)+merge(arr,L,mid,R);
}
public static void merge(int[] arr,int L,int M,int R){
// 定义辅助数组和指向L和M+1的指针
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存储小和 - 如果左侧小于右侧,那么得到小和为左侧Value * 右侧数下标到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++];
}
// 辅助数组拷贝至目标 - 完成arr的L - R区间排序
for(i=0;i < arr.length;i++){
arr[L + i] = help[i];
}
return res;
}
逆序对问题
也是有点类似的问题,解决思路是基本一致的,之前是求左边比自己小的,这里是求左边比自己大的,区别就是这个,其他解题思路一致。
我们可以同样反转一下思路,求每个数右侧比自己小的,这就是这个数对应的所有逆序对:
接着通过归并递归思路来求右边有多少数比左边小,然后回升记录逆序对即可