• 【算法】归并排序算法的编码和优化


    参考资料

    《算法(第4版)》          — — Robert Sedgewick, Kevin Wayne
     

    归并排序的概念

    归并排序的实现我是这样来描述的:先对少数几个元素通过两两合并的方式进行排序,形成一个长度稍大一些的有序序列。然后在此基础上,对两个长度稍大一些的有序序列再进行两两合并,形成一个长度更大的有序序列,有序序列的的长度不断增长,直到覆盖整个数组的大小为止,归并排序就完成了。
     

    归并排序的两种实现方式:递归和循环

    归并排序有两种实现方式: 基于递归的归并排序和基于循环的归并排序。(也叫自顶向下的归并排序自底向上的归并排序
     
    这两种归并算法虽然实现方式不同,但还是有共同之处的:
     
    1. 无论是基于递归还是循环的归并排序, 它们调用的核心方法都是相同的:完成一趟合并的算法,即两个已经有序的数组序列合并成一个更大的有序数组序列  前提是两个原序列都是有序的!)
    2. 从排序轨迹上看,合并序列的长度都是从小(一个元素)到大(整个数组)增长
     
     

    单趟归并算法

    单趟排序的实现分析

     
    下面我先介绍两种不同归并算法调用的公共方法, 即完成单趟归并的算法。(两个已经有序的数组序列合并成一个更大的有序数组序列)
     
    在开始排序前创建有一个和原数组a长度相同的空的辅助数组aux
     
    单趟归并的过程如下:
     
    1.  首先将原数组中的待排序序列拷贝进辅助数组的相同位置中,即将a[low...high]拷贝进aux[low...high]中
    2.  辅助数组aux的任务有两项:比较元素大小, 并在aux中逐个取得有序的元素放入原数组a中 (通过1使aux和a在low-high的位置是完全相同的!这是实现的基础)
    3.  因为aux[low...high]由两段有序的序列:aux[low...mid]和aux[mid...high]组成, 这里称之为aux1和aux2,我们要做的就是从aux1和aux2的头部元素开始,比较双方元素的大小。较小的元素放入原数组a中(若a[0]已被占则放在a[1]...依次类推),并取得较小元素的下一个元素, 和另一个序列中较大的元素比较。因为前提是aux1和aux2都是有序的,所以通过这种方法我们能得到更长的有序序列
    4.  如果aux的两段序列中,其中一段中的所有元素都已"比较"完了, 取得另一段序列中剩下的元素,全部放入原数组a的剩余位置。
     
    过程3和4的实现方法
    • 设置两个游标 i 和 j 用于“元素比较” (在aux中进行):变量,i 和 j,分别代表左游标和右游标,开始时分别指向aux[low]和aux[mid]
    • 设置游标k用于确定在a中放置元素的位置(在a中进行),k在开始时候指向a[low]
    • 总体上来说i, j, k的趋势都是向右移动的
     
    过程3和4的图示解说
     
    图A
     
     
    结合上面的过程3,  比较 i 和 j 当前所指的aux中的元素的大小, 取得其中比较大的那个元素(例如上图中的i),将其放入数组a中, 此时(在图中假设情况下): i加1,左游标右移。  同时k也加1, k游标也向右移动
     
    图B
     
    结合上面的过程4, 在 i 和 j 都向右移动的过程中, 在图中假设情况下,因为j当前所指的元素(图中位置)大于左半边即a[low...mid]的所有元素,导致 i 不断增加(右移)且越过了边界(mid), 所以这时候就不需要比较了,只要把j当前所指位置到high的元素都搬到原数组中,填满原数组中剩下的位置, 单趟归并就完成了, 在这一段过程中 j 连续加1,右游标连续右移。  同时k也连续加1, k游标也连续右移, 直到 j == high且k == high为止
     
    基于上面的表述, 总结出单趟归并算法中最关键的4个条件判断情形:
    1. 左半边用尽(取右半边的元素)
    2. 右半边用尽(取左半边的元素)
    3. 右半边元素小于左半边当前元素(取右半边的元素)
    4. 右半边元素大于等于左半边当前元素(取左半边的元素)
     
     

    单趟排序算法的代码

     
    有了上面的解释,写这个算法就不难了吧
     
    /**
       * @description: 完成一趟合并
       * @param a 输入数组
       * @param low,mid,high a[low...high] 是待排序序列, 其中a[low...mid]和 a[mid+1...high]已有序
       */
      private static void merge (int a [],int low,int mid,int high) {
        for(int k=low;k<=high;k++){
          aux[k] = a[k]; // 将待排序序列a[low...high]拷贝到辅助数组的相同位置
        }
        int i = low;    // 游标i,开始时指向待排序序列中左半边的头元素
        int j = mid+1;  // 游标j,开始时指向待排序序列中右半边的头元素
        for(int k=low;k<=high;k++){
          if(i>mid){
            a[k] = aux[j++]; // 左半边用尽
          }else if(j>high){
            a[k] = aux[i++]; // 右半边用尽
          }else if(aux[j]<aux[i]){
            a[k] = aux[j++]; // 右半边当前元素小于左半边当前元素, 取右半边元素
          }else {
            a[k] = aux[i++]; // 右半边当前元素大于等于左半边当前元素,取左半边元素
          }
        }
      }
    }
     
    【注意】在排序之初创建了一个长度和原数组a相同的辅助数组aux,这部分代码上文未给出
     

    单趟排序的过程图解

     
    为了更详细的描述单趟排序的过程,下面在上面的图A和图B的基础上给出每一步的图解:
     
    我们要排序的序列是 2 4 5 9 1 3 6 7, 合并的前提是2 4 5 9 和 1 3 6 7都是有序的
     
    先比较aux中2和1的大小,因为2>1,所以将1放入a[0]。这时, 游标 i 不动, 游标 j 右移, 游标 k 右移
     
    比较aux中2和3的大小,因为2<3,所以将2放入a[1]。这时, 游标 j 不动, 游标 i 右移, 游标 k 右移
     
    比较aux中4和3的大小,因为3<4,所以将3放入a[2]。这时, 游标 i 不动, 游标 j 右移, 游标 k 右移
     
    类似以上, 不解释
     
    类似以上, 不解释
     
    类似以上, 不解释
     
    类似以上, 不解释
     
    注意, 这这里 j 增加导致 j> high,  现在的情形是“右半边用尽”, 所以将aux左半边剩余的元素9放入a剩下的部分a[7]中, 单趟排序完成
     
    【注意】 上面这个例子中的序列只是数组的一部分, 并不一定是整个数组
     
     
    我在上面介绍过,两种不同归并算法: 基于递归的归并和基于循环的归并,  都是以单趟归并的算法为基础的。
     
    下面先来讲一下基于递归的归并排序(自顶向下的归并排序)
     

    基于递归的归并排序(自顶向下)

     
    基于递归的归并排序又叫做自顶向下的归并排序
     

    递归归并的思想

     
     
    最关键的是sort(int a [], int low,int high)方法里面的三行代码:
    sort(a,low,mid); 
    sort(a,mid+1,high);
    merge(a,low,mid,high);
    分别表示对左半边序列递归、对右半边序列递归、单趟合并操作。
     
    全部代码:
    /**
    * @Author: HuWan Peng
    * @Date Created in 9:44 2017/11/29
    */
    public class MergeSort {
      private static int aux [];
      /**
       * @description: 1. 初始化辅助数组aux,使其长度和原数组相同
       *               2. 包装sort,向外只暴露一个数组参数
       */
      public static void sort(int a []){
        aux = new int[a.length];
        sort(a,0,a.length-1);
      }
      /**
       * @description: 基于递归的归并排序算法
       */
      private static void sort (int a [], int low,int high) {
        if(low>=high) { return; } // 终止递归的条件
        int mid =  low + (high - low)/2;  // 取得序列中间的元素
        sort(a,low,mid);  // 对左半边递归
        sort(a,mid+1,high);  // 对右半边递归
        merge(a,low,mid,high);  // 单趟合并
      }
     
      /**
       * @description:  单趟合并算法
       * @param a 输入数组
       * @param low,mid,high a[low...high] 是待排序序列,其中a[low...mid]和 a[mid+1...high]已有序
       */
      private static void merge (int a [],int low,int mid,int high) {
        int i = low;    // 游标i,开始时指向待排序序列中左半边的头元素
        int j = mid+1;  // 游标j,开始时指向待排序序列中右半边的头元素
        for(int k=low;k<=high;k++){
          aux[k] = a[k]; // 将待排序序列a[low...high]拷贝到辅助数组的相同位置
        }
        for(int k=low;k<=high;k++){
          if(i>mid){
            a[k] = aux[j++]; // 左半边用尽
          }else if(j>high){
            a[k] = aux[i++]; // 右半边用尽
          }else if(aux[j]<aux[i]){
            a[k] = aux[j++]; // 右半边当前元素小于左半边当前元素, 取右半边元素
          }else {
            a[k] = aux[i++]; // 右半边当前元素大于等于左半边当前元素,取左半边元素
          }
        }
      }
    }
     
    测试代码:
     
    public class Test {
      public static void main (String args[]){
        int [] a = {1,6,3,2,9,7,8,1,5,0};
        MergeSort.sort(a);
        for(int i=0;i<a.length;i++){
          System.out.println(a[i]);
        }
      }
    }
     
    输出结果
    0
    1
    1
    2
    3
    5
    6
    7
    9
     

    递归栈深度和调用顺序

     
    递归导致的结果是,形成了一系列有层次、有先后调用顺序merge,  如下图左边的写入编号的merge列表
    从上到下,是各个merge的先后调用顺序,1最先调用, 15最后调用
    从右到左, 递归栈由深到浅,例如 1,2,4,5的递归深度是相同的, 而3比它们浅一个层次
     
    (这里是按照字母排序, A最小, Z最大)
     
    对上图可根据代码来理解
    sort(a,low,mid);      // A
    sort(a,mid+1,high);   // B
    merge(a,low,mid,high);// C
     
    首先,在第一层递归的时候,先进入的是第一行的sort方法里(A处),然后紧接着又进入了第二层递归的第一行sort方法(A处), 如此继续,由(a, low,mid)的参数列表可知其递归的趋势是一直向左移动的,直到最后一层递归,所以最先执行merge的对象是a[0]和a[1](上图编号1),再然后执行的是最后一层递归的第二行代码(B处),这时候merge的对象是a[2]和a[3](上图编号2)。 再然后, 返回上一层递归,对已经有序的a[0]、a[1]和a[2]、a[3]进行merge。(上图编号3)如此继续,递归的深度不断变浅, 直到对整个数组的左右两半进行merge。 (上图编号3)
     
     

    递归归并的轨迹图像

    (下面展示的归并进行了一些优化,对小数组使用插入排序)

     
     
    根据上文所讲的递归栈和调用顺序, 下面的轨迹图像就不难理解了: 从最左边的元素开始合并,而且左边的数组序列在第一轮合并后,相邻右边的数组按同样的轨迹进行合并, 直到合并出和左边相同长度的序列后,才和左边合并(递归栈上升一层)
     
     
     
     
     

    基于递归归并排序的优化方法

     

    优化点一:对小规模子数组使用插入排序

    用不同的方法处理小规模问题能改进大多数递归算法的性能,因为递归会使小规模问题中方法调用太过频繁,所以改进对它们的处理方法就能改进整个算法。 因为插入排序非常简单, 因此一般来说在小数组上比归并排序更快。 这种优化能使归并排序的运行时间缩短10%到15%;
     
    怎么切换呢? 只要把作为停止递归条件的
      if(low>=high) { return; }
    改成
        if(low + M>=high) { // 数组长度小于10的时候
          InsertSort.sort(int a [], int low,int high) // 切换到插入排序
          return;
        }
    就可以了,这样的话,这条语句就具有了两个功能:
     
    1. 在适当时候终止递归
    2. 当数组长度小于M的时候(high-low <= M), 不进行归并排序,而进行插排
     
    具体代码:
      private static void sort (int a [], int low,int high) {
        if(low + 10>=high) { // 数组长度小于10的时候
          InsertSort.sort(int a [], int low,int high) // 切换到插入排序
          return;
        } // 终止递归的条件
        int mid =  low + (high - low)/2;  // 取得序列中间的元素
        sort(a,low,mid);  // 对左半边递归
        sort(a,mid+1,high);  // 对右半边递归
        merge(a,low,mid,high);  // 单趟合并
      }
     

    优化点二:  测试待排序序列中左右半边是否已有序

     
    通过测试待排序序列中左右半边是否已经有序, 在有序的情况下避免合并方法的调用。
     
    例如对单趟合并,我们对a[low...high]中的a[low...mid]和a[mid...high]进行合并
    因为a[low...mid]和a[mid...high]本来就是有序的,存在a[low]<a[low+1]...<a[mid]和a[mid+1]<a[mid+2]...< a[high]这两种关系, 如果判断出a[mid]<=a[mid+1]的话, 不就可以保证从而a[low...high]本身就是不需要排序的有序序列了吗?
     
      private static void sort (int a [], int low,int high) {
        if(low>=high) {
          return;
        } // 终止递归的条件
        int mid =  low + (high - low)/2;  // 取得序列中间的元素
        sort(a,low,mid);  // 对左半边递归
        sort(a,mid+1,high);  // 对右半边递归
        if(a[mid]<=a[mid+1]) return; // 避免不必要的归并
        merge(a,low,mid,high);  // 单趟合并
      }
     

    优化点三:去除原数组序列到辅助数组的拷贝

    在上面介绍的基于递归的归并排序的代码中, 我们在每次调用merge方法时候,我们都把a对应的序列拷贝到辅助数组aux中来,即
        for(int k=low;k<=high;k++){
          aux[k] = a[k]; // 将待排序序列a[low...high]拷贝到辅助数组的相同位置
        }

     

    实际上,我们可以通过一种看起来比较逆天的方式把这个拷贝过程给去除掉。。。。。
     
    为了达到这一点,我们要在递归调用的每个层次交换输入数组和输出数组的角色,从而不断地把输入数组排序到辅助数组,再将数据从辅助数组排序到输入数组。
     
    卧槽?! 还有这么骚的操作要怎么搞? 请看:
     
      public static void sort(int a []){
        aux = a.clone(); // 拷贝一个和a所有元素相同的辅助数组
        sort(a,aux,0,a.length-1);
      }
      /**
       * @description: 基于递归的归并排序算法
       */
      private static void sort (int a[], int aux[], int low,int high) {
        if(low>=high) { return; } // 终止递归的条件
        int mid =  low + (high - low)/2;  // 取得序列中间的元素
        sort(aux, a,low,mid);  // 对左半边递归
        sort(aux, a,mid+1,high);  // 对右半边递归
        merge(a, aux, low,mid,high);  // 单趟合并
      }
    在这里我们做了两个操作:
    • 在排序前拷贝一个和原数组元素完全一样的辅助数组(不再是创建一个空数组了!)
    • 在递归调用的每个层次交换输入数组和输出数组的角色
     
     
    注意, 外部的sort方法和内部sort方法接收的a和aux参数刚好是相反的
     
     
    这样做的话, 我们就可以去除原数组序列到辅助数组的拷贝了!
     
    但是你可能会问: 骚年, 我们要排序的可是原数组a啊! 你不怕一不小心最后完全排序的是辅助数组aux而不是原数组a吗?
     
    Don't worry !! 这种情况不会发生, 看图:
     
     
    由图示易知, 因为外部sort和merge的参数顺序是相同的, 所以,无论递归过程中辅助数组和原数组的角色如何替换,对最后一次调用的merge而言(将整个数组左右半边合为有序的操作),   最终被排为有序的都是原数组,而不是辅助数组!
     
     
    全部代码:
     
    /**
    * @Author: HuWan Peng
    * @Date Created in 9:44 2017/11/29
    */
    public class MergeSort {
      private static int aux [];
      /**
       * @description: 1. 初始化辅助数组aux,使其和原数组元素完全相同
       *               2. 包装sort,向外只暴露一个数组参数
       */
      public static void sort(int a []){
        aux = a.clone(); // 拷贝一个和a所有元素相同的辅助数组
        sort(a,aux,0,a.length-1);
      }
      /**
       * @description: 基于递归的归并排序算法
       */
     
      private static void sort (int a[], int aux[], int low,int high) {
        if(low>=high) { return; } // 终止递归的条件
        int mid =  low + (high - low)/2;  // 取得序列中间的元素
        sort(aux, a,low,mid);  // 对左半边递归
        sort(aux, a,mid+1,high);  // 对右半边递归
        merge(a, aux, low,mid,high);  // 单趟合并
      }
     
      /**
       * @description:  单趟合并算法
       * @param a 输入数组
       * @param low,mid,high a[low...high] 是待排序序列,其中a[low...mid]和 a[mid+1...high]已有序
       */
      private static void merge (int a [],int aux [],int low,int mid,int high) {
        int i = low;    // 游标i,开始时指向待排序序列中左半边的头元素
        int j = mid+1;  // 游标j,开始时指向待排序序列中右半边的头元素
        // 这里的for循环拷贝已经去除掉了
        for(int k=low;k<=high;k++){
          if(i>mid){
            a[k] = aux[j++]; // 左半边用尽
          }else if(j>high){
            a[k] = aux[i++]; // 右半边用尽
          }else if(aux[j]<aux[i]){
            a[k] = aux[j++]; // 右半边当前元素小于左半边当前元素, 取右半边元素
          }else {
            a[k] = aux[i++]; // 右半边当前元素大于等于左半边当前元素,取左半边元素
          }
        }
      }
    }
     
    测试代码和输出结果同上文。
     

    基于循环的归并排序(自底向上)

     
    基于循环的归并排序又叫做自底向上的归并排序
     

    循环归并的基本思想

     
     
    基于循环的代码较为简单,这里就不多赘述了
     
    /**
    * @Author: HuWan Peng
    * @Date Created in 23:42 2017/11/30
    */
    public class MergeSort2 {
      private static int aux [];
     
      public static void sort(int a []){
        int N = a.length;
        aux = new int [N];
        for (int size =1; size<N;size = size+size){
          for(int low =0;low<N-size;low+=size+size) {
            merge(a,low,low+size-1,Math.min(low+size+size-1,N-1));
          }
        }
      }
     
      private static void merge (int a [],int low,int mid,int high) {
        int i = low;    // 游标i,开始时指向待排序序列中左半边的头元素
        int j = mid+1;  // 游标j,开始时指向待排序序列中右半边的头元素
        for(int k=low;k<=high;k++){
          aux[k] = a[k];
        }
        for(int k=low;k<=high;k++){
          if(i>mid){
            a[k] = aux[j++]; // 左半边用尽
          }else if(j>high){
            a[k] = aux[i++]; // 右半边用尽
          }else if(aux[j]<aux[i]){
            a[k] = aux[j++]; // 右半边当前元素小于左半边当前元素, 取右半边元素
          }else {
            a[k] = aux[i++]; // 右半边当前元素大于等于左半边当前元素,取左半边元素
          }
        }
      }
    }
     

    循环归并的轨迹图像

    (下图中的sz同上面的变量size)
     
     
     

     

     

     
  • 相关阅读:
    lvs中dr模式配置脚本
    使用AFNetworking第三方下载类
    java 经常使用測试框架
    Qt5的插件机制(1)--Qt 框架中的插件载入机制概述
    leetcode笔记:Merge Sorted Array
    oracle仿全文检索切词机制实现文本信息类似度查找
    hadoop学习;datajoin;chain签名;combine()
    php函数in_array奇怪现象
    Sql_Server中怎样推断表中某列是否存在
    Java Bean 简单介绍及其应用
  • 原文地址:https://www.cnblogs.com/penghuwan/p/7940440.html
Copyright © 2020-2023  润新知