归并排序及其优化

Q:什么是归并排序?
A:它是建立在归并操作上的一种有效的排序算法;是采用分治法的一个非常典型的应用;是一种稳定的

基本思想

要将一个数组排序,可以先(递归地)将它分成两半分别排序,然后将结果归并起来。

优点?它能保证将任意长度为 N 的数组排序所需时间和 NlogN 成正比;

缺点?所需的额外空间和 N 成正比。

原地归并的抽象方法

Q:为什么需要原地归并?
A:因为用归并将一个大数组排序时,需要进行多次归并,而且每次归并会都创建一个新数组来存储排序结果会带来问题。

Q:原地归并实现了什么?
A:可以先将前半部分排序,再将后半部分排序,然后数组中移动元素而不需要使用额外的空间(将两个有序的数组归并为一个有序的数组)

Q:如何实现归并?
A:创建一个适当大小的数组,然后将两个输入数组中的元素一个个从小到大方法这个数组中。

代码实现
根据排序算法类的模板实现选择排序(提醒:点蓝字查看详情)

    /**
     * 将子数组 arr[lo...mid] 和 arr[mid+1...hi] 归并成一个有序的数组并将结果存放在 arr[lo...hi] 中。
     * 将所有元素复制到一个辅助数组中,再把归并的结果放回原数组中
     */
    private static void merge(Comparable[] arr, int lo, int mid, int hi) {
        // 将 arr[lo...mid] 和 arr[mid+1...hi] 归并
        int indexI = lo;
        int indexJ = mid + 1;
        // 将 a[lo...hi] 复制到 aux[lo...hi]
        // System.arraycopy(arr, lo, aux, lo, hi - lo + 1);
        for (int indexK = lo; indexK <= hi; indexK++) {
            aux[indexK] = arr[indexK];
        }
        // 归并回到 arr[lo...hi]
        for (int indexK = lo; indexK <= hi; indexK++) {
            // 左半边用尽(取右半边的元素)
            if (indexI > mid) {
                arr[indexK] = aux[indexJ++];
            }
            // 右半边用尽(取左半边的元素)
            else if (indexJ > hi) {
                arr[indexK] = aux[indexI++];
            }
            // 右半边的当前元素小于左半边的当前元素(取右半边的元素)
            else if (less(aux[indexJ], aux[indexI])) {
                arr[indexK] = aux[indexJ++];
            }
            // 右半边的当前元素大于左半边的当前元素(取左半边的元素)
            else {
                arr[indexK] = aux[indexI++];
            }
        }
    }

自顶向下的归并排序(化零为整,递归解决)

由于以上的原地归并只能将两个有序的数组归并成一个有序的数组,所以得基于原地归并的抽象去实现一种递归归并。

要对子数组 arr[lo…hi] 进行排序,先将它分为 arr[lo…mid] 和 arr[mid+1…hi] 两部分,分别通过递归调用将它们单独排序,最后将有序的子数组归并为最终的排序结果。

Q:为什么它能将正确的排序?
A:如果它能将两个子数组排序,那么它就可以通过归并两个子数组来将整个数组排序。

运行轨迹

自顶向下的归并排序运行轨迹

代码实现

根据排序算法类的模板实现选择排序(提醒:点蓝字查看详情)

    private static Comparable[] aux;    // 归并所需的辅助数组

    public static void sort(Comparable[] arr) {
        aux = new Comparable[arr.length]; // 一次性分配空间
        sort(arr, 0, arr.length - 1);
    }

    private static void sort(Comparable[] arr, int lo, int hi) {
        // 将数组 arr[lo...hi] 排序
        if (hi <= lo) return;
        int mid = lo + ((hi - lo) >> 1);
        sort(arr, lo, mid);          // 将左半边排序
        sort(arr, mid + 1, hi);  // 将右半边排序
        merge(arr, lo, mid, hi);     // 归并结果
    }
性能分析

最佳情况:T(n) = O(n)
最差情况:T(n) = O(nlogn)
平均情况:T(n) = O(nlogn)

对于长度为 N 的任意数组,自顶向下的归并排序需要 1/2NlgN - NlgN 次比较

对于长度为 N 的任意数组,自顶向下的归并排序最多需要访问数组 6NlgN 次(2N 次用来复制、2N 次用来将排好序的元素移动回来、另外最多比较 2N 次)。

Q:主要缺点是什么
A:辅助数组所使用的额外空间和 N 的大小成正比。

自底向上的归并排序(循序渐进的解决)

实现归并的另一种方法:先归并那些微型数组,然后再成对归并得到子数组。首先两两归并,然后四四归并,然后八八归并,一直下去。

运行轨迹

代码实现

根据排序算法类的模板实现选择排序(提醒:点蓝字查看详情)

    private static Comparable[] aux;    // 归并所需的辅助数组

    public static void sortBU(Comparable[] arr) {
        int N = arr.length;
        aux = new Comparable[N];
        // sz 的初始值为 1 , 每次加倍
        for (int sz = 1; sz < N; sz = sz + sz) {            // sz子数组大小
            for (int lo = 0; lo < N - sz; lo += sz + sz) {  // lo:子数组索引
                // 最后一个子数组的大小,只有在数组大小是 sz 的偶数倍时,才会等于sz,否则会比 sz 小
                merge(arr, lo, lo + sz - 1, Math.min(lo + sz + sz - 1, N - 1));
            }
        }
    }
性能分析

对于长度为 N 的任意数组,自底向上的归并排序需要 1/2NlgN - NlgN 次比较,最多访问数组 6NlgN 次。(每一边访问数组 6N 次,比较次数 N/2 - N)

当数组长度为 2 的幂时,自顶向下和自底向上的归并排序所用的比较次数数组访问次数正好相同,只是顺序不同。

自底向上的归并比较适合用链表组织的数据。

总结

没有任何基于比较的算法能够保证使用少于 lg(N!) - NlgN 次比较将长度为 N 的数组排序。

归并排序是一种渐进最优的基于比较排序的算法。

优化方案

①、直接将辅助数组作为参数传入,而不直接使用静态数组。
②、对小规模子数组使用插入排序,一般可以将归并排序的时间缩短 10% ~ 15%;
③、判断测试数组是否已经有序,如果 arr[mid] <= arr[mid+1],我们就认为数组已经是有序的并跳过merge() 方法,可以是任意有序的子数组算法的运行时间变为线性的。
④、merge() 方法中不将元素复制到辅助数组,节省数组复制的时间。调用两种排序方法,一种:将数据从输入数组排序到辅助数组;另一种:将数据从辅助数组排序到输入数组。
重点:在每个层次交换输入数组和辅助数组的角色。

优化代码
/**
 * 归并排序优化方案(其实并不是特别明显,稳定性也不好)
 *
 * @author TinyDolphin
 * 2017/11/6 11:45.
 */
public class MergePlus {

    // 经验之谈:数组的长度为 7 时,切换
    private static final int CUTOFF = 7;

    private static void merge(Comparable[] src, Comparable[] dst, int lo, int mid, int hi) {
        int indexI = lo;
        int indexJ = mid + 1;
        for (int indexK = lo; indexK <= hi; indexK++) {
            if (indexI > mid) {
                dst[indexK] = src[indexJ++];
            } else if (indexJ > hi) {
                dst[indexK] = src[indexI++];
            } else if (less(src[indexJ], src[indexI])) {
                dst[indexK] = src[indexJ++];
            } else {
                dst[indexK] = src[indexI++];
            }
        }
    }

    // 将数组 arr 排序到数组 aux
    private static void sort(Comparable[] src, Comparable[] dst, int lo, int hi) {
        // 优化方案②:应该在子数组长度为 7 的时候切换到插入排序
        if (hi <= lo + CUTOFF) {
            insertionSort(dst, lo, hi);
            return;
        }
        int mid = lo + ((hi - lo) >> 1);

        // 优化方案④:在每个层次交换输入数组和辅助数组的角色
        sort(dst, src, lo, mid);
        sort(dst, src, mid + 1, hi);

        //优化方案③:判断测试数组是否已经有序
        if (!less(src[mid + 1], src[mid])) {
            System.arraycopy(src, lo, dst, lo, hi - lo + 1);
            return;
        }

        // 优化方案④:merge() 方法中不将元素复制到辅助数组
        merge(src, dst, lo, mid, hi);
    }

    public static void sort(Comparable[] arr) {
        // 优化方案①:直接将辅助数组作为参数传入
        Comparable[] aux = arr.clone();
        sort(aux, arr, 0, arr.length - 1);
    }

    private static void insertionSort(Comparable[] arr, int lo, int hi) {
        for (int indexI = lo; indexI <= hi; indexI++) {
            for (int indexJ = indexI; indexJ > lo && less(arr[indexJ], arr[indexJ - 1]); indexJ--) {
                exch(arr, indexJ, indexJ - 1);
            }
        }
    }

    /**
     * 比较两个元素的大小
     *
     * @param comparableA 待比较元素A
     * @param comparableB 待比较元素B
     * @return 若 A < B,返回 true,否则返回 false
     */
    private static boolean less(Comparable comparableA, Comparable comparableB) {
        return comparableA.compareTo(comparableB) < 0;
    }

    /**
     * 将两个元素交换位置
     *
     * @param arr    待交换元素所在的数组
     * @param indexI 第一个元素索引
     * @param indexJ 第二个元素索引
     */
    private static void exch(Comparable[] arr, int indexI, int indexJ) {
        Comparable temp = arr[indexI];
        arr[indexI] = arr[indexJ];
        arr[indexJ] = temp;
    }

    /**
     * 打印数组的内容
     *
     * @param arr 待打印的数组
     */
    private static void show(Comparable[] arr) {
        for (int index = 0; index < arr.length; index++) {
            System.out.print(arr[index] + " ");
        }
        System.out.println();
    }

    /**
     * 判断数组是否有序
     *
     * @param arr 待判断数组
     * @return 若数组有序,返回 true,否则返回 false
     */
    public static boolean isSort(Comparable[] arr) {
        for (int index = 1; index < arr.length; index++) {
            if (less(arr[index], arr[index - 1])) {
                return false;
            }
        }
        return true;
    }
}
优化测试代码

高效复制数组的方法】,提示:点击蓝色字体查看方法详情。

public class Main {
    public static void main(String[] args) {
        int length = 10000000;  // 千万数据量级别
        Integer[] arr = new Integer[length];
        Integer[] arr2 = new Integer[length];
        for (int index = 0; index < length; index++) {
            arr[index] = new Random().nextInt(length) + 1;
        }
        //高效复制数组的方法
        System.arraycopy(arr, 0, arr2, 0, arr.length);

        long start = System.currentTimeMillis();
        Merge.sort(arr);
        long end = System.currentTimeMillis();
        System.out.println("耗费时间:" + (end - start) + "ms");
        assert Merge.isSort(arr);

        start = System.currentTimeMillis();
        MergePlus.sort(arr2);
        end = System.currentTimeMillis();
        System.out.println("耗费时间:" + (end - start) + "ms");
        assert MergePlus.isSort(arr2);

    }

}
优化测试结果

不稳定,优化之后的代码大多数时候是慢于优化之前的代码。具体原因,笔者也不太清楚。
千万级别数据量

注意:编译器默认不适用 assert 检测(但是junit测试中适用),所以要使用时要添加参数虚拟机启动参数-ea 具体添加过程,请参照eclipse 和 IDEA 设置虚拟机启动参数

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
当涉及到排序方法时,有很多种方法可以选择。下面是几种常见的排序方法及其优化方法: 1. 冒泡排序:冒泡排序是一种简单直观的排序方法,但其时间复杂度为 O(n^2),在大数据量的情况下效率较低。优化方法有: - 增加一个标志位,记录是否有交换操作发生,如果没有,则表明已经排好序,可以提前结束循环。 - 每一轮循环时记录最后一次交换的位置,这个位置后面的元素已经排好序,下一轮循环时可以省略这部分。 2. 快速排序:快速排序是一种基于分治思想的排序方法,其时间复杂度为 O(nlogn),但最坏情况下时间复杂度为 O(n^2)。优化方法有: - 选择合适的枢轴元素,可以使用三数取中或随机选择等方法来避免最坏情况的发生。 - 当划分出的两个子数组的大小小于某个阈值时,可以使用插入排序等简单排序方法来代替快速排序,以避免递归过深。 3. 归并排序归并排序是一种基于分治思想的排序方法,其时间复杂度为 O(nlogn)。优化方法有: - 在合并两个有序数组时,可以使用双指针法,避免使用额外的数组空间。 - 当数组大小小于某个阈值时,可以使用插入排序等简单排序方法来代替归并排序,以避免递归过深。 4. 堆排序:堆排序是一种基于堆数据结构的排序方法,其时间复杂度为 O(nlogn)。优化方法有: - 在建堆时,可以使用自底向上的方法,避免对已经排好序的元素重复进行调整。 - 在堆排序过程中,可以使用递归或迭代两种方式来实现。 总的来说,优化排序方法的关键在于选择合适的排序算法和对其进行合理的改进,以提高排序的效率和性能。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值