一、基本思想
归并排序和快速排序都使用了分治法,分治法的策略是将一个规模为n的大问题分解成k个相同的子问题,这些子问题互相独立且与原问题性质相同,然后分别求解这些子问题,最后将这些子问题的解合并后得到原问题的解。
归并排序的的最坏时间复杂度为O ( N log N),快速排序的最坏时间复杂度为O ( N² ),但在某些情况下快速排序比归并排序的速度更快,算法之间的比较留待后续再谈,此处略过(或直接参阅《数据结构与算法分析.Java语言描述》一书)。
二、步骤描述
1、分解问题:如一个序列有n个元素,先将此序列拆分成两组。如果n为偶数,两组数量相等;如果n为奇数,其中一组的数量多1。重复分解过程,直到每组元素数量等于1为止。
2、合并求解:两两比较相邻的组的元素,并根据比较结果进行排序;反复求解并合并排序结果,直到重新归并为一个序列。
三、步骤图示
四、Java代码
public class MergeSort {
public static void sort(int[] array){
int length = array.length;
int[] tmpArray = new int[ length ];
sort(array, tmpArray, 0, length-1);
}
public static void sort(int[] array, int[] tmpArray, int left, int right){
if(left < right){
int center = (left + right)/2; //取中间值
sort(array, tmpArray, left, center); //递归分解
sort(array, tmpArray, center+1, right); //递归分解
merge(array, tmpArray, left, center+1, right); //合并排序
}
}
private static void merge(int[] array, int[] tmpArray, int leftStart, int rightStart, int rightEnd) {
int leftEnd = rightStart - 1; //左侧数组截止下标
int tmpPos = leftStart; //数组坐标
int total = rightEnd - leftStart + 1; //需要合并的数组元素数量
while(leftStart <= leftEnd && rightStart <= rightEnd){
if(array[ leftStart ] <= array[ rightStart ]){
//如果左侧数组元素小于或等于右侧数组元素,将左侧数组元素的值存入临时数组,并移动左侧数组下标
tmpArray[ tmpPos++ ] = array[ leftStart++ ];
}else{
//如果左侧数组元素大于右侧数组元素,将右侧数组元素的值存入临时数组,并移动右侧数组下标
tmpArray[ tmpPos++ ] = array[ rightStart++ ];
}
}
//如果左侧数组元素没有全部存入临时数组,将剩余元素循环写入临时数组
while(leftStart <= leftEnd){
tmpArray[ tmpPos++ ] = array[ leftStart++ ];
}
//如果右侧数组元素没有全部存入临时数组,将剩余元素循环写入临时数组
while(rightStart <= rightEnd){
tmpArray[ tmpPos++ ] = array[ rightStart++ ];
}
//将临时数组中排序好的元素写入原数组
for(int i = 0; i < total; i++, rightEnd-- ){
array[ rightEnd ] = tmpArray[ rightEnd ];
}
}
}
public class TestMergeSort {
public static void main(String[] args) throws InterruptedException{
long time1 = System.currentTimeMillis();
int length = 200000000;
int[] array = new int[length];
Random random = new Random();
for(int i = 0; i < length; i++){
int x = random.nextInt();
array[i] = x;
}
long time2 = System.currentTimeMillis();
System.out.println("生成数组时间:" + (time2 - time1) + "毫秒");
//归并排序
MergeSort.sort(array);
long time3 = System.currentTimeMillis();
System.out.println("归并排序时间" + (time3 - time2) + "毫秒");
}
}
两亿整数归并排序单线程测试结果
生成数组时间:3797毫秒
归并排序时间:61163毫秒
六、多线程测试
最近在看MapReduce,分布式排序是通过key的hashCode取模分别映射到不同的计算节点,然后将key对应的序列传递到该节点,分别排序后再统一归并排序结果,其实也是分治法的一种体现。所以在想,是不是可以利用电脑的多处理器开启多个线程来分别排序,然后再在主线程中合并排序结果,于是有了下面的尝试。
因为我的电脑cpu为双核,所以将数组分解成两个等量数组。
import java.util.Random;
import java.util.concurrent.CountDownLatch;
public class TestMergeSort {
public static void main(String[] args) throws InterruptedException{
long time1 = System.currentTimeMillis();
int length = 200000000;
int[] array = new int[length];
Random random = new Random();
for(int i = 0; i < length; i++){
int x = random.nextInt();
array[i] = x;
//System.out.println(x);
}
long time2 = System.currentTimeMillis();
System.out.println("生成数组时间:" + (time2 - time1) + "毫秒");
int minLength = length/2;
int[] a = new int[minLength];
int[] b = new int[minLength];
for(int i=0; i<2; i++){
int start = minLength * i;
int end = minLength * (i + 1);
if(i==0){
for(int j=start, k=0; j<end; j++,k++){
a[k] = array[j];
}
}else if(i==1){
for(int j=start, k=0; j<end; j++,k++){
b[k] = array[j];
}
}
}
//使用CountDownLatch来确保两个子线程都处理完毕后才执行最后的归并操作
CountDownLatch latch = new CountDownLatch(2);
new Thread(new Runnable(){
@Override
public void run() {
MergeSort.sort(a);
latch.countDown();
}
}).start();
new Thread(new Runnable(){
@Override
public void run() {
MergeSort.sort(b);
latch.countDown();
}
}).start();
//等待
latch.await();
//合并两个有序序列
merge(a, b, array);
long time3 = System.currentTimeMillis();
System.out.println("归并排序时间:" + (time3 - time2) + "毫秒");
}
//合并序列
private static void merge(int[] a1, int[] a2, int[] tmpArray){
int length1 = a1.length;
int length2 = a2.length;
int left = 0;
int right = 0;
int pos = 0;
while(left < length1 && right < length2){
if(a1[left] <= a2[right]){
tmpArray[pos] = a1[left];
left++;
}else{
tmpArray[pos] = a2[right];
right++;
}
pos++;
}
while(left < length1){
tmpArray[ pos++ ] = a1[ left++ ];
}
while(right < length2){
tmpArray[ pos++ ] = a2[ right++ ];
}
}
}
两亿整数归并排序双线程测试结果
生成数组时间:3874毫秒
归并排序时间:37743毫秒
双线程比单线程快了40%左右,如果CPU核心更多,开启更多的线程应该可以更快。但此时
空间复杂度也由单线程时的O(N)变成了多线程时的O(2N),典型的空间换时间(当线程数量>=2时,空间复杂度均为O(2N))。
七、其它
归并排序非常适用于在Java语言中进行对象序列的排序操作,原因是归并排序在大多数情况下比其它排序算法的比较次数要少,而对象之间的比较按照术语的说法是一个非常昂贵的操作。具体支持泛型的代码就不再写了,各位有兴趣可以自行去实现。
归并排序还有非递归的方式,留待后面再写。
最后,如文中有谬误之处,请留言指正,谢谢!