从2路归并到k路归并(四)

1,604 阅读4分钟

上一篇我们说到了三路归并的代码,结合二路归并和三路归并的代码,我们可以写出k路归并的实现.其中有两个核心的地方需要理解,理解了这两个地方,其他很容易写出来,毕竟就两个核心的方法sortmerge.下面我们重点说一下这两个地方,第一个就是如何组织merge方法的参数

组织merge方法参数

先回顾一下二路归并和三路归并中merge方法的签名

//二路归并
public static void merge(Comparable[] a,Comparable[] aux, int lo,int mid,int hi)
//三路归并
public static void merge(Comparable[] a, Comparable[] aux,int lo,int second,int third,int hi)

前两个参数不说,重点在于后面的参数,其实后面的参数就表达一个意思:想要merge几路有序数组.对于二路归并,就是归并两路有序数组,这两路有序数组是通过lo,mid,hi来表示,第一路有序数组的起止索引是lo,mid,第二路有序数组的起止索引是mid+1,hi

三路归并类似,第一路有序数组起止索引是lo,second-1,第二路起止索引是second,third-1,第三路起止索引是third,hi.

那当我们归并k路有序数组呢? 其实很简单,我们用一个对象列表存储k路的起止索引.此外,在归并过程中,我们需要知道每一路比较到了哪个元素,以判断该路元素是否被用完,所以我们还需要知道每一路的当前比较元素位置(索引),所以我们引入一个内部类来表示每路元素的位置相关信息

  /**
   * 内部类记录每路元素的位置信息
   */
  private static class GroupState{
    public GroupState(int NO,int curPos,int startPos,int maxPos){
      this.NO = NO;
      this.curPos = curPos;
      this.startPos = startPos;
      this.maxPos = maxPos;
    }
    /**
     * 每路的编号,从0开始,备用
     */
    public final int NO;
    /**
     * 当前比较元素位置
     */
    public int curPos;
    /**
     * 每路的起始位置
     */
    public final int startPos;

    /**
     * 每路的最大位置,大于该值表示该路元素用完
     */
    public final int maxPos;
  }

下面给出merge方法的实现

  public static void merge(Comparable[] a, Comparable[] aux, List<GroupState> states, int lo, int hi){
    for(int x=lo;x<=hi;x++)
      aux[x] = a[x];

    for(int x=lo;x<=hi;x++){
      Iterator<GroupState> iterator = states.iterator();
      while (iterator.hasNext()){
        GroupState state = iterator.next();
        int cur = state.curPos;
        int max = state.maxPos;
        //如果当前组已比完,删除,避免再次比较
        if(cur > max){
          iterator.remove();
          //因为每次只能有一个组比完,一旦删除后退出循环
          break;
        }
      }
      updateGrpCurPos(a,x,aux,states);
    }
  }
  
  //获取每路当前比较元素的最小值,放入源数组
  private static void updateGrpCurPos(Comparable[] a,int x,Comparable[] aux ,List<GroupState> states){
    //states includes at least one element
    GroupState min = states.get(0);
    if(states.size()>1){
      for(int i=1;i<states.size();i++){
        if(Sort.less(aux[states.get(i).curPos],aux[min.curPos]))
          min = states.get(i);
      }
    }
    a[x] = aux[min.curPos];
    //将最小元素所在路的当前索引后移,因为该元素已经放入原数组
    //下次应该从它后面的元素开始比较
    min.curPos++;
  }

Ok,讲完了merge,自然引出第二个重点,如何组织List<GroupState> states,这就是sort方法的职责.

生成List<GroupState> states

其实也简单,只要我们拿到了步长(每路元素的个数),就能很容易地知道每路的起止索引,这里说的止,对应GroupStatemaxPos属性.

  private static void sort(Comparable[] a,Comparable[] aux,int lo,int hi,int k){
    if(hi<=lo) return;
    //获得步长
    int step = (hi-lo)/k+1;
 
    List<GroupState> states = new ArrayList<>(k);
    for(int i=lo;i<=hi;i+=step){
      int start = i;
      //防止索引超出范围
      int end = Math.min(start+step-1,hi);
      GroupState state = new GroupState(i,start,start,end);
      states.add(state);
      sort(a,aux,start,end,k);
    }
    //严格来说我们甚至都不需要传递lo和hi,因为它们已经包含在了states内
    //但是为了后面方便处理,我们这里显示地传入
    merge(a,aux,states,lo,hi);
  }

Ok,通过sort方法我们也生成了List<GroupState> states,最后再加上入口函数,完美

  public static void sort(Comparable[] a,int k){
    int len=a.length;
    if(len<=1 || k<=1) return;
    //guarantee k is always less or equal to a.length
    if(len<=k) k=2;
    int lo=0;
    int hi=len-1;
    Comparable[] aux = new Comparable[len];
    sort(a,aux,lo,hi,k);
  }

总结

我们从介绍归并两个有序数组入手,引入了二路归并,然后三路归并,通过二路和三路的实现,我们归纳出了k路归并.在实际的问题中,我们需要充分测试来获得k的最优值.算法第四版介绍了倍率实验,是个不错的测试方法,有兴趣的同学可以参考那本书的实现. 上面的介绍,不管是二路,三路还是k路都是归并排序的经典实现,书中还有对该实现的各种优化,也值得大家去参考.

当然,k路归并的实现还有其他方式,比如优先队列等,大家可以做进一步的研究.

通过阅读这本书,更深刻地发现,自己的算法还是刚刚入门,学习算法没有捷径,只能多思考,多练习,所以想成为算法大牛? 无他,唯手熟尔