上一篇我们说到了三路归并的代码,结合二路归并和三路归并的代码,我们可以写出k路归并的实现.其中有两个核心的地方需要理解,理解了这两个地方,其他很容易写出来,毕竟就两个核心的方法sort
和merge
.下面我们重点说一下这两个地方,第一个就是如何组织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
其实也简单,只要我们拿到了步长(每路元素的个数),就能很容易地知道每路的起止索引,这里说的止,对应GroupState
的maxPos
属性.
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路归并的实现还有其他方式,比如优先队列等,大家可以做进一步的研究.
通过阅读这本书,更深刻地发现,自己的算法还是刚刚入门,学习算法没有捷径,只能多思考,多练习,所以想成为算法大牛? 无他,唯手熟尔