70-归并排序

JDK的TimeSort就使用了归并排序。

归并排序是建立在归并操作上的一种有效的排序算法,该算法是采用分治法的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列,即先使每个子序列有序,再使子序列段间有序。若将2个有序表合并为一个有序表,称为二路归并。

归并排序:将集合排序时分为2大阶段。

  1. 归:将原集合不断拆分,拆分到每个小数组只剩下一个元素时,拆分过程就结束。
  2. 并:将拆分后的小数组不断合并,直到合并到整个数组,此时整个数组已经有序。

最关键就是合并过程:没有用到任何排序方法,只是元素比较。

递归版本:

/**
 * 在arr上进行归并排序 稳定的
 * @param arr
 */
public static void mergeSort(int[] arr) {
    mergeSortInternal(arr, 0, arr.length - 1);
}

/**
 * 在arr[l...r]上进行归并排序
 * @param arr
 * @param l
 * @param r
 */
private static void mergeSortInternal(int[] arr, int l, int r) {
    //优化① 减少若干次递归的过程
    if(r - l <= 15){
        //拆分后的小区间直接使用插入排序,不再递归
        insertBase(arr, l, r);
        return;
    }

//    if(l >= r){
//        //区间只剩下一个元素,整个区间有序,不需要再排序
//        return;
//    }

    //若r和l都特别大的时候,有溢出的风险 (r + l) >> 1
    int mid = l + ((r - l) >> 1); //会规避溢出风险
    //在拆分后的两个小数组上使用归并排序
    //先排序左半区间
    mergeSortInternal(arr, l, mid);
    //再排序右半区间
    mergeSortInternal(arr, mid + 1, r);

//    //此时左半区间和右半区间已经有序,只需要合并两个小区间即可
//    merge(arr, l, mid, r);

    //优化② 减少若干次合并的过程
    //arr[mid]是左区间的最后一个元素,arr[mid + 1]是右区间的第一个元素
    //不是上来就和并,当两个小区间之间存在乱序时才合并
    //arr[mid] < arr[mid + 1],说明左区间已经小于右区间的所有值,整个区间已经有序,不需要merge
    if(arr[mid] > arr[mid + 1]) {
        merge(arr, l, mid, r);
    }
}

/**
 * 将已经有序的arr[l...mid]和arr[mid + 1...r]合并为一个大的有序数组arr[l...r]
 * @param arr
 * @param l
 * @param mid
 * @param r
 */
private static void merge(int[] arr, int l, int mid, int r) {
    //假设此时l = 1000, r = 2000
    //开辟一个大小和合并后数组大小相同的数组
    int[] temp = new int[r - l + 1];
    //将原数组内容拷贝到新数组中
    for (int i = l; i <= r; i++) {
        //temp[0] = arr[1000]
        //新数组的索引和原数组的索引有l个单位的偏移量
        temp[i - l] = arr[i];
    }
    //遍历原数组,选择左半区间和右半区间的最小值写回原数组
    //i对应于左半有序区间的第一个索引
    int i = l;
    //j对应右半区间的第一个索引
    int j = mid + 1;
    //k表示当前处理到原数组的哪个位置
    for (int k = l; k <=r; k++) {
        if(i > mid){
            //此时左半区间已经全部处理完毕,将右半区间的所有值写回原数组
            arr[k] = temp[j - l];
            j++;
        } else if(j > r) {
            //此时右半区间已经全部处理完毕
            arr[k] = temp[i - l];
            i++;
        } else if(temp[i - l] <= temp[j - l]) {
            arr[k] = temp[i - l];
            i++;
        } else {
            arr[k] = temp[j - l];
            j ++;
        }
    }
}

稳定性:稳定的。

数组拆分不会造成元素乱序,元素的相对位置不会发生移动,当最终合并时,<=值默认放在左区间,因此合并过程也是稳定的。

时间复杂度:O(nlogn)

  • 拆分过程:原数组长度为n,不断拆分数组,将元素一分为2,直到数组长度为1。n/2/.../2/2==1,总共拆分次数是logn。
  • 合并过程:最终合并的大数组的长度为N,遍历N次,才能将数组元素合并完。

归并排序和堆排序一样,都是非常稳定的O(nlogn)的排序算法,不管集合元素是否有序,上来就将集合一分为二。

看到nlogn时间复杂度,想到树结构(不一定是二叉树)。logn结构一定都和树有关,此处归并排序和快速排序,logn的原因都在于递归树。

非递归版本:自下而上

/**
 * 归并排序的非递归版本
 * @param arr
 */
public static void mergeSortNonRecursion(int[] arr) {
    //sz表示每次合并的元素个数,最开始从1个元素开始合并,(每个数组只有一个元素)
    //第二次循环时,合并的元素个数就成了2(每个数组有两个元素)
    //第三次循环时,合并的元素个数就成了4(每个数组有四个元素)
    for (int sz = 1; sz <= arr.length; sz = sz + sz) { //个数和数组长度可以相等
        //merge过程,i表示每次merge开始的索引下标
        for (int i = 0; i + sz < arr.length; i += sz + sz) { //索引比数组长度小1
            //i + sz表示第二个小数组的开始索引 < n 表示还有右半区间要合并
            //当sz长度过大时,i + 2sz - 1会超过数组长度了
            merge(arr, i, i + sz - 1, Math.min(i + 2 * sz - 1, arr.length - 1) ); //数组长度和索引有一个差值
            //Math.min(i + 2 * sz - 1, arr.length - 1)极端情况:当sz = arr.length时,要合并整个数组,2 * sz = 2n,没这个索引,此时我们就取arr.length - 1作为合并的最右侧索引值
        }
    }
}

 

你可能感兴趣的:(Java数据结构,排序算法,算法,数据结构)