我所理解的分治法与它的经典实现——归并排序

分治法与归并排序

分治法基本概念:

分治法是计算机科学中一种很重要的算法,分治法的核心理念就是把一个复杂的问题分成一个个规模较小的问题,然后对这些小问题各个击破。

“分治”是一种思想,它不涉及具体的算法,大多数情况下分治都是靠递归来达到效果的

分治法特征:

分治法的几个特征:

  1. 该问题的规模缩小到一定的程度就可以容易地解决;

  2. 该问题可以分解为若干个规模较小的相同问题,即该问题具有最优子结构性质;(前提:这条特征是应用分治法的前提,同时也体现了递归的特征)

  3. 利用该问题分解出的子问题的解可以合并为该问题的解;(关键:能不能用分治法,关键就在于具不具备这个特征,如果前两条特征都具备,而这一条不具备可以考虑使用动态规划和贪心算法)

  4. 该问题所分解出的各个子问题是相互独立的,即子问题之间不包含公共的子问题。(效率:如果子问题不是相互独立的,这样虽然可以使用分治法,但是效率上却会则扣,此时可以去考虑一下动态规划)

分治法基本步骤:

分治法在每一层递归上都有三个步骤:

step1 分解:将原问题分解为若干个规模较小,相互独立,与原问题形式相同的子问题;

step2 解决:若子问题规模较小而容易被解决则直接解,否则递归地解各个子问题;

step3 合并:将各个子问题的解合并为原问题的解。

归并排序思想:

应用分治法的最经典的问题之一就是归并排序,这也是小编当时在学习数据结构的时候,没太弄懂的一个排序算法,所以今天抽出空来,好好研究一下这个算法,并与大家共同分享一下。

归并排序的是一个典型的牺牲空间换取时间的算法,这在算法种也是比较常见的情况。
归并排序的时间复杂度是O(nlog⁡n)

归并排序相对于选择排序和插入排序有一些不足之处:
隐含在渐进符号前面的常量因子比另外两个算法的渐进符号前面的常量因子的值大。当然,一旦数组规模n变得非常大,那么这个常量因子也就没那么重要了。
归并排序不是原址的:它必须将整个输入数组进行完全的拷贝。而选择排序和插入排序在任何时间仅仅拷贝一个数组项而不是对所有数组项都进行拷贝。如果空间非常宝贵,那么使用归并排序不是一个好的选择。

我们以排列书架上的书籍为例,假设每本数据都有对应的标号,现在我们应用分治法的三步骤去考虑如何解决这个排序问题:

分解:通过找到位于p和r中间位置的数字q对问题进行分解。正如使用二分查找寻找中间点那样,将p和r相加,然后除以2,并向下取整。
解决:对分解步骤得出的两个子问题的书进行递归排序,对从位置p到位置q的书籍进行递归排序,且对从位置q+1到位置r的书籍进行递归排序。
合并:将从位置p到q的排序好的书籍和从位置q+1到r的排序好的书籍进行合并,使得从位置p到位置r的书籍排好序。

注意:当少于两本书籍需要排序(也就是p>=r)时,基础情况就会发生,因为不包含的书籍或者只拥有一本书的书籍已经是排好序的。

归并排序伪代码:

为了将这个观点转换成对数组进行排序,从位置p到位置r的书对应于子数组A[p...r]。下面是归并排序程序,它会调用一个程序MERGE(A,p,q,r),该程序会将排好序的子数组A[p..q]和A[q+1,..r]合并为单一的排好序的子数组A[p .. r]


程序 MERGE=SORT(A,p,q)

输入:
	A:一个数组。
	p,r: A的某个子数组的开始索引和末尾索引
	
结果:子数组A[p .. r]中的元素按照非递减顺序排序。
	如果p>=r,那么子数组A[p .. r]至多有一个元素,因此它一定是有序的。无需执行任何操作即可返回。
	否则,执行如下操作:
	将q赋值为⌊(p+r)/2⌋。
	递归调用MERGE-SORT(A,p,q)
	递归调用MERGE-SORT(A,q+1,r)
	调用MERGE(A,p,q,r)



程序 MERGE(A,p,q,r)

输入:
	A:一个数组
	p,q,r:关于数组A的索引。假定每个子数组A[p .. q]和A[q+1 .. r]均是有序的。

结果:子数组A[p .. r]包含初始时刻在A[p .. q]和A[q+1..r]中的元素,但是现在整个数组A[p .. r]是有序数组。
	令n1取q – p + 1,n2取r-q。
	令B[1.. n1] 以及C[1.. n2]为两个新数组
	将A[p .. q]中的元素一次拷贝到B[1.. n1]中,将A[q+1 .. r]的元素依次拷贝到C[1.. n2]中。
	令i和j均取1。
	令k从p到r依次取值:
	如果B[i]<=C[j],那么A[k]被赋值为B[i],同时将i自增1。
	否则(B[i]>C[j]),A[k]被赋值为C[j],同时将j自增1。



以上就是实现归并排序的两个核心伪代码。(提前说一下,上面的整个分析步骤是自顶向下的归并排序,)

归并排序Java实现:

package com.gefj.tests;

/**
 * @author GoldBech
 * @version 1.0
 * @ClassName MergeSort1.java
 * @Description 自顶向下归并排序
 * @createTime 2020年09月17日 14:15:00
 */
public class MergeSort1 {
//    定义一个全局的数组,用于重复使用,节省空间
    private static int[] temp;
//   归并操作使用for循环
    private static void mergeByFor(int[] array, int head, int mid, int tail){
//        i,j用于把array划分为两个数组,我们约定:i为下标开头的数组为数组A,以j为下标开头的数组为数组B
        int i = head;
        int j = mid + 1;
//        使用temp数组暂存array中从head到tail的元素,目的就是为了后续的排序操作
        if (tail + 1 - head >= 0) System.arraycopy(array, head, temp, head, tail + 1 - head);
//        开始对array数组中的从head到tail的元素进行排序
        for(int index = head; index <= tail; ++index){
//           如果数组A的元素先被使用完,那么就把数组B的元素依次添加到array数组中
            if(i > mid){
                array[index] = temp[j++];
            }
//           如果数组B的元素先被使用完,那么就把数组A的元素依次添加到array数组中
            else if(j > tail) {
                array[index] = temp[i++];
            }
//            如果数组B开头的元素小于数组A开头的元素,那么就先把数组A的元素添加到array数组中
            else if(temp[j] < temp[i]){
                array[index] = temp[j++];
            }
//            否则就把数组B的元素添加到array数组中
            else{
                array[index] = temp[i++];
            }
        }

    }


    private static void mergeByWhile(int[] data,int head,int mid,int tail){

        //把data[head]<--->data[mid]作为第一个有序序列 ,我们就叫它A数组
        //把data[mid+1]<--->data[tail]作为第二个有序序列,我们就叫它B数组
        //将两个有序序列合并,形成的新序列为data[head]<--->data[tail]
        int i = head, j = mid + 1;
        int k = 0;
        while(i<= mid &&j<= tail){
            //A序列和B序列依次从起始值开始比较
            //如果A序列值小,就将其移值temp中
            //并且A下标i+1;temp下标k+1
            if(data[i]<data[j]){
                temp[k++] =data[i++];
            }else{
                //如果B序列值小,就将其移值temp中
                //并且B下标i+1;temp下标k+1
                temp[k++] = data[j++];
            }
        }

        //如果A序列或者B序列已经全部移到temp中
        //则剩余的另一个序列依次移到temp中
        while(i<= mid){
            temp[k++] =data[i++];
        }
        while(j<= tail){
            temp[k++] = data[j++];
        }

        //遍历temp,将temp中元素移会data,此时data[head]-data[tail]为有序序列
        for (i = 0; i < k; i++)  {
            data[head + i] = temp[i];
        }
    }

//  分解操作
    private static void dismantle(int[] array, int head, int tail){
//        当子数组至多有一个元素,那么它一定是有序的,无需执行任何操作即可返回。
        if(tail <= head){
            return;
        }
//        mid代表数组的中间值,注意这里和算法中说的不同点,算法中说的是子数组,q = ⌊(p+r)/2⌋,而这里是对同一个数组的多次操作
        int mid = head + (tail - head) / 2;
        dismantle(array,head,mid);
        dismantle(array,mid + 1, tail);
        //mergeByFor(array,head,mid,tail);
        mergeByWhile(array,head,mid,tail);
    }





    public static void mergeSort(int[] array){
        temp = new int[array.length];
        dismantle(array,0,array.length - 1);
    }

    public static void main(String[] args) {

        int[] arr ={45,12,7,91,33,41,17} ;
        mergeSort(arr);
        for (int index:
             arr) {
            System.out.print(index + " ");
        }
    }
}

下面介绍一下自底向上的归并排序:

自底向上的归并排序算法的思想就是数组中先一个一个归并成两两有序的序列,两两有序的序列归并成四个四个有序的序列,然后四个四个有序的序列归并八个八个有序的序列,以此类推,直到,归并的长度大于整个数组的长度,此时整个数组有序。需要注意的是数组按照归并长度划分,最后一个子数组可能不满足长度要求,这个情况需要特殊处理。自顶下下的归并排序算法一般用递归来实现,而自底向上可以用循环来实现。

package com.gefj.tests;

/**
 * @author GoldBech
 * @version 1.0
 * @ClassName MergeSort2.java
 * @Description 自底向上归并排序
 * @createTime 2020年09月17日 14:27:00
 */
public class MergeSort2 {

    private static int[] aux;

//    归并操作与自顶向下的操作是一样的
    private static void merge(int[] array, int head, int mid, int tail){
        int firstIndex = head;
        int lastIndex = mid + 1;

        if (tail + 1 - head >= 0) System.arraycopy(array, head, aux, head, tail + 1 - head);
        for(int i = head; i <= tail; ++i){

            if(firstIndex > mid){
                array[i] = aux[lastIndex++];

            }else if(lastIndex > tail){
                array[i] = aux[firstIndex++];

            }else if(aux[lastIndex] < aux[firstIndex]){
                array[i] = aux[lastIndex++];

            }else{
                array[i] = aux[firstIndex++];
            }
        }
    }

    /*
     * @title bottomToUpSort
     * @description
     * 自底向上的基本思想是:第一趟归并排序时,将待排序的文件R[1.....n]看做是n个长度为1的有序文件,将这些子文件两两归并,
     * 若n为偶数,则得到n/2个长度为2的有序文件;若n为奇数,则最后一个子文件轮空(不参与归并,直接进入下一趟归并),
     * 待本趟归并完成后,前n/2-1个有序子文件长度为2,单最后一个子文件长度仍为1;
     * 第二趟归并则是将第一趟归并所得到的n/2个有序文件两两归并,
     * 如此反复,直到得到最后长度为n的有序文件位置。
     * @author GoldBech
     * @param: array
     * @updateTime 2020/9/18 11:24
     * @throws
     */
    public static void bottomToUpSort(int[] array){

        aux = new int[array.length];
        /*
        * 首先以1为步长调整array[i]和array[i+1],接着是array[2*i]和array[2*i+1]直到完成整个数组的第一轮调整。
        * 接下来以2为步长调整array[i],array[i+1]和array[2*i],array[2*i+1]直到完成整个数组的第二轮调整。
        *
        * */
        //外层循环控制步长,
        for(int len = 1; len < array.length; len = 2 * len){
            /*
            * 按照步长去归并,这里把一次步长的变化所进行的归并表示为一趟:
            *  那么每一趟需要执行多次merge:
            *    例如:第一次  head从0开始,把下标[0,1]两个元素进行排序归并;
            *         第二次  head从2开始,把下标[2,3]两个元素进行排序归并;
            *         ……
            *  这就是内层循环为什么是 head += 2 * len 的原因
            *
            *  归并的过程中,最难的地方就是控制merge中的 mid 和 tail这两个参数
            *       比如第一趟中的第一次归并:mid需要为0,tail需要为1;第二次归并 mid需要为2,tail需要为3 ...
            *           第二趟中的第一次归并:mid需要为1,tail需要为3;第二次归并 mid需要为5,tail需要为7 ...
            *       ......
            *       此外,tail还需要有一个上限:array.length - 1
            *       在内层循环中,变量就是head和len,花点时间去找一下规律,就能找到
            *           mid = head + len - 1; 每一趟开始的第一次归并 head都为0,但是mid却需要根据需要归并的元素数量改变,所以就要去找len 和 mid的关系
            *           tail = Math.min(head + (2 * len) - 1, array.length - 1): 这个比较好理解:head每次都随步长去改变,去定位需要归并的数组的开头,
            *               比如:第一趟的第一次,head = 0, tail需要等于1; 第二次,head = 2, tail需要等于3; (大前提:len = 1)
            *                    第二趟的第一次,head = 0, tail需要等于3; 第二次,head = 4,tail需要等于7;  (大前提:len = 2)
            *               ...
            *               很容易就可以总结出 tail = head + (2 * len) - 1 当然还需要界定tail的上界。
            *
            * */
            for(int head = 0; head < array.length - len; head += 2 * len){
                merge(array,head,head + len - 1, Math.min(head + (2 * len) - 1,array.length - 1));
            }
        }
    }

    public static void main(String[] args) {
        int[] arr ={45,12,7,91,33,41,17} ;
        bottomToUpSort(arr);
        for (int index:
                arr) {
            System.out.print(index + " ");
        }
    }

}

这里小编也是借鉴了很多博客写的内容,然后根据自己的理解去梳理和总结了一下。因为小编在理解问题上不是那么一点就透,所以尽可能的去总结了一下代码的逻辑和关键点,希望能帮到需要帮助的读者。

©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页