码迷,mamicode.com
首页 > 编程语言 > 详细

归并排序

时间:2019-01-12 19:01:08      阅读:135      评论:0      收藏:0      [点我收藏+]

标签:遍历   基本   快排   次数   基于   对比   分治策略   oid   master   

分治法

归并排序是完全遵循分治策略的排序算法。什么是分治法?

分治法,即将原问题分解为几个规模较小的子问题,递归的求解这些子问题,之后再合并这些子问题的解,最终得到原问题的解。

归并排序

归并排序遵照分治法的思想,可分为三个步骤:

  • 分解,将大小为\(n\)的数列分为两个大小为\(\frac{n}{2}\)的子数列
  • 使用归并排序递归排序子数列
  • 合并已排序的子数列,完成原数列的排序

同时,我们知道递归不可能无线递归下去,当分解到基本情形的时候,我们可以直接获得该问题的答案,这也是递归的出口。如果一个问题永远无法递归到基本情形,则分治法不适用。

归并排序(merge sort)的基本情形为:子数列的大小为1时,此时数列天生已排序好,开始向上回溯。

技术分享图片

上图可见,排序数列\((38,27,43,3,9,82,10)\),我们不断的分拆数列,直至每个子数列大小为1。然后,再将子数列两两归并,逐步回溯。最终,完成排序。

关键伪代码

以上对归并的描述可以总结以下步骤:

    merge-sort(A):
        if(A.length >= 1)
            array A divide into array L and array R
            merge-sort(L);
            merge-sort(R);
            merge(L,R);

可以看到,核心思想就是将数组Adivide成两个子数组,在对子数组进行归并。然后merge L和R两个子数组。

merge的核心伪代码如下:

for k in (1...A.length):
    if L[i] <= R[j]
        A[k] = L[i];
        ++i;
    else
        L[i] >= R[j]
        A[k] = R[j];
        ++j;

merge方法也是归并排序的核心部分,但是他的思想却很简单。

由于子数组LR都是有序的,因此只需要逐个对比两个数组的元素,其中小的元素放进数组A中,直到数组A被填满为止。

代码实现(Java)

/**
 * @author: luzj
 * @date:
 * @description: 归并排序
 * 大致思想:
 * 0 将数组A拆成B和C
 * 1 对B和C单独排序
 * 2 合并B和C成一个有序数组
 * 3 B和C的排序方式同A,此为递归的基础
 */
public class MergeSort {

    /**
     * 排序
     * @param A
     * @param p
     * @param r
     */
    void sort(Integer[] A, int p, int r) {
        if (p < r) {//开始回归的基本条件,确保最小数组大小为1结束递归
            int q = Math.floorDiv(p + r, 2);
            sort(A, p, q);
            sort(A, q + 1, r);
            merge(A, p, q, r);
        }
    }

    /**
     * 合并两个子数组
     * 其实是将左右两个已经排序的子数组,进行原址排序
     * @param A
     * @param p 左边子数组的边界
     * @param q 左右子数组的分界点
     * @param r 右边子数组的边界
     * 该方法的增长率为:O(n)
     */
    void merge(Integer[] A, int p, int q, int r) {
        int n1 = q - p + 1;//左边子数组的元素个数
        int n2 = r - q; //右边子数组的元素个数

        //将A数组拆成两个数组来装
        Integer[] L = new Integer[n1 + 1];
        Integer[] R = new Integer[n2 + 1];
        for (int i = 0; i < n1; i++) {
            L[i] = A[p + i];
        }

        for (int j = 0; j < n2; j++) {
            R[j] = A[q + j + 1];
        }

        //哨兵,当某个子数组已经遍历完后还接着遍历时,哨兵将很好的阻止越界并且将遍历转向另一个子数组
        L[n1] = Integer.MAX_VALUE;
        R[n2] = Integer.MAX_VALUE;
        int i = 0, j = 0;

        //重排,每次都进行比较,将较小的装进A数组(如果是降序排列,则塞较大的)
        for (int k = p; k <= r; k++) {
            if (L[i] <= R[j]) {
                A[k] = L[i];
                i++;
            } else {
                A[k] = R[j];
                j++;
            }
        }
    }

    public static void main(String[] args) {
        Integer[] A = {5,2,4,7,1,3,2,6};
        new MergeSort().sort(A,0,A.length-1);
        for (int i = 0; i < A.length; i++) {
            System.out.print(A[i]+" ");
        }
    }
}

时间复杂度

归并排序的时间复杂度为:\(nlg(n)\)

我们可以大致算一笔账:

每一次数组将会分裂成两份,即从中间一分为二,最终形成一个二叉树。那么这个二叉树多少层呢,答案是\(log_2{n}+1\)

同时注意到,每一次递归的时间复杂度都来自merge(A) 方法,而merge方法复杂度为\(c*{n_i}\).每一层都有\(c({n_1+n_2+...+n_i})\),即\(c*(n)\)的复杂度。因此,可以看到每一层不管有多少个节点,他的复杂度都是\(c*{n}\).

最终可得,merger-sort的复杂度为:\(O(nlg({n}))\)

这里还有一个结论,就是所有基于比较的排序方法,他的性能上限都是\(O(n*lg{(n)})\),因此快排的平均时间复杂度也不超过这个数。在《算法导论》中,有一个基于决策树的精彩论述,有兴趣的同学可以研读一下。

小结

归并排序使用分治法,时间复杂度为\(nlg(n)\)

参考

归并排序

标签:遍历   基本   快排   次数   基于   对比   分治策略   oid   master   

原文地址:https://www.cnblogs.com/Franken-Fran/p/merger-sort.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!