目录
思维导图链接
参考左程云算法课程
4.归并排序和几个经典题目分析 总览
题目1:归并排序法的实现
题目描述:
- 实现归并排序法,分别用递归法和非递归法
代码实现:
递归实现:
package class04;
import java.util.Arrays;
/**
*
* @author LiuS
* 归并排序多用于对象排序,可以稳定排序
* java库中,TimSort是改进的MergeSort,小于某值,用二分插入排序
* https://www.cnblogs.com/sunshuyi/p/12680918.html
* 小数据用插入原因是,深度已经不是主要影响因素,新建数组,两两比较复杂度会高于插入过程
*/
public class Code01_MergeSort {
// 小于此阀值,使用插入排序
private static final int MIN_MERGE = 32;
// 递归实现
public static <E extends Comparable<E>> void sort1(E[] arr) {
if(arr == null || arr.length < 2) return;
// 递归前,先将辅助数组准备好,减少每次递归重新开辟空间的消耗
E[] temp = Arrays.copyOf(arr, arr.length); // temp小标从0开始
// 递归函数
sort1(arr, 0, arr.length - 1, temp);
}
private static <E extends Comparable<E>>void sort1(E[] arr, int left, int right, E[] temp) {
// 1. 递归终止条件,代码优化,小数据用插入
if(right - left <= MIN_MERGE) {
insertionSort(arr, left, right);
return; // 千万别忘记返回了
}
// 2. 处理当前层
int mid = left + ((right - left) >> 1);
// 3. 将较大问题转为较小问题
sort1(arr, left, mid, temp);
sort1(arr, mid + 1, right, temp);
// 4. 现场恢复,将较小问题组合成较大问题
// 代码优化,排除左边均小于右边的情况
if(arr[mid].compareTo(arr[mid + 1]) > 0) {
merge(arr, left, mid, right, temp);
}
}
private static <E extends Comparable<E>>void merge(
E[] arr, int left, int mid, int right, E[] temp) {
// 1. 先定义好指针
int pL = left; // 左区间指针
int pR = mid + 1;// 右区间指针
int t = left; // 临时数组指针
// 2. 合并区间
// 先处理区间不越界情况
while(pL <= mid && pR <= right) {
temp[t ++] = arr[pL].compareTo(arr[pR]) <= 0
? arr[pL ++] : arr[pR ++];
}
//若区间越界,要么pL越界了,要么pR越界了
while(pL <= mid) {
temp[t ++] = arr[pL ++];
}
while(pR <= right) {
temp[t ++] = arr[pR ++];
}
// 3. 将temp数组元素copy回原数组指定区间位置
t = left;
while(left <= right) {
arr[left ++] = temp[t ++];
}
// 也可以用库函数
// System.arraycopy(temp, left, arr, left, right - left + 1);
}
// 插入排序法
private static <E extends Comparable<E>>void insertionSort(E[] arr, int left, int right) {
for(int i = left; i <= right; i ++) {
E target = arr[i];
int j = i;
for(; j - 1 >= left; j --) {
if(arr[j - 1].compareTo(target) > 0) {
arr[j] = arr[j - 1];
} else {
break;
}
}
arr[j] = target;
}
}
}
非递归实现:
// 非递归实现
public static <E extends Comparable<E>> void sort2(E[] arr) {
if(arr == null || arr.length < 2) return;
E[] temp = Arrays.copyOf(arr, arr.length);
int N = arr.length; // 边界条件
// 1. 定义步长,即每次merge要合并的左右区间长度,mid-left+1
int mergeSize = 1;
while(mergeSize < N) { // 步长不能越界
// 2. 定义好要处理的左区间起始指针,右区间尾指针
int pL = 0; // 要处理的左区间起始指针
while(pL < N) { // 指针要依次往右推进,不能越界
int mid = pL + mergeSize - 1; // 左区间最后一个元素
if(mid >= N) break; // 说明左组不够或没有右组,无需再与右组合并
// 右区间最后一个元素,可能越界,若越界就是区间最后一个元素
int pR = Math.min(mid + mergeSize, N - 1);
// 3. 将划分好的左右区间合并
merge(arr, pL, mid, pR, temp);
pL = pR + 1; // 依次处理下个要合并的左右区间
}
// 4. 维护mergeSize,依次处理更大的区间问题
// 防止数据范围是否溢出,即N在数据范围的边缘。mergeSize太靠近,*2会越界
if(mergeSize > N / 2) {
break; // 等于N/2时,还要处理,再扩大两倍,总的区间长度一定不小于N,所有数一定处理完了
}
mergeSize <<= 1;
}
}
代码测试:
public static void main(String[] args) {
int n = 1000000;
Integer[] arr = ArrayGenerator.generateRandomArray(n, n);
Integer[] arr1 = Arrays.copyOf(arr, arr.length);
SortingHelper.sortTime("MergeSort1", arr);
SortingHelper.sortTime("MergeSort2", arr1);
}
MergeSort1, n = 1000000 : 0.334222 s
MergeSort2, n = 1000000 : 0.282146 s
题目2:求数组小和
题目描述:
- 在一个数组中,一个数左边比它小的数的总和,叫数的小和,所有数的小和累加起来,叫数组小和。求数组小和。
例子: [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
代码实现:
import java.util.Arrays;
public class Code02_SmallSum {
public static int smallSum(int[] arr) {
if (arr == null || arr.length < 2) {
return 0;
}
int[] temp = Arrays.copyOf(arr, arr.length);
return smallSum(arr, 0, arr.length - 1, temp);
}
// arr[left..right]既要排好序,也要求小和返回
// 所有区间小数和累加,即,左区间,右区间,以及左右合并的大区间
private static int smallSum(int[] arr, int left, int right, int[] temp) {
if (left >= right) {
return 0; // 注意返回的结果是小数和,只有一个元素,为0
}
int mid = left + ((right - left) >> 1);
// 将所有区间统计的小数和累加,最终就是数组中所有数的小数和
return smallSum(arr, left, mid, temp)
+ smallSum(arr, mid + 1, right, temp)
+ merge(arr, left, mid, right, temp);
}
// merge不仅排序,排序是为了更好统计
// 还要统计出merge后的小数和
private static int merge(int[] arr, int left, int mid, int right, int[] temp) {
// 同样,先定义好指针
int pL = left; // 左区间指针,从左往右推
int pR = mid + 1; // 右区间指针,也是从左往右推
int t = left; // 辅助新数组下标指针
int res = 0; // 统计的小数和
// 传统的merger过程,只不过要增加统计功能
// 注意,左右相等时,与稳定排序不同,要放右边数,这样才能统计出左边数组成的所有小数和
while (pL <= mid && pR <= right) {
if (arr[pL] < arr[pR]) {
res += arr[pL] * (right - pR + 1); // pR右边数一定也大于pL
temp[t++] = arr[pL++]; // 处理左边的下个元素
} else {
temp[t++] = arr[pR++]; // 左边>=右边,当前左边数不组成小数和
}
}
while (pL <= mid) { // 右边已经遍历完,即左边剩下的数都比右边大
temp[t++] = arr[pL++];
}
while (pR <= right) { // 左边已经遍历完,即右边剩下数都比左边大
temp[t++] = arr[pR++]; // res无需再统计,遍历左边数时已经统计完了
}
// copy新数组
System.arraycopy(temp, left, arr, left, right - left + 1);
return res;
}
代码测试
// 比较器---暴力算法求小数和
public static int comparator(int[] arr) {
if (arr == null || arr.length < 2) {
return 0;
}
int res = 0;
for (int i = 1; i < arr.length; i++) {
for (int j = 0; j < i; j++) {
res += arr[j] < arr[i] ? arr[j] : 0;
}
}
return res;
}
// 随机数组生成器
public static int[] generateRandomArray(int maxSize, int maxValue) {
int[] arr = new int[(int) ((maxSize + 1) * Math.random())];
for (int i = 0; i < arr.length; i++) {
arr[i] = (int) ((maxValue + 1) * Math.random()) - (int) (maxValue * Math.random());
}
return arr;
}
// 辅助函数---copy数组
public static int[] copyArray(int[] arr) {
if (arr == null) {
return null;
}
int[] res = new int[arr.length];
for (int i = 0; i < arr.length; i++) {
res[i] = arr[i];
}
return res;
}
// 辅助函数---判断相等
public static boolean isEqual(int[] arr1, int[] arr2) {
if ((arr1 == null && arr2 != null) || (arr1 != null && arr2 == null)) {
return false;
}
if (arr1 == null && arr2 == null) {
return true;
}
if (arr1.length != arr2.length) {
return false;
}
for (int i = 0; i < arr1.length; i++) {
if (arr1[i] != arr2[i]) {
return false;
}
}
return true;
}
// 辅助函数---打印数组
public static void printArray(int[] arr) {
if (arr == null) {
return;
}
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i] + " ");
}
System.out.println();
}
// 代码测试
public static void main(String[] args) {
int testTime = 500000;
int maxSize = 100;
int maxValue = 100;
boolean succeed = true;
for (int i = 0; i < testTime; i++) {
int[] arr1 = generateRandomArray(maxSize, maxValue);
int[] arr2 = copyArray(arr1);
if (smallSum(arr1) != comparator(arr2)) {
succeed = false;
printArray(arr1);
printArray(arr2);
break;
}
}
System.out.println(succeed ? "scuccess!" : "error!");
}
scuccess!
题目3:求所有的逆序对
题目链接
题目描述:
- 在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。
输入一个数组,求出这个数组中的逆序对的总数。
示例:
输入: [7,5,6,4]
输出: 5
代码实现:
package class04;
import java.util.Arrays;
public class Code03_ReversePair {
// 本质上是求左边的数比当前的数大的个数有多少
// 因只需求个数,不求累加结果,以右边为基点即可
// 也可以以左边为基点,需要merge从右往左,相等时先处理右边,则稳定性不能保证
// 若本题求大数和,必须以左边为基点,与求小数和逻辑类似
public int reversePairs(int[] nums) {
if(nums == null || nums.length < 2) return 0;
int[] temp = Arrays.copyOf(nums, nums.length);
return reversePairs(nums, 0, nums.length - 1, temp);
}
// 递归实现排序,并返回逆序对个数,即大数个数
// 同样,所有区间个数累加,即,左区间,右区间,以及左右合并的大区间
private int reversePairs(int[] nums, int left, int right, int[] temp) {
if(left >= right) return 0;
int mid = left + ((right - left) >> 1);
return reversePairs(nums, left, mid, temp)
+ reversePairs(nums, mid + 1, right, temp)
+ (nums[mid] > nums[mid + 1] ? merge(nums, left, mid, right, temp) : 0);
}
// 以右边区间元素为基点,从左往右推
private int merge1(int[] nums, int left, int mid, int right, int[] temp) {
int pL = left;
int pR = mid + 1;
int t = left;
int res = 0;
while(pL <= mid && pR <= right) {
if(nums[pL] <= nums[pR]) { // 不构成逆序对
temp[t ++] = nums[pL ++];
} else { // 左大于右,左区间之后的元素与右边当前元素都构成逆序对
res += mid - pL + 1;
temp[t ++] = nums[pR ++];
}
}
while(pL <= mid) temp[t ++] = nums[pL ++];
while(pR <= right) temp[t ++] = nums[pR ++];
System.arraycopy(temp, left, nums, left, right - left + 1);
return res;
}
// 以左边区间元素为基点,从右往左推
private int merge(int[] nums, int left, int mid, int right, int[] temp) {
int pL = mid; // 左指针为左区间最后一个元素
int pR = right; // 右指针为右区间最后一个元素
int t = right; // 从右往左merge
int res = 0;
// merge从右往左,从大往小填
while(pL >= left && pR >= mid + 1) {
if(nums[pL] > nums[pR]) { // 左大,有逆序对,且右边往mid靠近的数都比左小
res += pR - mid; // 即此时左边的a是所有那些数的大数,若是求和,a*Na
temp[t --] = nums[pL --];
} else { // 左小,没有逆序对,相等也将右边元素先填入temp中
temp[t --] = nums[pR --];
}
}
while(pL >= left) temp[t --] = nums[pL --];
while(pR >= mid + 1) temp[t --] = nums[pR --];
System.arraycopy(temp, left, nums, left, right - left + 1);
return res;
}
}
题目4:求数组中的大两倍对数量
题目描述:
-
在一个数组中,
对于每个数num,求有多少个后面的数 * 2 依然<num,求总个数比如:[3,1,7,0,2]
3的后面有:1,0
1的后面有:0
7的后面有:0,2
0的后面没有
2的后面没有
所以总共有5个
代码实现:
package class04;
import java.util.Arrays;
public class Code04_BiggerThanRightTwice {
public static int biggerTwice(int[] arr) {
if (arr == null || arr.length < 2)
return 0;
int[] temp = Arrays.copyOf(arr, arr.length);
return biggerTwice(arr, 0, arr.length - 1, temp);
}
private static int biggerTwice(int[] arr, int left, int right, int[] temp) {
if (left >= right)
return 0;
int mid = left + ((right - left) >> 1);
// 注意,不能对merge条件判断,若是负数,左小于右,但右乘2就小于左了
return biggerTwice(arr, left, mid, temp)
+ biggerTwice(arr, mid + 1, right, temp)
+ merge2(arr, left, mid, right, temp);
}
// 统计和排序
// 以左基点进行统计
private static int merge(int[] arr, int left, int mid, int right, int[] temp) {
int pL = left;
int pR = mid + 1;
int t = left;
int res = 0;
// 1. 统计
while (pL <= mid) {
while (pR <= right && arr[pL] > (arr[pR] * 2)) { // 找到第一个不是的为止
pR++; // 找第一个不满足的位置
}
// 目前囊括进来的数,是从[mid+1, pR)
res += pR - mid - 1;
pL++;
}
// 2. merge排序
pL = left; // 变量复原
pR = mid + 1;
while (pL <= mid && pR <= right) {
temp[t++] = arr[pL] <= arr[pR] ? arr[pL++] : arr[pR++];
}
while (pL <= mid)
temp[t++] = arr[pL++];
while (pR <= right)
temp[t++] = arr[pR++];
System.arraycopy(temp, left, arr, left, right - left + 1);
return res;
}
// 以右基点进行统计
private static int merge2(int[] arr, int left, int mid, int right, int[] temp) {
int res = 0;
int pL = left;
int pR = mid + 1;
int t = left;
// 1. 统计
// 目前囊括进来的数,是从[pL, mid]
while(pR <= right) {
while(pL <= mid && arr[pL] <= arr[pR] * 2) { // 等于2倍不满足
pL ++; // 找第一个满足的位置
}
res += mid - pL + 1; // 左:0.1.2.3.4,3满足条件,有两个
pR ++;
}
// 2. merge排序
pL = left; // 变量复原
pR = mid + 1;
while (pL <= mid && pR <= right) {
temp[t++] = arr[pL] <= arr[pR] ? arr[pL++] : arr[pR++];
}
while (pL <= mid)
temp[t++] = arr[pL++];
while (pR <= right)
temp[t++] = arr[pR++];
System.arraycopy(temp, left, arr, left, right - left + 1);
return res;
}
}
代码测试
// 比较器----暴力算法
public static int comparator(int[] arr) {
int ans = 0;
for (int i = 0; i < arr.length; i++) {
for (int j = i + 1; j < arr.length; j++) {
if (arr[i] > (arr[j] << 1)) {
ans++;
}
}
}
return ans;
}
// 比较器---随机数组生成
public static int[] generateRandomArray(int maxSize, int maxValue) {
int[] arr = new int[(int) ((maxSize + 1) * Math.random())];
for (int i = 0; i < arr.length; i++) {
arr[i] = (int) ((maxValue + 1) * Math.random()) - (int) ((maxValue + 1) * Math.random());
}
return arr;
}
// 辅助函数,copy
public static int[] copyArray(int[] arr) {
if (arr == null) {
return null;
}
int[] res = new int[arr.length];
for (int i = 0; i < arr.length; i++) {
res[i] = arr[i];
}
return res;
}
// 辅助函数---打印数组
public static void printArray(int[] arr) {
if (arr == null) {
return;
}
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i] + " ");
}
System.out.println();
}
// 代码测试
public static void main(String[] args) {
int testTime = 500000;
int maxSize = 5;
int maxValue = 100;
System.out.println("测试开始");
for (int i = 0; i < testTime; i++) {
int[] arr1 = generateRandomArray(maxSize, maxValue);
int[] arr2 = copyArray(arr1);
int res1 = biggerTwice(arr1);
int res2 = comparator(arr2);
if (res1 != res2) {
System.out.println("Oops!");
printArray(arr1);
System.out.println("my: " + res1);
printArray(arr2);
System.out.println("to: " + res2);
break;
}
}
System.out.println("测试结束");
}
测试开始
测试结束
题目5:区间和的个数
题目链接
题目描述:
-
给你一个整数数组 nums 以及两个整数 lower 和 upper 。
求数组中,值位于范围 [lower, upper] (包含 lower 和 upper)之内的 区间和的个数 。区间和 S(i, j) 表示在 nums 中,位置从 i 到 j 的元素之和,包含 i 和 j (i ≤ j)。
例如:
输入:nums = [-2,5,-1], lower = -2, upper = 2
输出:3
解释:存在三个区间:[0,0]、[2,2] 和 [0,2] ,对应的区间和分别是:-2 、-1 、2
代码实现:
package class04;
/**
* 求数组中所有区间的和在指定区间内的个数
* @author ls
*
*/
public class Code05_CountOfRangeSum {
public int countRangeSum(int[] nums, int lower, int upper) {
if(nums == null || nums.length == 0) return 0;
long[] temp = new long[nums.length]; // 对前缀和数组排序
// 1. 建立preSum前缀和数组,数据有可能越界,转为long型
long[] preSum = new long[nums.length];
preSum[0] = nums[0];
for(int i = 1; i < nums.length; i ++) {
preSum[i] = preSum[i - 1] + nums[i];
}
return countRangeSum(preSum, 0, nums.length -1, lower, upper, temp);
}
// 2. 递归实现,转为求preSum中[x-upper,x-lower]问题
private int countRangeSum(long[] preSum, int left, int right, int lower, int upper, long[] temp) {
if(left == right) { // 分到只有一个元素时,要判断其是否满足指标
return preSum[left] <= upper && preSum[left] >= lower ? 1 : 0;
}
int mid = left + ((right - left) >> 1);
// 累加上各个区间统计的结果
return countRangeSum(preSum, left, mid, lower, upper, temp)
+ countRangeSum(preSum, mid + 1, right, lower, upper, temp)
+ merge(preSum, left, mid, right, lower, upper, temp);
}
// 3. merge中统计满足新指标的个数
private int merge(long[] preSum, int left, int mid, int right, int lower, int upper, long[] temp) {
// 先统计
int res = 0;
int pR = mid + 1; // 右区间起始指针,依次往右推进
int windowsL = left; // 左区间窗口首指针
int windowsR = left; // 左区间窗口尾指针
// [windowsL, windowsR),左闭右开,尾指针不满足条件,初始区间为空
while(pR <= right) {
// 计算好新的指标[x-upper, x-lower]
long min = preSum[pR] - upper;
long max = preSum[pR] - lower;
while(windowsR <= mid && preSum[windowsR] <= max) {
windowsR ++; // windowsR满足时一直往右推,找到第一个不满足的为止
} // windowsR前一个数满足,本身不满足
while(windowsL <= mid && preSum[windowsL] < min) {
windowsL ++; // windowsL不满足时一直往右推,找到第一个满足的为止
} // windowsL当前数满足>=min,跳出循环
res += windowsR - windowsL; //[14,14)不满足时为0
pR ++;
}
// 再排序
int t = left;
int pL = left;
pR = mid + 1;
while(pL <= mid && pR <= right) {
temp[t ++] = preSum[pL] <= preSum[pR] ?
preSum[pL ++] : preSum[pR ++];
}
while(pL <= mid) temp[t ++] = preSum[pL ++];
while(pR <= right) temp[t ++] = preSum[pR ++];
System.arraycopy(temp, left, preSum, left, right - left + 1);
return res;
}
// 测试
public static void main(String[] args) {
int[] nums = {-2, 5, -1};
int lower = -2;
int upper = 2;
System.out.println(new Code05_CountOfRangeSum().countRangeSum(nums, lower, upper)); // 3
}
}