什么是分治法
就是把一个复杂的问题分成两个或更多的相同或相似的子问题,直到最后子问题可以简单的直接求解,原问题的解即子问题的解的合并。
本文会用归并排序和快速排序来说明分治法。
归并排序是将“排序”(sort)问题转化为了“合并两个有序数组”(merge)。快速排序是将“排序”(sort)问题转化为了“挑选中点问题”(partition)
归并排序
归并排序即将排序问题转化为了合并问题,分为自顶向下的归并和自底向上的归并。
自顶向下的归并
核心代码的结构如下
private static void sort(int[] arr, int start, int end) {
if (start < end) {
int mid = (start + end) / 2;
sort(arr, start, mid);
sort(arr, mid + 1, end);
merge(arr, start, mid, end);
} else {
return;
}
}
当问题规模只有1时(start>=end),问题不用继续拆分下去,可以直接解决。
实际上将sort问题转化成了多个merge问题
public static void merge(int[] arr, int start, int mid, int end) {
int k = start;
int i;
int j;
for (i = start, j = mid + 1; i <= mid && j <= end;) {
if (arr[i] < arr[j]) {
aux[k] = arr[i];
k++;
i++;
} else {
aux[k] = arr[j];
k++;
j++;
}
}
if (i > mid) {
for (; j <= end;) {
aux[k] = arr[j];
k++;
j++;
}
} else if (j > end) {
for (; i <= mid;) {
aux[k] = arr[i];
k++;
i++;
}
}
for (k = start; k <= end; k++) {
arr[k] = aux[k];
}
}
merge算法需要一个大小为n的辅助空间(aux数组)
性能分析
时间复杂度:
由于问题规模的切分是严格等分的int mid = (start + end) / 2;所以形成的二叉树是高度为logN,和输入无关,每一层的归并总时间都是O(N),所以最后的时间都是O(NlogN)
空间复杂度: 做归并时一定需要一个大小为n的辅助数组,优化方案是将辅助数组定义在全局,而不是merge的作用域中,可以避免反复申请内存。空间复杂度O(N)
全部代码
package sort;
import java.util.Random;
public class fromTopToButtonMergeSort {
static int[] aux;
public static void sort(int[] arr) {
aux = new int[arr.length];
sort(arr, 0, arr.length - 1);
}
private static void sort(int[] arr, int start, int end) {
if (start < end) {
int mid = (start + end) / 2;
sort(arr, start, mid);
sort(arr, mid + 1, end);
merge(arr, start, mid, end);
} else {
return;
}
}
public static void merge(int[] arr, int start, int mid, int end) {
int k = start;
int i;
int j;
for (i = start, j = mid + 1; i <= mid && j <= end;) {
if (arr[i] < arr[j]) {
aux[k] = arr[i];
k++;
i++;
} else {
aux[k] = arr[j];
k++;
j++;
}
}
if (i > mid) {
for (; j <= end;) {
aux[k] = arr[j];
k++;
j++;
}
} else if (j > end) {
for (; i <= mid;) {
aux[k] = arr[i];
k++;
i++;
}
}
for (k = start; k <= end; k++) {
arr[k] = aux[k];
}
}
public static void display(int[] arr) {
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i] + '/');
}
}
public static void main(String[] args) {
int CONST = 100000;
int[] arr = new int[CONST];
Random r = new Random();
for (int i = 0; i < CONST; i++) {
arr[i] = r.nextInt() % 4000;
}
System.out.println("-------自顶向下的归并排序---------");
System.out.println("处理的数据量是:" + CONST);
long startTime = System.currentTimeMillis();
fromTopToButtonMergeSort.sort(arr);
long endTime = System.currentTimeMillis();
// display(arr);
System.out.println("处理时间:" + (endTime - startTime) + "ms");
}
}
自底向上的归并排序
与上面的递归不同,自底向上的排序认为每一个数字都是独立有序的,两两归并,循环直到使得整个数组有序
核心代码如下
private static void sort(int[] arr, int start, int end) {
int length = arr.length;
// 分组:临界条件——组长小于数组长度
for (int size = 1; size < length; size *= 2) {
// 相邻合并:临界条件——中间有断开
for (int i = 0; i + size < length; i += size * 2) {
merge(arr, i, i + size - 1, Math.min(i + size + size - 1, length - 1));
}
}
}
全部代码
package sort;
import java.util.Random;
public class fromButtonToTopMergeSort {
static int[] aux;
public static void sort(int[] arr) {
aux = new int[arr.length];
sort(arr, 0, arr.length - 1);
}
private static void sort(int[] arr, int start, int end) {
int length = arr.length;
// 分组:临界条件——组长小于数组长度
for (int size = 1; size < length; size *= 2) {
// 相邻合并:临界条件——中间有断开
for (int i = 0; i + size < length; i += size * 2) {
merge(arr, i, i + size - 1, Math.min(i + size + size - 1, length - 1));
}
}
}
public static void merge(int[] arr, int start, int mid, int end) {
int k = start;
int i;
int j;
for (i = start, j = mid + 1; i <= mid && j <= end;) {
if (arr[i] < arr[j]) {
aux[k] = arr[i];
k++;
i++;
} else {
aux[k] = arr[j];
k++;
j++;
}
}
if (i > mid) {
for (; j <= end;) {
aux[k] = arr[j];
k++;
j++;
}
} else if (j > end) {
for (; i <= mid;) {
aux[k] = arr[i];
k++;
i++;
}
}
for (k = start; k <= end; k++) {
arr[k] = aux[k];
}
}
public static void display(int[] arr) {
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i] + '/');
}
}
public static void main(String[] args) {
int CONST = 100000;
int[] arr = new int[CONST];
Random r = new Random();
for (int i = 0; i < CONST; i++) {
arr[i] = r.nextInt() % 4000;
}
System.out.println("-------自底向上的归并排序---------");
System.out.println("处理的数据量是:" + CONST);
long startTime = System.currentTimeMillis();
fromTopToButtonMergeSort.sort(arr);
long endTime = System.currentTimeMillis();
// display(arr);
System.out.println("处理时间:" + (endTime - startTime) + "ms");
}
}
快速排序
快速排序将排序问题转化成了挑选中点问题,核心代码结构如下:
private static void sort(int[] arr, int l, int r) {
if (l < r) {
int p = partition(arr, l, r);
sort(arr, l, p - 1);
sort(arr, p + 1, r);
} else {
return;
}
}
理想情况下树的高度是logN,每一层去挑选中点的总时间都是N(跟全部元素比较),时间复杂度是NlogN
单路快速排序的时间复杂度受输入的影响,当输入高度有序或者有大量重复时,会使得左右子树不均匀,树的高度变成N,时间复杂度会从NlogN退化成N^2。
所以为了解决这个问题有了两路和三路快速排序,可以解决有大量重复元素时的性能影响。(如果时正序或者乱序,两路或者三路都没有办法解决,时间复杂度都还是N^2)
两路快速排序
两路快速排序解决大量重复元素的核心是partition的arr[++i] < v和arr[--j] > v都不能带等于号,虽然这样会增加与标杆元素相等项的交换成本,但是可以避免相等元素都在树的一侧(比如想象一下快排10000000000)
package sort;
import java.util.Random;
public class quickSort2Way {
private static int partition(int[] arr, int l, int r) {
int v = arr[l];
int i = l, j = r + 1;
while (true) {
while (arr[++i] < v) {
if (i == r)
break;
}
while (arr[--j] > v) {
if (j == l)
break;
}
if (i >= j)
break;
swap(arr, i, j);
}
swap(arr, l, j);
return j;
}
private static void swap(int[] arr, int i, int j) {
int t = arr[i];
arr[i] = arr[j];
arr[j] = t;
}
private static void sort(int[] arr, int l, int r) {
if (l < r) {
int p = partition(arr, l, r);
sort(arr, l, p - 1);
sort(arr, p + 1, r);
} else {
return;
}
}
public static void main(String[] args) {
int CONST = 100000;
int[] arr = new int[CONST];
Random r = new Random();
for (int i = 0; i < CONST; i++) {
arr[i] = r.nextInt() % 4000;
}
System.out.println("-------两路快速排序-----------");
System.out.println("处理的数据量" + CONST);
long startTime = System.currentTimeMillis();
quickSort2Way.sort(arr, 0, arr.length - 1);
long endTime = System.currentTimeMillis();
System.out.println("处理时间:" + (endTime - startTime) + "ms");
}
}
三路快速排序
三路快速排序维护三个数组,小于/大于/等于标志元素,可以进一步优化有大量相等元素的情况,降低树的高度。
package sort;
import java.util.Random;
public class quickSort2Way {
private static int partition(int[] arr, int l, int r) {
int v = arr[l];
int i = l, j = r + 1;
while (true) {
while (arr[++i] < v) {
if (i == r)
break;
}
while (arr[--j] > v) {
if (j == l)
break;
}
if (i >= j)
break;
swap(arr, i, j);
}
swap(arr, l, j);
return j;
}
private static void swap(int[] arr, int i, int j) {
int t = arr[i];
arr[i] = arr[j];
arr[j] = t;
}
private static void sort(int[] arr, int l, int r) {
if (l < r) {
int p = partition(arr, l, r);
sort(arr, l, p - 1);
sort(arr, p + 1, r);
} else {
return;
}
}
public static void main(String[] args) {
int CONST = 100000;
int[] arr = new int[CONST];
Random r = new Random();
for (int i = 0; i < CONST; i++) {
arr[i] = r.nextInt() % 4000;
}
System.out.println("-------两路快速排序-----------");
System.out.println("处理的数据量" + CONST);
long startTime = System.currentTimeMillis();
quickSort2Way.sort(arr, 0, arr.length - 1);
long endTime = System.currentTimeMillis();
System.out.println("处理时间:" + (endTime - startTime) + "ms");
}
}
性能分析
时间复杂度:平均时间复杂度是O(NlogN),最差情况是O(N^2),无论是优化后的两路还是三路快排都没有办法解决这个问题(高度有序时依旧时N^2)
空间复杂度:快速排序是交换排序的一种,即可以实现原地排序,不需要额外的辅助空间,空间复杂度是取决于递归调用的深度,最好情况是O(logN),最差是O(N).