归并算法衍生的面试题

在一个数组中,一个数左边比它小的数的总和,叫数的小和,所有数的小和累加起来,叫数组小和。求数组小和。

例子: [1,3,4,2,5]

1左边比1小的数:没有

3左边比3小的数:1

4左边比4小的数:13

2左边比2小的数:1

5左边比5小的数:1342

所以数组的小和为1+1+3+1+1+3+4+2=16

思路: 一个数左边比他小的总和=一个数右边所有比他大的总和

归并中 merge方法  主要是针对左边所有数据 和 右边所有数据的合并

那么如果左边比右边小,那么右边的指针到最右距离乘以 你这个左边小的值  就是 一个数右边所有比他大的总和

所有的值累加一块就是最终解。

public static int process(int[] arr, int l, int r) {
		if (l == r) {
			return 0;
		}
		// l < r
		int mid = l + ((r - l) >> 1);
		return 
				process(arr, l, mid) 
				+ 
				process(arr, mid + 1, r) 
				+ 
				merge1(arr, l, mid, r);
	}

	public static int merge1(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 ans=0;
		while (p1<=m&&p2<=r){
			ans+=arr[p1]<arr[p2]?arr[p1]*(r-p2+1):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 (int i1 : help) {
			arr[L+i]=help[i1];
		}
		return ans;
	}

在一个数组中,

任何一个前面的数a,和任何一个后面的数b

如果(a,b)是降序的,就称为逆序对

返回数组中所有的逆序对

比如  2 41 3     输出 (21)(41)(43)

思路:此题求的是右边哪些数比这个数小

和上到题套路相反,归并求降序就行了


	public static int process(int[] arr, int l, int r) {
		if (l == r) {
			return 0;
		}
		// l < r
		int mid = l + ((r - l) >> 1);
		return process(arr, l, mid) + process(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 = help.length - 1;
		int p1 = m;
		int p2 = r;
		int res = 0;
		while (p1 >= L && p2 > m) {
			res += arr[p1] > arr[p2] ? (p2 - m) : 0;
			help[i--] = arr[p1] > arr[p2] ? arr[p1--] : arr[p2--];
		}
		while (p1 >= L) {
			help[i--] = arr[p1--];
		}
		while (p2 > m) {
			help[i--] = arr[p2--];
		}
		for (i = 0; i < help.length; i++) {
			arr[L + i] = help[i];
		}
		return res;
	}

在一个数组中,

对于每个数num,求有多少个后面的数 * 2 依然<num,求总个数

比如:[3,1,7,0,2]

3的后面有:10

1的后面有:0

7的后面有:02

0的后面没有

2的后面没有

所以总共有5

https://leetcode.com/problems/reverse-pairs/ 翻转对儿

这道题就是归并算法和要求的分开做就行了,左边的指针和右边的2倍比较如果符合,右边的就移动,看移动多少个位置就记录

public static int process(int[] arr, int l, int r) {
		if (l == r) {
			return 0;
		}
		// l < r
		int mid = l + ((r - l) >> 1);
		return process(arr, l, mid) + process(arr, mid + 1, r) + merge(arr, l, mid, r);
	}

	public static int merge(int[] arr, int L, int m, int r) {
		// [L....M] [M+1....R]
		int ans = 0;
		// 目前囊括进来的数,是从[M+1, windowR)
		int windowR = m + 1;
		for (int i = L; i <= m; i++) {
			while (windowR <= r && (long) arr[i] > (long) arr[windowR] * 2) {
				windowR++;
			}
			ans += windowR - m - 1;
		}
		int[] help = new int[r - L + 1];
		int i = 0;
		int p1 = L;
		int p2 = m + 1;
		while (p1 <= m && p2 <= r) {
			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[L + i] = help[i];
		}
		return ans;
	}

https://leetcode.com/problems/count-of-range-sum/

给定一个数组arr,两个整数lowerupper

返回arr中有多少个子数组的累加和在[lower,upper]范围上

思路:假设0-i前缀和为X,我们要求累加和在[lower,upper]范围上,可以转化为假如以i结尾,0-i-1范围前缀和在X-up到X-low之间的数

而这一块就是利用归并的merge来做到,那么为啥归并能结合前缀树解决这个问题呢?

初始归并:
此时只会有0或1个元素,不涉及左右两段的情况,是可以的
合并归并:
此时是有左右两段的,左右两段是分别有序的,对前缀和数组排序并不会修改数组中元素的值,只是改变了元素是位置,如对left~right=3~5位置的前缀和排序,排序后前缀和3~5位置的数还是原来3~5位置的数,只是排列变化了

你在merge中 假如 你右边有个值为8 题目要求(1-2)那么你就是找左边6-7的值,假如找到一个6符合,也就是说明 这个6代表的0-X和8代表的0-Y 之间X到Y是在(1-2之间的)

public static int countRangeSum(int[] nums, int lower, int upper) {
		if (nums == null || nums.length == 0) {
			return 0;
		}
		long[] sum = new long[nums.length];
		sum[0] = nums[0];
		for (int i = 1; i < nums.length; i++) {
			sum[i] = sum[i - 1] + nums[i];
		}
		return process(sum, 0, sum.length - 1, lower, upper);
	}

	public static int process(long[] sum, int L, int R, int lower, int upper) {
		if (L == R) {
			return sum[L] >= lower && sum[L] <= upper ? 1 : 0;
		}
		int M = L + ((R - L) >> 1);
		return process(sum, L, M, lower, upper) + process(sum, M + 1, R, lower, upper)
				+ merge(sum, L, M, R, lower, upper);
	}

	public static int merge(long[] arr, int L, int M, int R, int lower, int upper) {
		int ans = 0;
		int windowL = L;
		int windowR = L;
		// [windowL, windowR)
		for (int i = M + 1; i <= R; i++) {
			long min = arr[i] - upper;
			long max = arr[i] - lower;
			while (windowR <= M && arr[windowR] <= max) {
				windowR++;
			}
			while (windowL <= M && arr[windowL] < min) {
				windowL++;
			}
			ans += windowR - windowL;
		}
		long[] help = new long[R - L + 1];
		int i = 0;
		int p1 = L;
		int p2 = M + 1;
		while (p1 <= M && p2 <= R) {
			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[L + i] = help[i];
		}
		return ans;
	}

总结:如果我们求什么东西,就是左边和右边的和啊,左边和右边的有一定关系的这种,就往merge靠拢,这样思路就会清晰

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

普朗克的朗姆酒

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值