title: 归并排序算法(基于Java实现)
tags: 归并算法
归并排序算法的原理与代码实现:
一、归并排序算法的原理
其核心思想其实蛮简单的,就是如果我们要排序一个数组,我们先数组从中间分成前后两部分,然后对前后两部分分别排序,然后再将排好序的两部分合并在一起,这样整个数组就都有序了。总而言之,归并排序使用的就是分治思想,顾名思义,就是分而治之,将一个大问题分解成小的子问题来解决。小的子问题解决了,大问题也就解决了。而分治算法一般都是用递归来实现的,分而治之是一种解决问题的处理思想,递归则是一种编程技巧。
![](https://tva1.sinaimg.cn/large/0081Kckwgy1globtxnd25j30vq0pen1o.jpg)
可以先来看一段伪代码:
// 归并排序算法, A是数组,n表示数组大小
merge_sort(A, n) {
merge_sort_c(A, 0, n-1)
}
// 递归调用函数
merge_sort_c(A, p, r) {
// 递归终止条件
if p >= r then return
// 取p到r之间的中间位置q
q = (p+r) / 2
// 分治递归
merge_sort_c(A, p, q)
merge_sort_c(A, q+1, r)
// 将A[p...q]和A[q+1...r]合并为A[p...r]
merge(A[p...r], A[p...q], A[q+1...r])
}
你可能已经发现了,merge(A[p…r], A[p…q], A[q+1…r]) 这个函数的作用就是,将已经有序的 A[p…q]和 A[q+1…r]合并成一个有序的数组,并且放入 A[p…r]。那这个过程具体该如何做呢?
如图所示,我们申请一个临时数组 tmp,大小与 A[p…r]相同。我们用两个游标 i 和 j,分别指向 A[p…q]和 A[q+1…r]的第一个元素。比较这两个元素 A[i]和 A[j],如果 A[i]<=A[j],我们就把 A[i]放入到临时数组 tmp,并且 i 后移一位,否则将 A[j]放入到数组 tmp,j 后移一位。
继续上述比较过程,直到其中一个子数组中的所有数据都放入临时数组中,再把另一个数组中的数据依次加入到临时数组的末尾,这个时候,临时数组中存储的就是两个子数组合并之后的结果了。最后再把临时数组 tmp 中的数据拷贝到原数组 A[p…r]中。
![](https://tva1.sinaimg.cn/large/0081Kckwly1glovs2p3s9j30vq0nstct.jpg)
二、归并排序算法的代码实现
package com.company;
import java.util.Arrays;
public class MergeSort2 {
public static void mergeSort(int[] a, int p, int r){
if(p < r){
int mid = p + (r - p)/2;
mergeSort(a, p, mid);//左排序
mergeSort(a,mid+1, r);//右排序
merge(a, p, mid, r);//左右合并
}
}
public static void merge(int[] a, int p, int m, int r){
int[] help = new int[r - p + 1];
int i = 0;
int p1 = p;
int p2 = m + 1;
//这个地方代码写法很巧妙!!!
while(p1 <= m && p2 <= r){
if(a[p1] < a[p2]){
help[i++] = a[p1++];
}else{
help[i++] = a[p2++];
}
}
//将剩余的数据拷贝至help数组
while(p1 <= m){
help[i++] = a[p1++];
}
while(p2 <= r){
help[i++] = a[p2++];
}
//再将help中的数组拷贝回数组a中
for(i = 0; i<help.length; i++){
a[p++] = help[i];
}
}
public static void main(String[] args) {
int[] a = {10, 9, 7, 4, 3, 2, 1, 8};
System.out.println("之前的排序:");
System.out.println(Arrays.toString(a));
mergeSort(a, 0, 7);
System.out.println("之后的排序:");
System.out.println(Arrays.toString(a));
}
}
输出的结果为:
之前的排序:
[10, 9, 7, 4, 3, 2, 1, 8]
之后的排序:
[1, 2, 3, 4, 7, 8, 9, 10]
三、代码优化
可以利用“哨兵”简化编程的技巧,如果上述的merge()合并函数借助“哨兵”,代码就会简洁很多。
哨兵方法:
-
先将要合并的两个放到tmpLeft和tmpRight数组,其中每个数组都多出一个位置放哨兵 ;
-
tmpLeft[leftSize] = int.MaxValue; tmpRight[rightSize] = int.MaxValue;
-
比较两个tmp数组,哪个小就放到原数组,使用哨兵不用再判断是否有剩下的有序数组。
for (k=left,i=0,j=0; k<=right; k++){ if(tmpLeft[i] < tmpRight[j]){ arr[k] = tmpLeft[i]; i++; }else{ arr[k] = tmpRight[j]; j++; } }
完整的merge函数代码如下:(只需要将该段代码与上述的merge函数代码段替换即可,就能够实现代码的优化。)
private static void mergeBySentry(int[] arr, int p, int q, int r){ int[] leftArr = new int[q - p +2]; int[] rightArr = new int[r - q + 1]; for(int i=0; i<=q - p; i++){ leftArr[i] = arr[p + i]; } //第一个数组添加哨兵(最大值) leftArr[q - p + 1] = Integer.MAX_VALUE; for(int i = 0; i<r-q; i++){ rightArr[i] = arr[q+1+i]; } //第二个数组添加哨兵(最大值) rightArr[r-q] = Integer.MAX_VALUE; int i = 0; int j = 0; int k = 0; while(k<=r){ //当左边数组达到哨兵值时,i不再增加,直到右边数组读取完剩余值,同理右边数组也一样 if(leftArr[i] <= rightArr[j]){ arr[k++] = leftArr[i++]; }else{ arr[k++] = rightArr[j++]; } } }
四、归并排序的性能分析
结合我前面画的那张图和归并排序的伪代码,你应该能发现,归并排序稳不稳定关键要看 merge() 函数,也就是两个有序子数组合并成一个有序数组的那部分代码。在合并的过程中,如果 A[p…q]和 A[q+1…r]之间有值相同的元素,那我们可以像伪代码中那样,先把 A[p…q]中的元素放入 tmp 数组。这样就保证了值相同的元素,在合并前后的先后顺序不变。所以,归并排序是一个稳定的排序算法。
从我们的原理分析和伪代码可以看出,归并排序的执行效率与要排序的原始数组的有序程度无关,所以其时间复杂度是非常稳定的,不管是最好情况、最坏情况,还是平均情况,时间复杂度都是 O(nlogn)。
归并排序并没有像快排那样,应用广泛,这是为什么呢? 因为它有一个致命的“弱点”,那就是归并排序不是原地排序算法。
实际上,递归代码的空间复杂度并不能像时间复杂度那样累加。刚刚我们忘记了最重要的一点,那就是,尽管每次合并操作都需要申请额外的内存空间,但在合并完成之后,临时开辟的内存空间就被释放掉了。在任意时刻,CPU 只会有一个函数在执行,也就只会有一个临时的内存空间在使用。临时内存空间最大也不会超过 n 个数据的大小,所以空间复杂度是 O(n)。