归并排序(Merge sort),是建立在归并操作上的一种有效的排序算法,时间复杂度为 O(nlog n)。1945年由约翰.冯.诺伊曼首次提出。该算法是采用分治算法的非常典型的应用,且各层分治递归可以同时进行。常用的归并排序的实现方式有递归方式和迭代方式,我们通过一组数据来分析归并排序的排序原理。初始序列: 85 47 31 70 57 86 28 1 38 31
递归式求解是这样工作的:将待排序的序列分成两部分,然后对这两部分分别进行排序,然后再把这两部分已经排序好的序列合并起来得到最终的结果。对于划分后的每一部分使用相同的方式递归的进行划分、各个部分分别排序、合并的操作。递归的终止条件是每个部分都只有一个元素,那么此时每个部分也都是排序好的,在把这些部分进行以此合并,从而得到最终排序完成的序列。
初始序列: 85 47 31 70 57 86 28 1
第一次划分: 85 47 31 70 | 57 86 28 1
第二次划分: 85 47 | 31 70 | 57 86 | 28 1
第三次划分: 85 | 47 | 31 | 70 | 57 | 86 | 28 | 1 (此时每个部分都不能在继续划分了,递归到达终止条件,分别对每个部分进行排序,因为每个部分都只有一个元素,因此不要进行排序,直接按照递归的顺序进行归并)
第一次归并: 47 85 | 31 70 | 57 86 | 1 28
第二次归并: 31 47 70 85 | 1 28 57 86
第三次归并: 1 28 31 47 57 70 85 86 (至此,我们就对需要排序的序列完成了排序工作,代码实现如下)
public void sort(int[] array) {
sort(array,0,array.length-1);
}
private void sort(int[] array,int lt,int rt){
if(lt < rt){ //递归终止条件,也就是每个部分只有一个元素时
int mid = (rt-lt)/2 + lt;
sort(array,lt,mid);
sort(array,mid+1,rt);
//此时左右两部分[lt....mid]和[mid+1......rt]都已经排序完成,进行合并操作
merge(array,lt,partition,rt);
}
}
//归并array[lt...mid]和array[mid+1....rt]两部分,这是归并排序最关键的部分。
private void merge(int[] array, int lt, int mid, int rt) {
int[] aux = new int[rt-lt+1]; //开辟空间暂存待合并期间的数组元素
for (int i = lt; i <= rt ; i++) {
aux[i-lt] = array[i];
}
int i = lt, j = mid+1;
for (int k = lt; k <= rt ; k++) {
if(i > mid){ //此时i到达边界,表面array[lt....mid]的区间元素已全部被归并。
array[k] = aux[j-lt];
j++;
}
else if(j > rt){//此时i到达边界,表面array[lt....mid]的区间元素已全部被归并。
array[k] = aux[i-lt];
i++;
}
//选择两个区间中较小的元素进行归并
else if(aux[i-lt] <= aux[j-lt]){
array[k] = aux[i-lt];
i++;
}
else{
array[k] = aux[j-lt];
j++;
}
}
}
上面的算法实现了归并排序,但是还有一些地方值得进行优化。①如果在对左右两部分进行排序后,如果左边部分[lt.....mid]部分的最大值小于等于右边部分[mid+1,rt]的最小值,那么整个区间[lt.....rt]都是有序的因此就不需要再进行merge操作。(虽然这是一种优化思路,但是在真正的运行结果上并不见得这种改进就一定比最初的实现好,因为进行if判断也是需要消耗时间)②对于区间长度小于某一值时可以转而使用插入排序进行排序(虽然插入排序的时间复杂度为O(n^2),但是对于这两种算法的时间复杂度的最高阶项都存在一个系数,而插入排序的最高阶项系数小于归并排序的最高阶项系数,因此在对于数据量较小时,虽然插入排序是O(n^2)级别的排序算法,它的性能却优于归并排序)。下面是两个版本的改进:
2th edition:
private void sort(int[] array,int lt,int rt){
if(lt < rt){ //递归终止条件,也就是每个部分只有一个元素时
int mid = (rt-lt)/2 + lt;
sort(array,lt,mid);
sort(array,mid+1,rt);
//此时左右两部分[lt....mid]和[mid+1......rt]都已经排序完成,进行合并操作
if(array[mid] > array[mid+1]) //只有在整个区间[lt.....rt]不是有序时才进行merge操作
merge(array,lt,partition,rt);
}
}
3th edition:
private void sort(int[] array, int lt, int rt) {
if(rt - lt <= 15){ //区间长度小于等于16时转而使用插入排序,这里的取值也会影响性能
insertionsort(array,lt,rt);
return;
}
int mid = (rt - lt) / 2 + lt;
sort(array,lt,mid);
sort(array,mid+1,rt);
merge(array,lt,mid,rt);
}
//对数组array的[lt....rt]区间进行插入排序
private void insertionsort(int[] array, int lt, int rt) {
for (int i = lt+1; i <=rt ; i++) {
int temp = array[i];
int j = i;
for (; (j > lt) && (temp < array[j-1]) ; j--) {
array[j] = array[j-1];
}
array[j] = temp;
}
}
接下来我们通过一些测试用例来测试归并排序的的三个版本的性能:
测试用例为一组随机元素的数组 ,元素个数 = 1000000
mergesort 排序 1000000 个元素共耗时:0.237803676s
排序结果:true
mergesort 2th edition 排序 1000000 个元素共耗时:0.183761944s
排序结果:true
mergesort 3th edition 排序 1000000 个元素共耗时:0.164161536s
排序结果:true
----------------------------------------------------------------------------------
测试用例为一组近乎有序的数组(对有序序列进行少量次交换),元素个数 = 1000000,交换次数 = 100
mergesort 排序 1000000 个元素共耗时:0.042354722s
排序结果:true
mergesort 2th edition 排序 1000000 个元素共耗时:0.053551938s
排序结果:true
mergesort 3th edition 排序 1000000 个元素共耗时:0.060508686s
排序结果:true
----------------------------------------------------------------------------------
测试用例为一组包含大量重复元素的数组,元素个数 = 1000000,数组元素值的范围 [0,20]
mergesort 排序 1000000 个元素共耗时:0.152879319s
排序结果:true
mergesort 2th edition 排序 1000000 个元素共耗时:0.11087104s
排序结果:true
mergesort 3th edition 排序 1000000 个元素共耗时:0.108981508s
排序结果:true
----------------------------------------------------------------------------------
//对于5000000的数据量
测试用例为一组随机元素的数组 ,元素个数 = 5000000
mergesort 排序 5000000 个元素共耗时:0.984056449s
排序结果:true
mergesort 2th edition 排序 5000000 个元素共耗时:0.876236485s
排序结果:true
mergesort 3th edition 排序 5000000 个元素共耗时:0.737002266s
排序结果:true
----------------------------------------------------------------------------------
测试用例为一组近乎有序的数组(对有序序列进行少量次交换),元素个数 = 5000000,交换次数 = 100
mergesort 排序 5000000 个元素共耗时:0.193765704s
排序结果:true
mergesort 2th edition 排序 5000000 个元素共耗时:0.126874958s
排序结果:true
mergesort 3th edition 排序 5000000 个元素共耗时:0.287038947s
排序结果:true
----------------------------------------------------------------------------------
测试用例为一组包含大量重复元素的数组,元素个数 = 5000000,数组元素值的范围 [0,20]
mergesort 排序 5000000 个元素共耗时:0.595866136s
排序结果:true
mergesort 2th edition 排序 5000000 个元素共耗时:0.556082326s
排序结果:true
mergesort 3th edition 排序 5000000 个元素共耗时:0.456393715s
排序结果:true
----------------------------------------------------------------------------------
归并排序的时间复杂度为O(nlogn),因此对于1000000甚至是5000000数量级的数据进行排序能够在很快的时间排序完成。
对于递归式的实现,使用了分治算法的思想:现将要求解答问题拆分成若干个子问题,对子问题分别求解,然后再讲各个子问题合并得到最终的结果,如果子问题还不能求解,就再对子问题进行拆分直到能够求解。考虑迭代式的实现方式,又称之为自底向上实现方式。我们从上面递归式的执行过程可以看出,当我们将每个问题划分成只有一个元素时,不需要在进行排序和继续划分,直接向上合并即可。由此我们可以设计出自底向上归并排序算法。
初始序列: 85 | 47 | 31 | 70 | 57 | 86 | 28 | 1
第一次归并: 47 85 | 31 70 | 57 86 | 1 28
第二次归并: 31 47 70 85 | 1 28 57 86
第三次归并: 1 28 31 47 57 70 85 86
public void sort(int[] array) {
final int len = array.length;
for (int sz = 1; sz <= len ; sz+=sz) { //合并操作时每个部分的元素个数
for (int i = 0; i+sz < len ; i+=sz*2) {
merge(array,i,i+sz-1,Math.min(len-1,i+sz*2-1));
}
}
}
private void merge(int[] array, int lt, int partition, int rt) {
final int len = rt - lt + 1;
int[] aux = new int[len];
for (int i = lt; i <= rt; i++) {
aux[i-lt] = array[i];
}
int i = lt,j = partition + 1;
for (int k = lt; k <= rt; k++) {
if(i > partition){
array[k] = aux[j-lt];
j++;
}else if(j > rt){
array[k] = aux[i-lt];
i++;
}else if(aux[i-lt] <= aux[j-lt]){
array[k] = aux[i-lt];
i++;
}else{
array[k] = aux[j-lt];
j++;
}
}
}
这种实现方式有一个很重要的特性是没有使用到数组的索引就完成了排序工作,这个特性十分有用,我们根据它完成对链表的排序工作。