算法01之归并排序及多线程测试

本文介绍了归并排序的基本思想、步骤,并通过Java代码实现。对比了单线程与多线程环境下,对两亿整数进行归并排序的时间消耗,展示了多线程在排序中的效率提升,同时讨论了空间复杂度的变化。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、基本思想
    归并排序和快速排序都使用了分治法分治法的策略是将一个规模为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语言中进行对象序列的排序操作,原因是归并排序在大多数情况下比其它排序算法的比较次数要少,而对象之间的比较按照术语的说法是一个非常昂贵的操作。具体支持泛型的代码就不再写了,各位有兴趣可以自行去实现。
归并排序还有非递归的方式,留待后面再写。
    最后,如文中有谬误之处,请留言指正,谢谢!
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值