• 归并排序


    这次终于要开始学习跟生活昔昔相关的算法了,首先映入眼帘的是归并排序,对于什么是归并排序网上文章一大堆,这里再来明确一下,先来看一张原理图【下面会用程序来论证整个如图所示的递归合并过程】

    而看一下归并排序的文字描述,理论跟实践相结合:

    对思路清楚之后,下面则具体实现一下,实现方法有两种:递归、非递归。具体如下:

    递归方式实现归并排序[自顶向下]:

    先编写基础框架:

    上面先贴出具体实现,然后下面会基于实现一步步去分析其实现原理:

    #include <iostream>
    
    //合并两个已经排好序的数组
    void merge(int array[], int temp[], int left, int middle, int right) {
        int p_left = left, p_right = middle;
        int i = left;
        while(p_left < middle || p_right < right) {
            if(p_left >= middle) {
                temp[i] = array[p_right];
                p_right++;
            } else if(p_right >= right) {
                temp[i] = array[p_left];
                p_left++;
            } else if(array[p_left] > array[p_right]) {
                temp[i] = array[p_right];
                p_right++;
            } else {
                temp[i] = array[p_left];
                p_left++;
            }
            i++;
        }
        for (int i = left; i < right; ++i)
        {
            array[i] = temp[i];
        }
    }
    
    //其中是一个[left, right)闭开区间
    void merge_sort_recursive(int array[], int temp[], int left, int right) {
        if(right - left <= 1)
            return;
        int middle = (left + right) / 2;
        merge_sort_recursive(array, temp, left, middle);
        merge_sort_recursive(array, temp, middle, right);
        merge(array, temp, left, middle, right);
    }
    
    //递归实现归并
    void merge_sort_recursive(int array[], int length) {
        int* temp = new int[length];
        merge_sort_recursive(array, temp, 0, length);
        delete []temp;
    }
    
    int main(void) {
        int array[] = {10, 4, 6, 3, 8, 2, 5, 7};
    
        merge_sort_recursive(array, 8);
    
        for (int i = 0; i < 8.; ++i) {
            std::cout << array[i] << " ";
        }
        std::cout << std::endl;
        return 0;
    }

    编译运行:

    是不是对上面的实现一头雾水,没关系,我也一头雾水,下面来剖析它,通过实现再去整理出实现原理,最终再彻底掌握它:

    这里将数组{10, 4, 6, 3, 8, 2, 5, 7}通过递归实现归并达到排序的整个过程一步步展现出来,首先看下归并入口方法:

    下面就按程序的三步骤来分析,完全按程序实现的逻辑顺序来:

    ①、int* temp = new int[length];

    为啥要用一个跟数据长度一样长的临时变量呢?实际上就是用它进行数据排序的,将排好序的数据到时再同步到真正的数组中去,具体可以在分析流程②上可以看出来。

    ②、merge_sort_recursive(array, temp, 0, length);【核心】

    接着它会调用另一个重载的方法:

    其中传过来的参数left=0,right=8,其中需要特别注意的是是包含left,不包含right,是一个闭开区间。

      a、

        这是递归结束条件,也就是表示只剩一个元素了,具体可以在分析c、d步骤中去理解,很显然此时为false。

      b、取出中间元素的位置,其中归并排序的思想就是以中间为分界,左边排好序,右边再排好序,最后再左右进行合并则达到归并排序的目的。这里取值是:middle=(0+8) / 2 = 4;
        

      c、对[left,middle)这个区间进行递归调用,也就是merge_sort_recursive(array, temp, 0, 4):

        

        接下来具体来分析一下整个递归过程,还是按着merge_sort_recursive的四部步骤去分析:

        ca、,right=4,left=0,right-left=4,不是小于1的,所以条件不成立,继续往下执行。

        cb、,middle = (0 + 4) / 2 = 2;

        cc、,merge_sort_recursive(array, temp, 0, 2),可见又进行一个递归过程,继续分析:
          

          cca、,right=2, left=0, right-left=2,不是小于1,所以条件不成立,继续往下执行。

          ccb、,middle = (0 + 2) / 2 = 1;

          ccc、, merge_sort_recursive(array, temp, 0, 1),继续递归
              ccca、,right=1, left=0, right-left=1,条件刚好成立,于是乎整个方法都返回递归结束。

          ccd、, merge_sort_recursive(array, temp, 1, 2),继续递归

              ccda、,right=2, left=1, right-left=1,条件刚好成立,递归结束。
          这时数据调用的关系如下:
          

          cce、,merge(array, temp, 0, 1, 2),那这个merge方法调用完之后会是什么效果呢?下面具体分析一下流程:

            
            其中参数left=0, middle=1, right=2,下面具体分析:
            ccea、,p_left=0, p_right=1

            cceb、,i=0,这个变量是干嘛用的呢?其实它就是用来将排好序的一一加到temp临时数组,而每添加一个则累加一次,这个在下面步骤中可以很清楚的看到。
            ccec、开始不断循环进行比较,其实这个过程跟上节学的排好序的合并链表思想是一模一样的,从代表风格可以看出来,只是这里采用指针位置移位的方法。
              
              loop1:   其中循环条件:(0 < 1 || 1 < 2),条件为真,则通往循环体:

                 ccec①、(0 >= 1),条件不满足,执行条件②;
                 ccec②、(1 >= 2),条件不满足,执行条件③;

                    ccec③、(array[0] > array[1])=(10 > 4)条件满足,则执行条件里面的语句:
                      temp[0] = array[1] = 4,也就是先取两数中较少的放到temp中
                        p_right++ ====>  p_right=2
                 ccec⑤、i++ ====>  i=1

                loop2:   其中循环条件:(0 < 1 || 2 < 2),条件为真,则通往循环体:

                 ccec①、(0 >= 1),条件不满足,执行条件②;
                 ccec②、(2 >= 2),条件满足,执行条件里面的语句;
                        temp[1] = array[0] = 10,也就是将两数中较大的放到了temp中

                     p_left++  ====>  p_left=1

                 ccec⑤、i++ ====>  i=2

                loop3:   其中循环条件:(1 < 1 || 2 < 2),条件为假,退出循环。
            cced:将temp中的元素一一复制到目标array数组中,目前temp只有两个已经从小到大排好序的数据:4,10
              

          这时可以看到2左边的元素已经排好序了:
          

        cd、,merge_sort_recursive(array, temp, 2, 4), 由于已经分析了cc的整个递归过程,这里就不仔细分析它了,流程上跟cc是一模一样的,经过这步之后,整个递归的情况如下:
        

        以上cc、cd两步骤就对2左边的数和右边的数进行了从小到大的顺序排序了

        ce、,merge(array, temp, 0, 2, 4),经过上面cce的详细分析合并过程,其实就是对[4、10]和[3、6]两个已经排好序的进一并合并成[3、4、6、10]这样从小到大的序列,下面详细按cce的步骤再来熟悉一下整个的合并过程:
          
          其中参数left=0, middle=2, right=4,下面具体分析:
          cea、,p_left=0, p_right=2
          ceb、,i=0,这个变量干嘛用应该就不用多解释了,上面已经分的过。

          cec、开始不断循环进行比较,具体不多解释,主要分析循环的整个过程:

            

            loop1:   其中循环条件:(0 < 2 || 2 < 4),条件为真,则通往循环体:

                 cec①、(0 >= 2),条件不满足,执行条件②;
                 cec②、(2 >= 4),条件不满足,执行条件③;

                    cec③、(array[0] > array[2])=(4 > 3)条件满足,则执行条件里面的语句:
                      temp[0] = array[2] = 3,也就是先取两数中较少的放到temp中
                        p_right++ ====>  p_right=3
                 cec⑤、i++ ====>  i=1

              loop2:   其中循环条件:(0 < 2 || 3 < 4),条件为真,则通往循环体:

                 cec①、(0 >= 2),条件不满足,执行条件②;
                 cec②、(3 >= 4),条件不满足,执行条件③;
                 cec③、(array[0] > array[3])=(4 > 6)条件不满足,执行条件④;

                    cec④、temp[1] = array[0] = 4;

                       p_left++ ====> p_left=1;

                 cec⑤、i++ ====>  i=2

             loop3:   其中循环条件:(1 < 2 || 3 < 4),条件为真,则通往循环体:

                 cec①、(1 >= 2),条件不满足,执行条件②;
                 cec②、(3 >= 4),条件不满足,执行条件③;

                    cec③、(array[1] > array[3])=(10 > 6)条件满足,则执行条件里面的语句:
                       temp[2] = array[3] = 6,也就是先取两数中较少的放到temp中
                          p_right++ ====>  p_right=4

                 cec⑤、i++ ====>  i=3

            loop4:   其中循环条件:(1 < 2 || 4 < 4),条件为真,则通往循环体:

                 cec①、(1 >= 2),条件不满足,执行条件②;
                 cec②、(4 >= 4),条件满足,则执行条件里面的语句:

                      temp[3] = array[1] = 10;

                      p_left++ ====> p_left=2;               

                 cec⑤、i++ ====>  i=4

            loop5:   其中循环条件:(2 < 2 || 4 < 4),条件为假,退出循环。

          ced:将temp中的元素一一复制到目标array数组中,目前tempr的序列就如我们猜想的[3、4、6、10]
              

        这时经过这个合并可以看到已将2左边和右边已排好序的数列合并成从小到大了:
        

      d、依照c的步骤,对[middle,right)这个区间进行递归调用,也就是merge_sort_recursive(array, temp, 4, 8);

        

        这里就不一一分析递归过程了,具体详细递归全并过程已经在c步骤中进行了描述,贴出最终结果:
        

      e、
        merge(array, temp, 0, 4, 8),也就是最终将4左侧已经排好序的[3、4、6、10]和右侧已经排好序的[2、5、7、8]进行最终合并,达到整体从小到大的排序,到这一步也就是整个递归归并实现的全过程,如图:
        
      从上面这符图就可以清楚的知道是一个自顶向下的归并过程。

    ③、delete []temp;

    当临时数组用完之后,需要将其内存释放,不多解释。

    这样整个递归实现归并算法的流程走完,有木有茅塞顿开呢?下面再来总结一下实现思路,以便在要忘得差不多的时候自己从头写时有一个思路参考【以C++实现为例】:

    步骤1、先声明一个临时数组,数组的长度则跟待排序的数组一样,其主要目的就是为了将排序之后的数组放在它里面,而不是直接去改原数组的序列,等所有步骤结束之后这个数组就是一个已经排好序的数列了,接着再将它原封不动的拷贝到原数组达到最终排序的目的。

    步骤2、声明一个方法专门用来递归实现归并,也就是最核心的方法,其参数为:原数组、临时数组、要归并的数组起始位置left【从0开始,是个闭区间】、要归并的数组结束位置right【这是数组的长度,是个开区间】,而该方法具体实现分如下步骤:

      a、先来判断递归的结束条件,这是很关键的,而条件就是发现只有一个元素了则结束递归,而如何判断只剩一个元素了呢?用right-left <= 1,其实很好理解,因为是一个[left,right)的闭开区间,当还剩一个无毒时,left=0、right=1。

      b、计算出中间的位置,因为归并排序的核心思想就是折半之后将左边和右边的数据排序好,然后再将左右排序好的数据进行合并。

      c、不断递归左边的数据,相当于一个数据拆解的过程,直到只剩一个元素结束递归,其参数为:原数组、临时数组、left,middle。

      d、不断递归右边的数据,相当于一个数据拆解的过程,直到只剩一个元素结束递归,其参数为:原数组、临时数组、middle,right。
      e、将左右两边已经排好序的数据进行合并,这个就跟上篇学习的合并已经排好序的链表逻辑类似,其具体合并的过程如下:

        ①、声明两个临时变量p_left、p_right,分别指向left、middle,然后会不断的循环去更改这两个临时变量去进行数据合并。

        ②、声明一个变量i,默认直接指向参数中的left,它的作用主要是用来往temp指定位置塞数据用的,而往temp塞数据是会让一定顺序进行的。

        ③、接着开始循环进行合并,也是该方法的核心:

          1、首先循环的条件是:p_left < middle || p_right < right。也就是左边的数据和右边的数据都走完了则循环结束,条件满足则下执行循环体

          2、如果左边的数据已经走完,则将p_right指向的数放到temp中,并且p_right++。

          3、如果右边的数据已经走完,则将p_left指向的数放到temp中,并且p_left++。

          4、如果左边的数据大于右边的数据,则将右边的数据放到temp中,并且将p_right++。

          5、否则表示左边的数据小于右边的数据,则将左边的数据放到temp中,并且将p_left++。

          6、最后将i++,将temp的下标往下挪。

      f、将temp已经排好序的完全拷贝至array中。

    步骤3、将临时变量的内存释放掉,因为它的使用价值已经到此结束。

    非递归方式实现归并排序[自底向上]:

    非递归的方式整体步骤跟递归的思路是相反的,有了递归的思路,非递归的方式很容易理解,依然看一张用非递归方式来实现归并排序的整体思路:

    实基本思想是:将数组中的相邻元素两两配对。用Merge()函数将他们排序,构成n/2组长度为2的排序好的子数组 段,然后再将他们合并成长度为4的子数组段,如此继续下去,直至整个数组排好序。下面看下具体实现:

    编译运行:

    下面依然用debug的方式一步步分析来理解其实现:

     

    ①、int* temp = new int[length];不多解释,跟递归实现的一样。

    ②、接着开始循环开启归并模式,如下:


      step loop1:

      a、:外层循环,具体为啥这么写等整个过程分析完之后就明白了。

        b、:内层循环,计算left的位置。

          left loop1: step = 1、left = 0;

            abc、:计算middle的位置:middle = min(0 + 1, 8) = 1

            abd、:计算right的位置:right = min(0 + 2 * 1, 8) = 2

            abe、:这个方法有木有很熟悉,木错,用的跟递归的合并方法一模一样,所以这里就不多解释了。
          left = left + 2 * step = 0 + 2 * 1 = 2;
          
          left loop2: step = 1、left = 2;

            abc、:计算middle的位置:middle = min(2 + 1, 8) = 3

            abd、:计算right的位置:right = min(2 + 2 * 1, 8) = 4

            abe、

          left = left + 2 * step = 2 + 2 * 1 = 4;
              

          left loop2: step = 1、left = 4;

            abc、:计算middle的位置:middle = min(4 + 1, 8) = 5

            abd、:计算right的位置:right = min(4 + 2 * 1, 8) = 6

            abe、

          left = left + 2 * step = 4 + 2 * 1 = 6;
          

          left loop2: step = 1、left = 6;

            abc、:计算middle的位置:middle = min(6 + 1, 8) = 7

            abd、:计算right的位置:right = min(6 + 2 * 1, 8) = 8

            abe、

          left = left + 2 * step = 6 + 2 * 1 = 8;
          
        而内层循环的left < length条件不满足了,则退出此次的内层循环,外层的step = step * 2 = 2
      总结:通过上面第一轮内层循环流程走完,外层的step是用来控制是每次比较的数量,而内层的left肯定是用来控制left的位置的,而middle和left就是根据step和left来决定的,其中step每次是+2,而left则是每次基于上一次+2倍的step。

      step loop2:

        a、:外层循环

        b、:内层循环

          left loop1: step = 2、left = 0;

            abc、:计算middle的位置:middle = min(0 + 2, 8) = 2

            abd、:计算right的位置:right = min(0 + 2 * 2, 8) = 4

            abe、
          

          left = left + 2 * step = 0 + 2 * 2 = 4;

          left loop2: step = 2、left = 4;

            abc、:计算middle的位置:middle = min(4 + 2, 8) = 6

            abd、:计算right的位置:right = min(4 + 2 * 2, 8) = 8

            abe、
          

          left = left + 2 * step = 4 + 2 * 2 = 8;
          而此时left < length不满足条件,此次的内层循环结束。外层的step = step * 2 = 4

      step loop3:

        a、:外层循环

        b、:内层循环

          left loop1: step = 4、left = 0;

            abc、:计算middle的位置:middle = min(0 + 4, 8) = 4

            abd、:计算right的位置:right = min(0 + 2 * 4, 8) = 8

            abe、
            

          left = left + 2 * step = 0 + 2 * 4 = 8;
          而此时left < length不满足条件,此次的内层循环结束。外层的step = step * 2 = 8,而外层的step<length也不满足条件,则外层循环也结束,至此整个非递归方法结束。

    ③、delete []temp;也不多解释。

    以上详细的对归并的两种实现方法分析了其实现过程,算法实现完了当然得分析一下它的时间复杂度,这个待下次学习快速排序之后再一次分析~因为他们的时间复杂度是一样的。

  • 相关阅读:
    Python中使用MongoEngine
    Python中MongoDB使用
    JAVA 日期相关API (JDK 8 新增)
    JAVA 日期相关API(JDK 8 之前)
    StringBuffer 和StringBuilder
    String 类型转换
    String类常用方法
    JAVA String类
    关于线程锁的释放和保留
    java线程同步--使用线程池
  • 原文地址:https://www.cnblogs.com/webor2006/p/7130706.html
Copyright © 2020-2023  润新知