【排序算法】归并排序
提到归并排序,采用的是一种“分而治之”的分治思想,对于一个较为复杂的问题,我们可以将其划分为若干个子问题,自己求解其中一个子问题,得到递推或者递归关系,剩余的让计算机求解即可。
归并排序需要的就是这样的分治思想。
我们可以先看一个关于递归的一个经典的问题:汉诺塔问题:
如图:
要想按照规则把1,2,3三个盘子全部放到最右边的柱子上,肯定需要如下操作(这是程序运行的结果,程序是怎样实现的,我们先放一边,这个是正确的方法):
那么怎么得到这个程序的呢?
我们可以先把问题进行简化,如果解决一个只有一个盘子的汉诺塔问题,那么很容易了,我们就直接把那个最左边的那一个盘子直接放在最右边就完成了。我们再往后递推,如果有两个盘子,则需要中间那个盘子作为辅助,先把一号盘子放在中间,再把二号放在最右边,最后把位于中间柱子的一号盘子放回右边柱子即可。
我们往下逐步在进行分析,多运行几次不同数量的盘子,发现到最后,无论怎样进行求解,目的都是让最下面的盘子先落到右边的柱子,然后是倒数第二个,最后只剩一个就成功了。
下面是代码:
public class Main {
public static void main(String[] args) {
Hanoi(3,"柱子1","柱子2","柱子3");
}
private static void Hanoi(int n, String left, String middle, String right) {
if (n == 1) {
System.out.println("第"+n+"个盘子从 "+left+" 移动到 "+right);
return;
}
Hanoi(n-1,left,right,middle);
System.out.println("第"+n+"个盘子从 "+left+" 移动到 "+right);
Hanoi(n-1,middle,left,right);
}
}
不难想到,n也就是盘子的数量等于1时为递归的终止条件,和刚才说的就是一个盘子的时候直接从第一个柱子移动到第三个柱子。
那么我们接下来的递归调用,也就是想办法把左边的最后一个盘子的上面的所有盘子移动到中间的辅助柱子,这就应证了刚才所说的都是让下面的盘子先落到右边的柱子。程序最后再递归调用一次Hanoi()方法是为了回溯,也就是让位于中间辅助柱子的盘子全部回归到左边柱子。
了解了这样的归并思想,我们可以开始分析归并排序了。
对于归并排序,我的策略是先写一个方法,它能实现让两个有序的数组内的元素两两进行比较,放到需要返回的目标数组,这样,返回的目标数组的左一般肯定有序,右一般也肯定有序。
如图:
每次比较完后,较小的那个数的指针向后移,然后在于另一个数组的上次比较的元素进行比较。
当然,我们可以把这两个有序数组看作为一个数组。
代码如下:
private static int[] merge(int[] arr) {
//分配一个新数组保存归并后的结果
int[] ret = new int[arr.length];
//定义变量mid,然后对[0,mid]和(mid,arr.length-1]范围内进行归并
int mid = ((arr.length - 1) >> 1);
//记录储存到ret数组的下标
int index = 0;
int i, j;
for (i = 0, j = mid + 1; i <= mid && j < arr.length; ) {
//储存对比后二者较小的值
if (arr[i] <= arr[j]) {
ret[index++] = arr[i++];
} else {
ret[index++] = arr[j++];
}
System.out.println(Arrays.toString(ret));
}
//如果i没有递增到mid,那么就需要把后面的值同样赋值给ret
while (i <= mid) {
ret[index++] = arr[i++];
}
//j也同理
while (j < arr.length) {
ret[index++] = arr[j++];
}
return ret;
}
测试结果:
实现这个代码我们离真正实现归并排序就不远了,如过真正实现归并排序,需要执行到最后两个元素进行比较,也就是看做两个只有1个元素的数组,他们肯定是有序的,再进行归并肯定也是有序的。
此时就需要文章开头所说的分治思想了,如果递归划分子问题,可以发现刚才写的那个merge()方法只有一个参数,是肯定不够的,我们要逐步缩小归并的范围,这就需要再加三个参数了,一个是左边界的参数,一个是右边界的参数,还有一个就是右半边数组的起始位置。
代码如下:
private static void merge(int[] arr, int left, int mid, int right) {
int[] tempArr = new int[right - left + 1];
int index = 0;
int i = left;
int j = mid+1;
while (i <= mid && j <= right) {
if (arr[i] <= arr[j]) {
tempArr[index++] = arr[i++];
} else {
tempArr[index++] = arr[j++];
}
}
while (i <= mid ) {
tempArr[index++] = arr[i++];
}
while (j <= right) {
tempArr[index++] = arr[j++];
}
if (index >= 0) {
System.arraycopy(tempArr, 0, arr, left, index);
}
}
然后总的排序的方法的代码就需要我们对于[0,mid]和[mid+1,arr.length-1]这两个范围逐步缩小进行归并了,这就需要递归。其中mid的值为 left + ((right - left) >> 1) ,这样处理一是为了避免整数溢出,二是用右移运算符比直接除以2速度上要快一点。
完整代码如下:
public class MergeSort {
public static void main(String[] args) {
int[] arr = new int[]{0, 1, 3, 7, 9, 2, 5, 4, 6, 8};
sort(arr, 0, arr.length - 1);
System.out.println(Arrays.toString(arr));
}
public static void sort(int[] arr, int left, int right) {
if (left >= right) {
return;
}
int mid = left + ((right - left) >> 1);
sort(arr, left, mid);
sort(arr, mid + 1, right);
merge(arr, left, mid, right);
}
private static void merge(int[] arr, int left, int mid, int right) {
int[] tempArr = new int[right - left + 1];
int index = 0;
int i = left;
int j = mid+1;
while (i <= mid && j <= right) {
if (arr[i] <= arr[j]) {
tempArr[index++] = arr[i++];
} else {
tempArr[index++] = arr[j++];
}
}
while (i <= mid ) {
tempArr[index++] = arr[i++];
}
while (j <= right) {
tempArr[index++] = arr[j++];
}
if (index >= 0) {
System.arraycopy(tempArr, 0, arr, left, index);
}
}
}
最后就是对刚才所写的归并排序进行检验了,还是生成1000次长度为10000的随机数数组对之进行排序,和Java自带的排序进行对比检验:
代码为:
public static void check() {
int cnt = 0;
boolean flag = true;
long before = System.currentTimeMillis();
while (cnt != 1000) {
int[] arr = generateRandomArray();
int[] toCheckArray = new int[arr.length];
System.arraycopy(arr, 0, toCheckArray, 0, arr.length);
Arrays.sort(arr);
MergeSort.sort(toCheckArray,0,arr.length-1);
for (int i = 0; i < arr.length; i++) {
if (arr[i] != toCheckArray[i]) {
flag = false;
break;
}
}
cnt++;
}
System.out.println(flag ? "正确" : "错误");
long after = System.currentTimeMillis();
System.out.println("用时:" + (after - before) + "ms");
}
运行结果为: