前言
当前所有算法都使用测试用例运行过,但是不保证100%的测试用例,如果存在问题务必联系批评指正~
在此感谢左大神让我对算法有了新的感悟认识!
问题介绍
原问题
给定整数数组arr,计算数组的小和
小和指的是数组中每一个数前面比自己小的数的和为s1,s2… ,那么s1 + s2 + s3 … 为数组arr的小和
如:arr = {1,3,5,2,4,6}
s1为arr[0]前面比自己小的数的和,s1 = 0
s2 为arr[1]前面比自己小的数的和, s2 = 1
同理s3 = 1+3 = 4
则arr的小和为:0+1+4+1+6+15 = 27
解决方案
原问题:
解法一:
直接双重循环找到每一个数为底的小和,最后求和即可,这里不再赘述,代码过于简单
解法二:
在merge排序的基础上添加一些操作
1、在归并排序的merge阶段,对于左边比右边小的情况,此时右边有n个数,则产生arr[left] * n大小的小和,加到sum中即可
具体看代码与感悟
代码编写
java语言版本
原问题:
/**
* 二轮测试:计算arr的小和
* 小和指的是从arr的第一个元素开始,前面比当前值小的数的和为一个小和
* 一直到最后一个数时,所有的小和之和为结果。
* @param arr
* @return
*/
public static int getSmallAddCp1(int[] arr) {
if (arr == null || arr.length == 0) {
return 0;
}
return processCp1(arr, 0, arr.length-1);
}
/**
* 返回从left到right的小和
* @param arr
* @param left
* @param right
* @return
*/
private static int processCp1(int[] arr, int left, int right) {
if (left == right) {
// 只有一个数的时候,和不包含自己,所以返回0
return 0;
}
// 前中位数
int mid = (left + right)/2;
// 左边
int leftSum = processCp1(arr, left, mid);
int rightSum = processCp1(arr, mid + 1, right);
int mergeSum = mergeCp2(arr, left, mid, right);
return leftSum + rightSum + mergeSum;
}
/**
* 版本1:合并计算小和
* 根据右边的数,循环判断左边的每一个数是否小于当前右边的数,小于说明需要增加小和,大于则不产生小和
* 这个时间复杂度是n^2
* @param arr
* @param left
* @param mid
* @param right
* @return
*/
private static int mergeCp1(int[] arr, int left, int mid, int right) {
if (left == mid && mid == right) {
return 0;
}
int sum = 0;
for (int r = mid + 1; r <= right; r++) {
int cur = arr[r];
for (int l = left; l <= mid; l++) {
// 比较是否比右边小
if (arr[l] < arr[r]) {
// 产生小和
sum += arr[l];
}
}
}
return sum;
}
/**
* 版本2:合并计算小和
* 将归并排序融进来,但是会改变原有数组的顺序,除非给的是深拷贝副本
* 这个时间复杂度是nlogn
* @param arr
* @param left
* @param mid
* @param right
* @return
*/
private static int mergeCp2(int[] arr, int left, int mid, int right) {
if (left == mid && mid == right) {
return 0;
}
int l = left;
int r = mid+1;
// 容器用来装有序数组
int[] container = new int[right - left + 1];
int cindex = 0;
int sum = 0;
while (l <= mid && r <= right) {
if (arr[l] <= arr[r]) {
// 1、先产生小和,这里因为有排序所以r之后的数都会比r大,所以小和应该要乘以后面的数量
sum += arr[l] * (right - r + 1);
// 2、放入容器
container[cindex++] = arr[l];
l++;
}else {
// 不产生小和,只执行排序
container[cindex++] = arr[r];
r++;
}
}
// 结束后,将剩下的放入容器中
if (l > mid) {
// 左边到头,右边还有
while (r <= right) {
container[cindex++] = arr[r++];
}
}else {
while (l <= mid) {
container[cindex++] = arr[l++];
}
}
// 将容器的新排序装回到arr中
l = left;
for (int i = 0; i < container.length; i++) {
arr[l++] = container[i];
}
return sum;
}
public static void main(String[] args) {
System.out.println(getSmallAddCp1(new int[]{1,3,5,2,4,6}));
}
c语言版本
正在学习中
c++语言版本
正在学习中
思考感悟
首先我写了两个版本的解法,第一个版本是我刚开始实现的方法,后来看到书上提了一句,才想起来为什么不在merge的时候进行一波归并排序呢?于是出现了nlogn的解法~
这道题考察两个点:
1、归并排序的熟悉程度
归并排序在我刚开始写算法的时候挺怕这种递归和动态规划的题目,那么现在我可以感觉到自己对于归并排序的实现已经是可以不用记忆的方式直接按照理解实现出来了,很高兴自己一年半来的进步!
2、第二个就是这道题另一个考察思路了
首先我们知道一个数如果比后面的数小,那么比它小的数一定也比它自己大的数小,这个是传递性
那么在归并算法的时候,我们可以知道两个子数组自身是有序的,并且左边和右边都是在自己的子数组中交换,没有出现交叉这个很重要,如果出现交叉那么就没有办法保证左边的数组在原数组中一定出现在右边数组的左边。
其次,根据传递性,如果左边数组中有一个数a小于右边的数组数b,那么a一定小于b后面的所有数,假设b后面有n个数,n个数比a大,那么a就会被加n+1次。这个是另一种角度看这个问题,如果理解了这道题基本就没有难度了,书中给的难度是中上。
写在最后
方案和代码仅提供学习和思考使用,切勿随意滥用!如有错误和不合理的地方,务必批评指正~
如果需要git源码可邮件给2260755767@qq.com
再次感谢左大神对我算法的指点迷津!