一、归并排序
- 整体就是一个简单递归,左边排好序、右边排好序、让其整体有序
- 让其整体有序的过程里用了排外序方法
- 利用master公式来求解时间复杂度
- 归并排序的实质
时间复杂度O(N*logN),额外空间复杂度O(N)
选择、冒泡、插入 的时间复杂度高O(N^2)因为浪费了许多比较行为
递归,L到R范围排好序,求出中点位置,先让左侧有序,再让右侧有序,最后合并两侧
合并思想:123 256
准备一个辅助空间,谁小拷贝谁进辅助空间,辅助空间有序之后再拷贝会原数组达到原数组有序,相等时保证右组先拷贝
左侧有一个下标指向1,右侧有一个下标指向2,如果左侧小于等于右侧先拷贝左侧到合并的数组中,如果不小于等于右侧就拷贝右侧的数组中。
public static void mergeSort(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
mergeSort(arr, 0, arr.length - 1);
}
public static void process(int[] arr, int L,int R){
if(L == R){
return;
}
int mid = L + ((R - L) >> 1);
process(arr, L,mid);
process(arr, mid + 1,R);
merge(arr, L, mid,R);
}
public static void mergeSort(int[] arr, int l, int r) {
if (l == r) {
return;
}
int mid = l + ((r - l) >> 1);
mergeSort(arr, l, mid);
mergeSort(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++];
}
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];
}
}
归并排序的扩展
1.小和问题
在一个数组中,每一个数左边比当前数小的数累加起来,叫做这个数组的小和。求一个数组的小和。
更换思路,求一个数右边有多少个数比它大。
利用归并排序时生成左右两个有序数组,当右侧数比左侧数大,使用下标差计算有多少个数比左侧数大,记录下来
当左侧数比右侧大时,记录下来,当左右相等时,要先拷贝右组的数并且不产生小和,当数组合并完成时可以得出小和。
public static int smallSum(int[] arr) {
if (arr == null || arr.length < 2) {
return 0;
}
return mergeSort(arr, 0, arr.length - 1);
}
public static int mergeSort(int[] arr, int l, int r) {
if (l == r) {
return 0;
}
int mid = l + ((r - l) >> 1);
return mergeSort(arr, l, mid)
+ mergeSort(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) {
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;
}
2.逆序对问题
在一个数组中,左边的数如果比右边的数大,则这两个数构成一个逆序对,请打印所有逆序对。
等同于小和问题,都是由mergeSort改写的题
荷兰国旗问题
问题一
给定一个数组arr,和一个数num,请把小于等于num的数放在数 组的左边,大于num的 数放在数组的右边。要求额外空间复杂度O(1),时间复杂度O(N)
选定小于等于区,如果指针位置值小于等于num值,将指针位置值与小于等于区下一个值交换,小于等于区向前移动,i++;
问题二(荷兰国旗问题)
给定一个数组arr,和一个数num,请把小于num的数放在数组的 左边,等于num的数放 在数组的中间,大于num的数放在数组的 右边。要求额外空间复杂度O(1),时间复杂度O(N)
public static int[] partition(int[] arr, int l, int r, int p) {
int less = l - 1;
int more = r + 1;
while (l < more) {
if (arr[l] < p) {
swap(arr, ++less, l++);
} else if (arr[l] > p) {
swap(arr, --more, l);
} else {
l++;
}
}
return new int[] { less + 1, more - 1 };
}
要么让 i 往左走压缩待定区域,让小于区域推着等于区域奔向大于区域
要么让 i 发货到大于区域,让大于区域往左扩,来压缩待定区域
二、快速排序
快排1.0
选择数组最后一个数字作为num,让数组中最后一个数字之前做到小于等于 num 的数放在左边,大于 num 的数放在右边;让num和大于区域的第一个数做交换,使小于等于 num 区域扩充,右侧全是大于等于 num,这样 num 就排好了,让左侧和右侧递归的进行这个行为
快排2.0
每次搞定中间一批数,将等于 num 的都放到中间,一次确定所有等于 num 的数
在数组有序时1.0版本和2.0版本快速排序最差情况的时间复杂度退化到 O(N^2)
出现这种情况的原因是划分池打的很偏
快排3.0
从数组中随机选择一个数,将它放到最后一个数没然后将它当成 num 来划分,小于放左边,等于放中间,大于放右边。
public static void quickSort(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
quickSort(arr, 0, arr.length - 1);
}
//arr[l..r]排好序
public static void quickSort(int[] arr, int l, int r) {
if (l < r) {
swap(arr, l + (int) (Math.random() * (r - l + 1)), r);
int[] p = partition(arr, l, r);
quickSort(arr, l, p[0] - 1);// < 区
quickSort(arr, p[1] + 1, r);// > 区
}
}
//这是一个处理arr[l..r]的函数
//默认以arr[r]做划分,arr[r] -> p <p ==p >p
//返回等于区域(左边界,右边界),所以返回一个长度为2的数组res,res[0] res[1]
public static int[] partition(int[] arr, int l, int r) {
int less = l - 1;// <区右边界
int more = r;// >区左边界
while (l < more) {// L 表示当前数的位置 arr[R] -> 划分值
if (arr[l] < arr[r]) {// 当前数 < 划分值
swap(arr, ++less, l++);
} else if (arr[l] > arr[r]) {// 当前数 > 划分值
swap(arr, --more, l);
} else {
l++;
}
}
swap(arr, more, r);
return new int[] { less + 1, more };
}
public static void swap(int[] arr, int i, int j) {
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
快速排序额外空间复杂度O(logN),最差时间复杂度O(N)