归并排序
思想
之前递归文章说道,递归就是把一个大问题拆分成一定规模的子问题,然后合并结果。那么其实归并也是一样的思想:把一个数组无限拆分子任务,直到规模不可拆分为止。怎么拆分呢?取mid中间index值,拆分出来的左边子任务和右边子任务都排序,然后再merge,merge时再比较左边右边子任务的数值比较,就达到了整体数组排序的效果。这里merge需要申请一个临时数组进行回填拍排序的数组,你会发现,不需要的比较的值就直接就回填到数组中了,像冒泡、选择排序每一个值都必须对比,而归并就省去了一些这种重复比较的时间复杂度。归并排序重点就在于怎么merge。下面画个图来描述一下:
实现
public static void main(String[] args) throws Exception {
int[] arr = new int[]{3, 6 , 1, 8};
process1(arr, 0, arr.length - 1);
Arrays.stream(arr).forEach(System.out::println);
}
public static void process1(int[] arr, int L, int R) {
if (L == R) {
return;
}
int mid = L + ((R - L) >> 1);
process1(arr, L, mid);
process1(arr, mid + 1, R);
merge(arr, L, mid, R);
}
public static void merge(int[] arr, int L, int mid, int R) {
int help[] = new int[R- L + 1];
// help每个位置上存放的最小的数
int i = 0;
// 左边子任务的开始位置
int p1 = L;
// 右边子任务的开始位置
int p2 = mid + 1;
// 只要左边和右边的子任务始终小于最大任务长度
// 左右子任务数任何一边比较完,就跳出循环
while (p1 <= mid && p2 <= R) {
help[i++] = arr[p1] <= arr[p2] ? arr[p1++] : arr[p2++];
}
/**
* 核心:不需要比较的数因为已经排过序了,所以节省了一定排序时间复杂度
*/
// 如果右边子任务的index比较完了,就把左边子任务不需要比较的数放进help
while (p1 <= mid) {
help[i++] = arr[p1++];
}
// 如果左边子任务的index比较完了,就把右边子任务不需要比较的数放进help
while (p2 <= R) {
help[i++] = arr[p2++];
}
for (i = 0; i < help.length; i++) {
arr[L + i] = help[i];
}
}
时间复杂度
T(N) = a * T(N/b) + O(N^d)。怎么计算的呢?process1方法中你发现除了merge,其他计算都是常数级别的操作。那么merge的时间复杂度是多少呢?merge无非不就是遍历递归每个子任务的长度的数组吗?规模次数a=2, 平均规模大小b=2,d = 1,所以递归通项公式匹配出 第一种复杂度:
拓展
在一个数组中,一个数左边比它小的数的总和,叫数的小和,所有数的小和累加起来,叫数组小和,求下面数组小和。
例子:[1,3,4,2,5]
1左边比1小的数:没有
3左边比3小的数:1
4左边比3小的数:1、3
2左边比3小的数:1
5左边比3小的数:1、3、4、2
所以数组的小和为1+1+3+1+1+3+4+2 = 16
由于这样的方式复杂度是O(N^2) 复杂度,那么用归并排序的思路求数组小和。
思想
实现
public static int process1(int[] arr, int L, int R) {
if (L == R) {
return 0;
}
int mid = L + ((R - L) >> 1);
return process1(arr, L, mid)
+ process1(arr, mid + 1, R)
+ merge(arr, L, mid, R);
}
public static int merge(int[] arr, int L, int mid, int R) {
int help[] = new int[R- L + 1];
// help每个位置上存放的最小的数
int i = 0;
// 左边子任务的开始位置
int p1 = L;
// 右边子任务的开始位置
int p2 = mid + 1;
// 每一次merge的小和
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++];
}
/**
* 核心:不需要比较的数因为已经排过序了,所以节省了一定排序时间复杂度
*/
// 如果右边子任务的index比较完了,就把左边子任务不需要比较的数放进help
while (p1 <= mid) {
help[i++] = arr[p1++];
}
// 如果左边子任务的index比较完了,就把右边子任务不需要比较的数放进help
while (p2 <= R) {
help[i++] = arr[p2++];
}
for (i = 0; i < help.length; i++) {
arr[L + i] = help[i];
}
return res;
}
快排
思想
首先按照递归通用思想,不断拆分成子任务,直到无法拆分。
实现
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);
}
// 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);
}
}
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;
}
时间复杂度
其实和归并复杂度一样的。同样只拆分的子任务的数值在各自的左边部分和右边部分做比较与迭代,因为是随机找一个数拆分左右子任务,所以不按最差情况来计算复杂度,概率事件期望是和最好的情况接近的。