归并排序整体流程:
时间复杂度 O(N * logN)
代码实现(java)
包含递归和非递归
public class MergeSort {
public static void main(String[] args) {
int[] arr = new int[] {4,1,2,5,7,2,1,7,9,3};
for (int i : arr) {
System.out.print(i + " ");
}
mergeSort2(arr);
System.out.println();
for (int i : arr) {
System.out.print(i + " ");
}
}
/**
* 递归方式的归并排序
* @param arr
*/
public static void mergeSort1(int[] arr) {
if (arr == null) return;
process(arr, 0, arr.length - 1);
}
public static void process(int[] arr, int left, int right) {
// 只有一个元素,返回
if (left == right) return;
int mid = left + (right - left) / 2;
// 先递归调用process,对数组进行拆分,拆分到只剩下2个元素开始,
// 一步一步进行排序,然后归并起来在进行进一步的排序,知道整个数组排序好
process(arr, left, mid);
process(arr, mid + 1, right);
// 对当前 left 到 right 进行排序
merge(arr, left, mid, right);
}
/**
* 非递归的归并排序
* @param arr
*/
public static void mergeSort2(int[] arr) {
if (arr == null || arr.length < 2) return;
int N = arr.length;
// 步长,从1开始
int mergeSize = 1;
while (mergeSize < N) {
int L = 0; // 左组的起始位置
while (L < N) {
if (mergeSize >= N - L) { // 右组已经不够了,直接跳过
break;
}
// 左组的终点
int M = L + mergeSize - 1;
// 右组的终点, 不是M+步长 就是不够的情况到达数组终点N-1
int R = M + Math.min(mergeSize, N - M - 1);
merge(arr, L, M, R);
L = R + 1;
}
// 避免步长越界(数字最大值)
// 如果下次步长扩大后,已经大于N,就不可能还可以进行merge了,直接跳出即可
if (mergeSize > N / 2) {
break;
}
mergeSize <<= 1;
}
}
public static void merge(int[] arr, int left, int mid, int right) {
int[] temp = new int[right - left + 1];
int i = 0;
// 前半部分的头
int L = left;
// 后半部分的头
int M = mid + 1;
// 合并
while (L <= mid && M <= right) {
temp[i++] = arr[L] > arr[M] ? arr[M++] : arr[L++];
}
while (L <= mid) {
temp[i++] = arr[L++];
}
while (M <= right) {
temp[i++] = arr[M++];
}
// 将排好序的部分复制回原数组
for (int j = 0; j < temp.length; j++) {
arr[left + j] = temp[j];
}
}
}
面试题
1. 小和问题
在一个数组中,每一个数左边比当前数小的数累加起来,叫做这个数组的小和。求一个数组的小和。
例:[1,3,4,2,5]
1左边比1小的数:0
3左边比3小的数:1
4左边比4小的数:1 + 3 = 4
2左边比2小的数:1
5左边比5小的数:1 + 3 + 4 + 2 = 10
所以小和为1+4+1+10=16
要求时间复杂度为 O(N * logN)
思路
其实就是统计右组中有多少个数大于左组中的较小的数(左组中不产生小和,只有和右组比较时才产生小和,所以统计时不会有重复情况出现,每次都是和最新的右组范围进行比较,也就是每次都和原数组中更靠右的指定范围内的元素进行比较)
- 每次进行归并排序的时候,对当前次的排序进行判断
- 因为前半部分和后半部分都是有序的,所以判断如果前半部分中的一个数小于后半部分中的某一个数,那么前半部分中这个数,会比后半部分中这个数出现位置及之后的位置都小(有序),所以统计次数并相加到结果中
- 最终在递归过程中,将每次归并时统计的和相加,就是小和结果
注意:如果发现相等,则先拷贝右边的数,因为左边的指针不能先动,要先找到右边比左边大的数才能动,这样才能不漏掉小数;否则左边先动,右边可能有比它大的数时,左指针已经移动,会错过
代码实现
public class SmallSum {
public static int smallSum(int[] arr) {
if (arr == null || arr.length < 2) {
return 0;
}
return process(arr, 0 , arr.length - 1);
}
private static int process(int[] arr, int L, int R) {
if (L == R) return 0;
int mid = L + (R - L) / 2;
return process(arr, L, mid) + process(arr, mid + 1, R) + merge(arr, L, mid, R);
}
private static int merge(int[] arr, int l, int mid, int r) {
int[] help = new int[r - l + 1];
int i = 0;
int p1 = l;
int p2 = mid + 1;
int res = 0;
while (p1 <= mid && p2 <= r) {
res += arr[p1] < arr[p2] ? (r - p2 + 1) * arr[p1] : 0;
help[i++] = arr[p1] < arr[p2] ? arr[p1++] : arr[p2++];
}
while (p1 <= mid) {
help[i++] = arr[p1++];
}
while (p2 <= r) {
help[i++] = arr[p2++];
}
for (int j = 0; j < help.length; j++) {
arr[l + j] = help[j];
}
return res;
}
public static void main(String[] args) {
int[] arr = {1,2,3,4};
System.out.println(process(arr, 0, arr.length - 1)); // 10 = 1 + 3 + 6
}
}
2. 数组中的逆序对
思路
求逆序对,其实就是求一个数前面有几个数是大于这个数的;恰好与小和问题相反
- 我们使用归并排序,每次取出左右两部分,然后都从后向前进行遍历判断
- 如果左半部分出现大于右半部分的数,说明这个数大于右半部分的对应数及之前的数,所以就可以将右半部分较小的数及之前数的个数累加到结果中(因为左右都有序)
- 最终递归的得到个数即可
代码实现
public class ReversePairs {
public static int reversePairs(int[] nums) {
if (nums == null || nums.length < 2) {
return 0;
}
return process(nums, 0, nums.length - 1);
}
private static int process(int[] nums, int left, int right) {
if (left == right) return 0;
int mid = left + (right - left) / 2;
return process(nums, left, mid) + process(nums, mid + 1, right) + meger(nums, left, mid, right);
}
private static int meger(int[] nums, int left, int mid, int right) {
int[] help = new int[right - left + 1];
int p1 = mid;
int p2= right;
int res = 0;
int i = help.length - 1;
while (p1 >= left && p2 > mid) {
res += nums[p1] > nums[p2] ? p2 - mid : 0;
help[i--] = nums[p1] > nums[p2] ? nums[p1--] : nums[p2--];
}
while (p1 >= left) {
help[i--] = nums[p1--];
}
while (p2 > mid) {
help[i--] = nums[p2--];
}
for (int j = 0; j < help.length; j++) {
nums[left + j] = help[j];
}
return res;
}
public static void main(String[] args) {
int[] arr = {7,5,6,4};
System.out.println(process(arr, 0 , arr.length - 1));
}
}
3. 数组中一个数num的右边有多少数*2后依然小于num
思路
每次归并排序,遍历左半部分,找右半部分不能满足题目要求的位置,该位置之前的数都满足,累加到结果中
代码实现
public class BiggerThanRightTwice {
public static int biggerTwice(int[] arr) {
if (arr == null || arr.length < 2) {
return 0;
}
return process(arr, 0, arr.length - 1);
}
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);
}
private static int merge(int[] arr, int left, int mid, int right) {
int res = 0;
// 先计算
// 目前囊括进来的数,是从[M+1, windowR),windowR取不到,不被包括结果
int windowR = mid + 1;
for (int i = left; i <= mid; i++) {
while (windowR <= right && arr[i] > (arr[windowR] * 2)) {
windowR++;
}
res += windowR - mid - 1;
}
// 再排序
int[] help = new int[right - left + 1];
int i = 0;
int p1 = left;
int p2 = mid + 1;;
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 res;
}
public static void main(String[] args) {
int[] arr = {6,7,1,3,2};
System.out.println(biggerTwice(arr)); // 5
}
}