文章目录
本系列文章共有十一篇:
冒泡排序及优化详解
快速排序及优化详解
插入排序及优化详解
希尔排序及优化详解
选择排序及优化详解
归并排序及优化详解
堆排序详解
计数排序及优化详解
桶排序详解
基数排序及优化详解
十大排序算法的比较与性能分析
一、归并排序基础
归并排序是建立在归并操作上的一种有效的排序算法,该算法是采用分治法的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
归并排序是一种稳定的排序方法。
1.1 归并排序图示
归并排序的步骤:
1>创建临时数组,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列;
2>设定两个指针,最初位置分别为两个已经排序序列的起始位置;
3>比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置;
4>重复步骤 3 直到某一指针达到序列尾;
5>将另一序列剩下的所有元素直接复制到合并序列尾。
此处引用网上一张比较经典的gif来展示归并排序的整个过程:
如果上面的图,不够直观的话,可以看下面的这张图:
1.2 归并排序实现
将上述过程用代码表示,示例如下:
public class MergeSort {
public static void main(String[] args) {
int[ ] array = new int[ ]{5,2,3,9,4,7};
int[ ] temp = new int[6];
/*借助temp数组对array数组中的0 - array.length-1位置的元素进行排序*/
mergeSort(array,0,array.length-1,temp);
for(int i = 0;i<array.length;i++)
System.out.print(array[i]+" ");
}
static void mergeSort(int arr[],int start,int end,int temp[]){
if(start<end){
int mid = (start+end)/2;
/*拆分左右序列*/
mergeSort(arr,start,mid,temp);
mergeSort(arr,mid+1,end,temp);
/*将每个拆分的序列进行合并*/
mergeArray(arr,start,mid,end,temp);
}
}
/*将两个数组归并排序*/
static void mergeArray(int arr[],int start,int mid,int end,int temp[]) {
/*将每个子序列,即arr[start] - arr[end],拆分成左右两部分:下标i到m为左部分,
*下标j到n为右部分
*/
int i = start,j = mid+1;
int m = mid,n = end;
/*k代表临时数组元素的下标*/
int k = 0;
/*左右两个子序列都有元素*/
while(i<=m && j<=n){
/*左右,哪个子序列的数据小,就放入临时数组temp*/
if(arr[i]<arr[j])
temp[k++] = arr[i++];
else
temp[k++] = arr[j++];
}
/*左边序列还有数据,直接追加到temp数组中*/
while(i<=m)
temp[k++] = arr[i++];
/*右边序列还有数据,直接追加到temp数组中*/
while(j<=n)
temp[k++] = arr[j++];
/*将临时数组temp的元素追加在原数组arr中*/
for(i=0;i<k;i++)
arr[start+i] = temp[i];
}
}
二、归并排序优化
归并排序常用的优化方式有3种:
1>对于小数组可以使用插入排序或者选择排序,避免递归调用。
2>在合并两个子数组之前,可以比较左边子数组的最后一个值是否小于右边子数组的第一个值,如果小于,则不用再逐个比较左右子数组的元素,因为左右子数组都已有序,直接合并就行。
3>为了节省将元素复制到辅助数组作用的时间,可以在递归调用的每个层次交换原始数组与辅助数组的角色。
2.1 小数组使用插入排序
该种优化方式比较容易理解,实现方式为在mergeArray方法的开始位置,加入对子数组的数量判断即可,示例代码如下:
/*优化方式1:对于小序列,使用插入排序,比如小序列数量<=7时*/
if((end-start) >= 7){
insertionSort(arr,end-start);
}
2.2 避免多余比较
第二种优化,针对的是某一子数组已经有序,并且完全大于(小于)另一子数组,在本次合并时,则不需要再对该有序子序列中的元素挨个进行比较的情况,此时,直接返回即可。
我们可以先将初始的数组改成{4,2,3,5,7,9},然后在mergeArray的while循环中,加入一些语句,打印一下当时的子数组中的元素,示例如下:
while(i<=m && j<=n){
System.out.print("arr["+i+"]:"+arr[i]+",arr["+j+"]:"+arr[j]);
System.out.println();
/*左右,哪个子序列的数据小,就放入临时数组temp*/
if(arr[i]<arr[j])
temp[k++] = arr[i++];
else
temp[k++] = arr[j++];
}
此时,打印结果如下:
arr[0]:4,arr[1]:2
arr[0]:2,arr[2]:3
arr[1]:4,arr[2]:3
arr[3]:5,arr[4]:7
arr[3]:5,arr[5]:9
arr[4]:7,arr[5]:9
arr[0]:2,arr[3]:5
arr[1]:3,arr[3]:5
arr[2]:4,arr[3]:5
从上面的打印可以看出,数组的后三个元素组成的子数组是{5,7,9},即有序后,还是在和前面的{2,3,4}挨个比较,这个过程完全没用必要,可以规避掉。这也是此种优化方式的目的,示例代码如下:
public static void main(String[] args) {
int[ ] array = new int[ ]{4,2,3,5,7,9};
int[ ] temp = new int[6];
/*借助temp数组对array数组中的0 - array.length-1位置的元素进行排序*/
mergeSort(array,0,array.length-1,temp);
System.out.println("排序后的结果:");
for(int i = 0;i<array.length;i++)
System.out.print(array[i]+" ");
}
static void mergeSort(int arr[],int start,int end,int temp[]){
if(start<end){
int mid = (start+end)/2;
/*拆分左右序列*/
mergeSort(arr,start,mid,temp);
mergeSort(arr,mid+1,end,temp);
/*将每个拆分的序列进行合并*/
mergeArray(arr,start,mid,end,temp);
}
}
/*将两个数组归并排序*/
static void mergeArray(int arr[],int start,int mid,int end,int temp[]) {
/*将每个子序列,即arr[start] - arr[end],拆分成左右两部分:下标i到m为左部分,
*下标j到n为右部分
*/
int i = start,j = mid+1;
int m = mid,n = end;
/*k代表临时数组元素的下标*/
int k = 0;
if(arr[mid]<arr[mid+1]){
return;
}
/*左右两个子序列都有元素*/
while(i<=m && j<=n){
System.out.print("arr["+i+"]:"+arr[i]+",arr["+j+"]:"+arr[j]);
System.out.println();
/*左右,哪个子序列的数据小,就放入临时数组temp*/
if(arr[i]<arr[j])
temp[k++] = arr[i++];
else
temp[k++] = arr[j++];
}
/*左边序列还有数据,直接追加到temp数组中*/
while(i<=m)
temp[k++] = arr[i++];
/*右边序列还有数据,直接追加到temp数组中*/
while(j<=n)
temp[k++] = arr[j++];
/*将临时数组temp的元素追加在原数组arr中*/
for(i=0;i<k;i++)
arr[start+i] = temp[i];
}
测试结果如下:
arr[0]:4,arr[1]:2
arr[0]:2,arr[2]:3
arr[1]:4,arr[2]:3
排序后的结果:
2 3 4 5 7 9
2.3 节省元素拷贝时间
此种优化,是想节省在原数组和辅助数组之间拷贝元素的时间,做法是:先克隆原数组到辅助数组,然后对克隆出来的辅助数组进行拆,拆完后合并时,替换原有数组中对应位置的元素,示例代码如下:
public static void main(String[] args) {
int[ ] array = new int[ ]{4,2,3,5,7,9,8};
/*拷贝一个和a所有元素相同的辅助数组*/
int[] arrTemp = array.clone();
sort(array,arrTemp,0,array.length-1);
System.out.println("排序后的结果:");
for(int i = 0;i<array.length;i++)
System.out.print(array[i]+" ");
}
/*基于递归的归并排序算法*/
static void sort (int a[], int temp[], int start,int end) {
if(end > start){
int mid = start+(end-start)/2;
/*对左右子序列递归*/
sort(temp, a,start,mid);
sort(temp, a,mid+1,end);
/*合并左右子数组*/
merge(a, temp, start,mid,end);
}
}
/*arr[low...high] 是待排序序列,其中arr[low...mid]和 a[mid+1...high]已有序*/
static void merge (int arr[],int temp[],int start,int mid,int end) {
/*左边子序列的头元素*/
int i = start;
/*右边子序列的头元素*/
int j = mid+1;
for(int k = start;k <= end;k++){
if(i>mid){
/*左边子序列元素用尽*/
arr[k] = temp[j++];
}else if(j>end){
/*右边子序列元素用尽*/
arr[k] = temp[i++];
}else if(temp[j]<temp[i]){
/*右边子序列当前元素小于左边子序列当前元素, 取右半边元素*/
arr[k] = temp[j++];
}else {
/*右边子序列当前元素大于等于左边子序列当前元素,取左半边元素*/
arr[k] = temp[i++];
}
}
}
三、稳定性、复杂度及适用场景
3.1 稳定性
归并排序是一种稳定的排序算法,因为在拆、合的过程中,如果多个元素相等,不用移动其相对位置,就可完成排序过程。
3.2 时间复杂度
因为归并排序使用的是分治的思想,所以其时间复杂度为O(nlogn)。
3.3 适用场景
待排序序列中元素较多,并且要求较快的排序速度时。