归并排序与随机快排
归并排序
- 整体是递归,左边排好序+右边排好序+merge让整体有序
- 让其整体有序的过程里用了排外序方法
- 利用master公式求解时间复杂度
- 非递归实现
划分的M其实就是左边的右边界,所以进入递归时是process(arr,L,M),最后merge的判断条件都是<=M和<=R,这与快排的划分,左边小于划分值,右边大于划分值,划分值不再需要排序不同。
递归实现:
public static void mergeSort1(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
process(arr, 0, arr.length - 1);
}
// arr[L...R]范围上,变成有序的
// L...R N T(N) = 2*T(N/2) + O(N) -> 套master公式
public static void process(int[] arr, int L, int R) {
if (L == R) { // base case
return;
}
int mid = L + ((R - L) >> 1);
// 排左边
process(arr, L, mid);
// 排右边
process(arr, mid + 1, R);
// 合并
merge(arr, L, mid, R);
}
public static void merge(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;
while (p1 <= M && p2 <= R) {
help[i++] = arr[p1] <= arr[p2] ? arr[p1++] : arr[p2++];
}
// 要么p1越界了,要么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];
}
}
非递归实现:
public static void mergeSort2(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
int N = arr.length;
int mergeSize = 1;// 一组是2倍的mergeSize,当前有序的,左组长度
while (mergeSize < N) { // log N
int L = 0;
// 0....
while (L < N) {
// L...M 左组(mergeSize)
int M = L + mergeSize - 1;
if (M >= N) { // 这种情况剩下的数字肯定有序
break;
}
// L...M M+1...R(mergeSize) 有可能右组凑不齐
int R = Math.min(M + mergeSize, N - 1);
merge(arr, L, M, R);
L = R + 1;
}
if (mergeSize > N / 2) { // 防止溢出
break;
}
mergeSize <<= 1;
}
}
归并排序在每次归并的时候将比较结果保留下来,因此复杂度降低为
例子:在一个数组中,一个数左边比它小的数的总和,叫数的小和,所有数的小和累加起来,叫做组小和。求数组小和。
[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右边有四个比它大的数,那么就是由四个1, 以此类推就是有两个3,一个4,一个2
归并就是分成小组去找有几个大的后逐渐扩大小组大小,自己的组内是不会再找了,每次都是从新的右组里去找,拷贝时相等要先拷贝右组
public static int smallSum(int[] arr) {
if (arr == null || arr.length < 2) {
return 0;
}
return process(arr, 0, arr.length - 1);
}
// arr[L..R]既要排好序,也要求小和返回
// 所有merge时,产生的小和,累加
// 左 排序 merge
// 右 排序 merge
// 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)
+
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 = 0;
int p1 = L;
int p2 = m + 1;
int res = 0;
while (p1 <= m && p2 <= r) {
// 利用右边有序,根据序号直接算出有几个arr[p1]
res += arr[p1] < arr[p2] ? (r - p2 + 1) * arr[p1] : 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 (i = 0; i < help.length; i++) {
arr[L + i] = help[i];
}
return res;
}
每一个数有多少比它大,小都可以用归并排序来做
题目如果是小于等于的话,在判断条件上要改成小于等于,同时等于的情况下要先把左边的放入help数组中,先放右边会缺失比较的数据,造成最终值小于正确值
例如,35,35,35,35
记整个数组为35[1],35[2],35[3],35[4]
如果还是先放右边,那么p2会指向35[4],p1还是指向35[1]
少了35[2]与后续数字的比较
res += arr[p1] <= arr[p2] ? arr[p1] * (r - p2 + 1) : 0;
help[i++] = arr[p1] <= arr[p2] ? arr[p1++] : arr[p2++];
快速排序
给定一个数组arr,和一个整数num。请把小于num的数放在数组的左边,大于num的数放在 数组的右边。要求额外空间复杂度O(1),时间复杂度O(N)
划分小于等于区,每次有比这个数小的,就与小于等于区的下一个数交换,小于等于区扩大一个,继续往下走
升级问题荷兰旗问题,将数组划分为小区,等于区,大区
1) [i] == num, i ++
2) [i] < num, [i] 与< 区右一个交换,<区向右扩,i++
3) [i] > num, [i] 与> 区左一个交换,> 区向左扩,i 不变
4) i 与 > 区碰到时停止
// arr[L...R] 玩荷兰国旗问题的划分,以arr[R]做划分值
// <arr[R] ==arr[R] > arr[R]
public static int[] netherlandsFlag(int[] arr, int L, int R) {
if (L > R) {
return new int[] { -1, -1 };
}
if (L == R) {
return new int[] { L, R };
}
int less = L - 1; // < 区 右边界
int more = R; // > 区 左边界
int index = L;
while (index < more) {
if (arr[index] == arr[R]) {
index++;
} else if (arr[index] < arr[R]) {
swap(arr, index++, ++less);
} else { // >
swap(arr, index, --more);
}
}
// 因为选的arr[R] 最后把这个数换到等于区
swap(arr, more, R);
return new int[] { less + 1, more };
}
快排1.0
划分小区,大区,固定一个数位置
public static int partition(int[] arr, int L, int R) {
if (L > R) {
return -1;
}
if (L == R) {
return L;
}
int lessEqual = L - 1;
int index = L;
while (index < R) {
if (arr[index] <= arr[R]) {
swap(arr, index, ++lessEqual);
}
index++;
}
swap(arr, ++lessEqual, R);
return lessEqual;
}
public static void quickSort1(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
process1(arr, 0, arr.length - 1);
}
public static void process1(int[] arr, int L, int R) {
if (L >= R) {
return;
}
// L..R partition arr[R] [ <=arr[R] arr[R] >arr[R] ]
int M = partition(arr, L, R);
process1(arr, L, M - 1);
process1(arr, M + 1, R);
}
快排2.0
划分小区,大区,固定值相等的位置
public static void quickSort2(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
process2(arr, 0, arr.length - 1);
}
public static void process2(int[] arr, int L, int R) {
if (L >= R) {
return;
}
int[] equalArea = netherlandsFlag(arr, L, R);
process2(arr, L, equalArea[0] - 1);
process2(arr, equalArea[1] + 1, R);
}
上面两个时间复杂度是
快排3.0
随机选一个划分值
public static void quickSort3(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
process3(arr, 0, arr.length - 1);
}
public static void process3(int[] arr, int L, int R) {
if (L >= R) {
return;
}
swap(arr, L + (int) (Math.random() * (R - L + 1)), R);
int[] equalArea = netherlandsFlag(arr, L, R);
process3(arr, L, equalArea[0] - 1);
process3(arr, equalArea[1] + 1, R);
}
1)通过分析知道,划分值越靠近中间,性能越好;越靠近两边,性能越差
2)随机选一个数进行划分的目的就是让好情况和差情况都变成概率事件
3)把每一种情况都列出来,会有每种情况下的时间复杂度,但概率都是1/N
4)那么所有情况都考虑,时间复杂度就是这种概率模型下的长期期望!
时间复杂度O(N*logN),额外空间复杂度O(logN)都是这么来的
题目练习: