归并排序

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

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

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

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

先编写基础框架:

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

#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;也不多解释。

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

原文地址:https://www.cnblogs.com/webor2006/p/7130706.html