归并排序(Merge Sort)就是利用归并的思想实现排序方法。它的原理是假设初始序列含义n个记录,则可以看成是n个有序子序列,每个序列的长度为1,然后两两归并,得到【n/2】([x]表示不小于x的最小整数)个长度为2或1的有序咨询;再两两归并.......;如此重复,知道得到一个长度为n的有序序列为止,这种排序方法成为2路归并排序。
下面看一张图片,可以帮助我们更好的理解归并排序:
左侧是数组的初步拆分过程,右侧是逐步合并过程,并最终得到一个有序序列。
代码如下:
package code2.排序_03;
/**
* 归并排序
*/
public class Code01_MergeSort {
public void printArray(int[] arr) {
if (arr == null) {
return;
}
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i] + " ");
}
System.out.println();
}
private void process (int[] arr, int left, int right)
{
if (left == right) {
return;
}
int mid = (left + right) >> 1;
process(arr, left, mid);
process(arr, mid + 1, right);
merge(arr, left, mid, right);
}
private void merge (int[] arr, int left,int mid, int right)
{
int[] help = new int[right - left + 1];
int p1 = left;
int p2 = mid +1;
int i = 0;
while (p1 <= mid && p2 <= right) {
help[i++] = arr[p1] < arr[p2] ? arr[p1++] : arr[p2++];
}
while (p1 <= mid) {
help[i++] = arr[p1++];
}
while (p2 <= right) {
help[i++] = arr[p2++];
}
for (int j =0; j < help.length; j++) {
arr[left+j] = help[j];
}
}
public static void main(String[] args) {
Code01_MergeSort sort = new Code01_MergeSort();
int[] arr = {8,6,7,9,10,5,7,3,2};
sort.printArray(arr);
sort.process(arr, 0, arr.length-1);
System.out.println("排序后:");
sort.printArray(arr);
}
}
如果代码看的有些吃力,可以结合下面我手绘的归并排序的过程进行理解
只会个归并排序,其实没啥意义。不仅仅是归并排序,任何算法都是一样的,我们必须要能够掌握原理,灵活运用才行。下面来看通过归并排序延伸出来的面试题。
面试题一:最小和问题
在一个数组中,一个数左边比它小的数的总和,叫数的小和,所有数的小和累加起来,叫数组小和。求数组小和。
例子: [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. 普通两层遍历肯定是可以解出这道题的,但是两层遍历的时间的复杂度是O(N^2). 而归并排序的时间复杂度是 N*logN, 性能上更优。
2. 找到每个数左侧的比这个数小的数进行求和。变相也就是从左到右,找到当前数右侧比自己大的数出现了几次,出现一次,自己加一次。举个例子: 如果有序数组是{1,2,3,4}. 那么在我们从左到右遍历的时候,当前值为1,那么有3个值是比1大,因此1+1+1. 当前数为2时,有2个数比2大,那么 2 + 2. 如果当前数为3,值有一个数比3大,因此保留3. 最终的结果是1+1+1+2+2+3 = 10. 那么最终的最小和尾10. 代码如下:
package code2.排序_03;
/**
* 在一个数组中,一个数左边比它小的数的总和,叫数的小和,所有数的小和累加起来,叫数组小和。求数组小和。
* 例子: [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
*/
public class Code02_SmallSum {
private int process (int[] arr, int left, int right)
{
if (left == right) {
return 0;
}
int mid = (left + right) >> 1;
return process(arr, left, mid) + process(arr, mid + 1, right) + merge(arr, left, mid, right);
}
private int merge (int[] arr, int left,int mid, int right)
{
int[] help = new int[right - left + 1];
int p1 = left;
int p2 = mid +1;
int i = 0;
int result = 0;
while (p1 <= mid && p2 <= right) {
result += arr[p1] < arr[p2] ? (right-p2+1)*arr[p1] : 0;
help[i++] = arr[p1] > arr[p2] ? arr[p1++] : arr[p2++];
}
while (p1 <= mid) {
help[i++] = arr[p1++];
}
while (p2 <= right) {
help[i++] = arr[p2++];
}
for (int j =0; j < help.length; j++) {
arr[left+j] = help[j];
}
return result;
}
public static void main(String[] args) {
Code02_SmallSum sort = new Code02_SmallSum();
int[] arr = {5,2,3,4,1};
int smallSum =sort.process(arr, 0, arr.length-1);
System.out.println(smallSum);
}
}
面试题2:逆序对
在一个数组中,
任何一个前面的数a,和任何一个后面的数b,
如果(a,b)是降序的,就称为逆序对
返回数组中所有的逆序对
解题思路:上一题是找右侧比自己大的数,这一题则是找有侧比自己小的数。思路相同
package code2.排序_03;
/**
*在一个数组中,
* 任何一个前面的数a,和任何一个后面的数b,
* 如果(a,b)是降序的,就称为逆序对
* 返回数组中所有的逆序对
*/
public class Code03_ReverseParis {
private int process (int[] arr, int left, int right)
{
if (left == right) {
return 0;
}
int mid = (left + right)/2;
return process(arr, left, mid) + process(arr, mid + 1, right) + merge(arr, left, mid, right);
}
private int merge (int[] arr, int left,int mid, int right)
{
int[] help = new int[right - left + 1];
int p1 = left;
int p2 = mid +1;
int i = 0;
int result = 0;
while (p1 <= mid && p2 <= right) {
result += arr[p1] < arr[p2] ? 0 : (right - p2 + 1);
help[i++] = arr[p1] < arr[p2] ? arr[p2++] : arr[p1++];
}
while (p1 <= mid) {
help[i++] = arr[p1++];
}
while (p2 <= right) {
help[i++] = arr[p2++];
}
for (int j =0; j < help.length; j++) {
arr[left+j] = help[j];
}
return result;
}
public static void main(String[] args) {
Code03_ReverseParis sort = new Code03_ReverseParis();
int[] arr = {3,8,4,1,0};
int num =sort.process(arr, 0, arr.length-1);
System.out.println(num);
}
}
上面2道题只是开胃菜,递归排序的经典写法都是从左到右进行递归。不知道你们发现没有,想要从左到右,找到右侧比自己大的数,得用升序归并。从左到右想要找到比自己小的数,得用降序归并。
思考: 面试题一是最小和,假设数组为 {5,2,3,4,1},而你使用降序,猜猜得到的最小和会是多少?为什么呢?
面试题3 (Hard):在一个数组中,对于每个数num,求有多少个后面的数 * 2 依然
比如:[3,1,7,0,2]
3的后面有:1,0
1的后面有:0
7的后面有:0,2
0的后面没有
2的后面没有
所以总共有5个
理解不了归并排序,相信这一题会直接懵逼。
package code2.排序_03;
/**
*在一个数组中,
* 对于每个数num,求有多少个后面的数 * 2 依然 (long) arr[windowR] * 2) {
windowR++;
}
result += windowR - mid - 1;
}
int[] help = new int[right - left + 1];
int p1 = left;
int p2 = mid +1;
int i = 0;
while (p1 <= mid && p2 <= right) {
help[i++] = arr[p1] < arr[p2] ? arr[p1++] : arr[p2++];
}
while (p1 <= mid) {
help[i++] = arr[p1++];
}
while (p2 <= right) {
help[i++] = arr[p2++];
}
for (int j =0; j < help.length; j++) {
arr[left+j] = help[j];
}
return result;
}
public static void main(String[] args) {
Code04_BiggerThanRightTwice sort = new Code04_BiggerThanRightTwice();
int[] arr = {3,1,7,0,2};
int num =sort.process(arr, 0, arr.length-1);
System.out.println(num);
}
}
面试题4 (Super Hard):题目描述:https://leetcode.cn/problems/count-of-range-sum/ 给定一个数组arr,两个整数lower和upper,返回arr中有多少个子数组的累加和在[lower,upper]范围上。
package unit2.class05;
// 这道题直接在leetcode测评:
// https://leetcode.com/problems/count-of-range-sum/
public class Code01_CountOfRangeSum {
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;
}
}
这一题属于相当难的,涉及到前缀和相关知识。后续会更新解题思路