归并排序

分治法与归并排序

分治法

许多有用的算法在结构上是递归的:为了解决一个给定的问题,算法一次或多次递归地调用其自身以解决紧密相关的若干子问题。这些算法使用的就是分治思想

1.先将先原问题分解成若干子问题,这些子问题是原问题规模较小的实例
2.对这小较小问题进行求解,分解到最后小到不能最小时能够直接求解
3.将这些子问题的解进行合并和汇总后就得到了原问题的解

归并排序

归并算法就是这样一种使用了分治算法进行求解问题的算法,它的思想就是将一组需要排序的序列一分为二,左边右边都递归地调用归并排序排好序后进行合并。待排序的序列只有一个的时候可以直接排序,然后递归回升将这些小的有序序列给合并起来,从而整体排序成功。

合并这个过程就是,现在有两堆扑克牌面朝下,而且都是有序的,从大到小堆起来(最顶上的是最小的)。现在需要将这两堆给合并起来产生一堆有序的扑克牌,这就是归并排序中合并需要做的事情。做法是,左右都翻开最顶上一张牌,把较小的放到新的堆中。不断的比较最顶上的牌,直到将这两堆都放入到新堆中,此时排序就结束了,出现了一堆有序的扑克牌。

归并排序.gif

仔细看代码实现就能够理解了

代码实现

/**
 * author waigo
 * create 2021-02-03 22:20
 */
public class MergeSort {
    /**
     *
     * @param A 待排序的数组
     * @param p 待排序区间开始下标
     * @param q 中间分界下标
     * @param r 待排序区间结束下标
     */
    public static void merge(int[] A, int p, int q, int r) {
        //创建新的两个数组,分别存有一半的数据
        int n1 = q - p + 1;//第一堆的个数
        int n2 = r - q;
        int[] L = new int[n1 + 1], R = new int[n2 + 1];
        for (int i = 0; i < n1; i++)
            L[i] = A[p + i];
        for (int i = 0; i < n2; i++)
            R[i] = A[q + i + 1];
        //放置哨兵牌,这个有大用,免去了后面复杂的判断下标是否溢出
        L[n1] = Integer.MAX_VALUE;
        R[n2] = Integer.MAX_VALUE;
        //总共需要循环r-p+1次就是将两堆牌全部复制回原先的数组中
        int leftTop = 0, rightTop = 0;//两个指针用来遍历两个子数组
        for (int i = p; i <= r; i++)//需要排序的总数是r-p+1
            //找到两堆顶上较小那个放回,此时两堆都是有序的
            //由于最后一个是哨兵值为最大的,所以是不会作为较小的,所以会一直卡在这里指针不会移动也就不会溢出
            A[i] = L[leftTop] <= R[rightTop] ? L[leftTop++] : R[rightTop++];
    }

    public static void mergeSort(int[] A, int p, int r) {
        //如果p>=r则此时需要处理的至多一个元素,一个元素是有序的
        if (p < r) {
            //分治法,对左半边排序,对右半边排序,然后两边有序之后使用merge进行排序
            int q = (p + r) / 2;//处理区间的中间下标
            mergeSort(A, p, q);//左半区A[p...q]
            mergeSort(A, q + 1, r);//右半区A[q+1...r]
            merge(A, p, q, r);//左右都排好序之后就可以使用merge进行归并排序
        }
    }
}

测试代码

public class MergeSortTest {
    @Test
    public void mergeTest() {
        int[] arr = RandomProducer.produceRandom(-500, 100000000, 8000000);
        Date from = new Date();
        MergeSort.mergeSort(arr, 0, arr.length - 1);
//        Logger.getGlobal().info("排序后:"+Arrays.toString(a));
        System.out.println("运行了" + (new Date().getTime() - from.getTime()) + "毫秒");
        //使用800,0000个随机数序列进行测试,发现归并排序差不多需要1650ms,这个速度是比快排的1200ms要慢的,
        //但是比希尔排序的2500ms要快很多
    }
}

非递归实现

递归版本通过Master公式T(N) = a * T(N/b) + O(N^d)

T(n) = 2*T(n/2)+O(N)-->log(b,a)==log(2,2)==d==1,所以时间复杂度O(N)=N*logN

通过改写非递归版本可以更加直观看出确实时间复杂度是O(N*logN)

public static void mergeSort1(int[] nums) {
    int N = nums.length;
    int mergeSize = 1;//指的是左边有序的长度
    //每次merge的大小是2*mergeSize,所以若是mergeSize>N/2就表示上一次已经完成排序了
    // (错误,比如数组长度是3,3/2=1,很明显mergeSize=1后还没排序好)
    while (mergeSize < N) {//logN,每次乘以2追赶,相对于求N个节点的树高为logN
        int L = 0;
        while (L < N) {
            //M表示这一次merge的中点,因为上一次完成了mergeSize的merge,所以M要在上一次完成了merge区间的最后
            //M-L+1表示L和M夹起来的区间[L...M]的大小等于mergeSize,M-L+1=mergeSize
            int M = mergeSize + L - 1;
            if (M >= N) break;//表示左边都不够mergeSize了,完成一个merge至少需要区间size>mergeSize
            //R表示这次merge的右边点,应该是一个mergeSize结尾,R-M = mergeSize,这个区间不包括M
            //但是可能这右边可能不够mergeSize大小了
            int R = Math.min(M + mergeSize, N - 1);
            merge(nums, L, M, R);
            L = R + 1;//开始下个区间
        }
        ////太细了,假如N的长度等于Integer.MAX_VALUE,那此时mergeSize乘以2会溢出的
        if (mergeSize > (N >> 1)) break;
        mergeSize <<= 1;//mergeSize *= 2
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。