归并排序--一种高级排序算法

归并排序(mergeSort)介绍

归并排序是一种高级排序算法,速度仅次于快速排序,为稳定排序算法,没有对应的简单排序算法。

思路推演:

1、想法应该是从发现两个有序数组合成一个有序数组是O(n)开始的,如何将两个有序数据合成一个有序数组?依次从两个数组中取数进行比较,将较小的放入新数组。
准备两个索引i=0和j=0,逐个比较arrA[i]和arrB[j],将较小值给新数组,并将较小值所在数据的索引++,一旦某个索引++之后超出了数组的长度,说明这个数组全部放过去了,这时就可以将另一个数组余下值按次序放入新数组。
一次循环比较即可完成,上面的逻辑的时间复杂度是O(n)
2、假设将一个大数组,一分为2,二分为4,,,最终拆分成最多只有两个元素的数组,将这两个元素的数组比较一次,排个序。然后逐个顺序合并起来,就是归并排序。

时间复杂度:

O(log(n))
拆开数组,O(logn),2个元素的数组每个比较一次O(1)*n/2 = O(n/2),合并的时候会每一次合并是n,一共logn层,所以合并是O(nlogn),总时间是O(nlogn) + O(n/2) + O(logn),去除低次的就是O(nlogn)的时间复杂度,空间复杂度看怎么用,可以在每一次合并的时候创建一个temp数组,这样需要从2+ 4+ 8+ …+ n个长度的数组,也就是O(n^2)的空间。也可以从头到尾使用一个temp数组,在一开始的时候根据arr的length创建,带着走完整个算法,根据起止位置,分段取用。

稳定性:

稳定排序算法

属于相邻比较,交换,所以可以实现为稳定的。是否稳定只是说能不能实现稳定,假设对于两个元素的数组排序时,采用前面大等于后面则交换,那就是不稳定的,采用前面大于后面才交换,就是稳定的。
高级算法唯一能实现为稳定算法的。

代码思路:

这个代码生生去想,很难直接写出来,掉了不少次坑才写出来。
1、前面说的拆分成最多2个长度的数组和后续的比较合并,其实都可以在一个数组内完成,借助递归,一分为二、二分为四,也就是每次要从中间分开(当然也可以不从中间分开,哪怕我们强行分成前面2个元素和后面所有元素也可以,但是二分能达到logn。所以要理解算法,而不是死记硬背)。
2、传入开始index和结束index
3、判断startIndex是否小于endIndex,递归应该结束的情况是endIndex - startIndex == 1或者等于0,(最后发现这个其实也可以放到递归里,但是那是优化之后的,一开始按照最原始的算法思路来实现)
4、如果endIndex-startIndex > 1(比如0 1 2),那么数组就是可以继续拆分的,继续拆成前半部分和后半部分,将前半部分和后半部分分好后,merge成一个大数组。
5、伪代码

// 为了方便运算,endIndex第一次传入为(数组长度-1)。
mergeSort(arr, startIndex, endIndex){
    if(startIndex != endIndex){
        if(endIndex - startIndex == 1){
            // 将这个最小数组交换
        }else{
            int midIndex = (startIndex + endIndex) >> 1;
            //前半部分startIndex, midIndex
            //后半部分midIndex + 1, endIndex
            //合并
        }
    }
}

//
mergeArray(arr, startIndex, midIndex, endIndex){
// 根据这三个确定两个相邻的数据段,认为是两个数组,进行数据整合
// 这个时候每次需要新建一个长度为(endIndex - startIndex + 1)的temp数组,才方便合并,或者最开始新建一个大的长度为原始数组长度的temp数组,然后一直带着走。理论上后者效率更高一些。
}

Java代码实现:

public static void main(String[] args){
    int[] arr = new int[]{10, 1, 4, 2, 8, 3, 5};
    mergeSort(arr);
    System.out.println(Arrays.toString(arr));
}

/**
 * 作为一个引子,因为下面的方法需要用到递归,这样写好看一写,也可以直接写到main方法中
 * @param arr
 */
private static void mergeSort(int[] arr){
    doMergeSort(arr, 0, arr.length - 1);
}

/**
 * 递归 1 3 2 4
 * @param arr
 * @param startIndex
 * @param endIndex
 * @return
 */
private static void doMergeSort(int[] arr, int startIndex,int endIndex){
    // 等于当前数组的前半部分和后半部分先mergeSort再mergeArray
    // 前半部分和后半部分长度小于等于2(差小等于1)的时候,分拆的递归结束,开始往回收拢,逐步合成一个大数组
    if(startIndex < endIndex){
    // 找到中间位置
        int midIndex = (startIndex + endIndex) >> 1;
        doMergeSort(arr, startIndex, midIndex);
        doMergeSort(arr, midIndex + 1, endIndex);
        mergeArray(arr, startIndex ,midIndex,endIndex);
    }
    /*if(startIndex != endIndex){
        // 如果间隔是1,进行排序,如果间隔是0,不排序,如果间隔大于1继续分拆
        if(endIndex - startIndex == 1){
            if(arr[startIndex] > arr[endIndex]){
                int tmp = arr[startIndex];
                arr[startIndex] = arr[endIndex];
                arr[endIndex] = tmp;
            }
        }else{
            int midIndex = (startIndex + endIndex) >> 1;
            doMergeSort(arr, startIndex, midIndex);
            doMergeSort(arr, midIndex + 1, endIndex);
            mergeArray(arr, startIndex ,midIndex,endIndex);
        }
    }*/
}

/**
 * 假设数组为1 3 2 4或者 1 2 4
 *  代码难以直接写出来的时候,举几个简单的例子激发一下思路
 * @param arr
 * @param startIndex
 * @param midIndex
 * @param endIndex
 * @return
 */
private static void mergeArray(int[] arr, int startIndex, int midIndex,int endIndex){
    //
    int len = endIndex - startIndex + 1;
    int[] temp = new int[len];
    int i = startIndex;
    int j = midIndex + 1;
    int tempIndex = 0;
    // temp用来简化操作,先放到temp中,再全部丢到arr中
    // 直到一方走完
    while(true){
        if(arr[i] <= arr[j]){
            temp[tempIndex] = arr[i];
            i++;
        }else{
            temp[tempIndex] = arr[j];
            j++;
        }
        tempIndex++;
        if(i > midIndex){
            // j中剩下的不动,arr中的也不用动
            break;
        }
        if(j > endIndex){
            // i中剩下的需要全部移动到temp中国
            // 剩下的个数为m = midIndex - i + 1
            // 剩下的全部填满(也就是填满是len - m)
            for(int z = i; z <= midIndex;z++){
                temp[len - (midIndex - z + 1)] = arr[z];
                tempIndex++;
            }
            break;
        }
    }

   // merge过来,一直merge到startIndex + tempIndex,因为temp的有效数据到tempIndex(不包括tempIndex)
    for(i = startIndex; i < startIndex + tempIndex; i++){
        arr[i] = temp[i - startIndex];
    }
}

使用一个temp数组的写法

// 使用一个temp的归并排序
public static void main(String[] args) {
        int[] arr = new int[]{10, 8, 5, 1, 4, 2, 3};
        mergeSort(arr);
        System.out.println(Arrays.toString(arr));
    }

    private static void mergeSort(int[] arr) {
        int[] temp = new int[arr.length];
        doMergerSort(arr, 0, arr.length - 1, temp);
    }

    private static void doMergerSort(int[] arr, int startIndex, int endIndex, int[] temp) {
        if (startIndex < endIndex) {
            // 如果等于1,递归结束
            // 比较最小的元素,但是比较最小的元素,也可以看成两个长度为1的数组的合并,所以逻辑可以写到最后,直到startIndex == endIndex
//            if(endIndex - startIndex == 1){
//            }else{
//            }
            // 将左边进行mergeSort
            // 将右边进行mergeSort
            // 合并左右两边
            int midIndex = (startIndex + endIndex) >> 1;
            doMergerSort(arr, startIndex, midIndex, temp);
            doMergerSort(arr, midIndex + 1, endIndex, temp);
            mergeArray(arr, startIndex, midIndex, endIndex, temp);
        }
    }

    /**
     * 合并数组
     *
     * @param arr
     * @param startIndex
     * @param midIndex
     * @param endIndex
     * @param temp
     * @return
     */
    private static void mergeArray(int[] arr, int startIndex, int midIndex, int endIndex, int[] temp) {
        // temp数组的索引,从startIndex到最终停止的位置的数据将会被复制到arr
        int tempIndex = startIndex;
        // 左边数组起始位置
        int i = startIndex;
        // 右边数组起始位置
        int j = midIndex + 1;

        // 从两个数组中逐个取第一个未被选定的数进行比较,取较小值放入temp中
        while (true) {
            // 为保证稳定性,前面小于等于后面
            if (arr[i] <= arr[j]) {
                temp[tempIndex] = arr[i];
                i++;
            } else {
                temp[tempIndex] = arr[j];
                j++;
            }
            tempIndex++;

            // 如果i超出了边界,tempIndex会停留在j中的某个位置,不需要复制,循环停止
            if (i > midIndex) {
                break;
            }

            // 如果j超出了边界,需要将i中剩余的数据,复制到对应的位置
            if (j > endIndex) {
                // 复制数据
                for (; i <= midIndex; tempIndex++, i++) {
                    temp[tempIndex] = arr[i];
                }
                break;
            }
        }

        // 将temp中的数据复制到arr中,上面每次都是tempIndex++然后再退出循环,也就是tempIndex多加了1,需要注意边界
        for (; startIndex < tempIndex; startIndex++) {
            arr[startIndex] = temp[startIndex];
        }
}

猜你喜欢

转载自blog.csdn.net/u011531425/article/details/80530269