这是一道非常经典的面试题,不少人问过我蛮多次了。
我回答他们,下次去网上搜一搜呀。
然后他们告诉我,面试的是JAVA程序员,但是网上的都是C、C#之类的语言,有没有办法用JAVA实现呢?他们还补充道,最好能使用到集合类中的List。
Er....其实万变不离其宗,不过既然有这个需求,那么我就来用Java中的List实现一下这个题目吧。
而且真要我一时半会儿马上把这个实现并讲清楚,其实也是比较麻烦的,写篇博客记录一下吧。
>原题:
Merge k sorted linked lists and return it as one sorted list. Analyze and describe its complexity.
>解题思路
1.全局上,设计一个K归并算法,可以遍历整个List[]集合,把数据归并到一块去;
2.细节上,采用两两归并的方法,把两个List归并为一条。
总的来说,就是partition加merge的思路。
>核心代码
首先,我们要先实现把两个List进行归并的算法:
public static List<Integer> merge2List(List<Integer> l1, List<Integer> l2) {
//容错性处理
if(l2.size()==0) return l1;
if(l1.size()==0) return l2;
List<Integer> temp=new LinkedList<Integer>();
int i=0,j=0;
while(temp.size()!=(l1.size()+l2.size())){
if(i>=l1.size()){
//l1到头,把剩余的l2里的值,装到temp里去
while(j<l2.size()){
temp.add(l2.get(j++));
}
break;
}
else if(j>=l2.size()){
while(i<l1.size()){
temp.add(l1.get(i++));
}
break;
}
else if(l1.get(i) > l2.get(j)){
//l2的值较小,取l2的值放到temp,同时l2的指针向前推进一格
temp.add(l2.get(j++));
}
else{
temp.add(l1.get(i++));
}
}
return temp;
}
这段代码说得蛮清楚的,就是逐个扫描List l1和l2中的元素,然后把他们归并到一起去。
不难看出,整个算法的复杂度是:
F(n)=l1.size()+l2.size(),我们记为,多次归并后的总运行次数,我们记为:F1(n);
然后是对K条List进行归并的算法:
public static List<Integer> mergeKLisT(){
if(K == 0) return null;
if(K == 1) return ls[0];
for(int i=2;i/2<K;i*=2) {//i : 0~2*K
for(int j=0;j<K;j+=i) {
if(j+i/2>=K)break;//越界处理
ls[j] = merge2List(ls[j],ls[j+i/2]);
}
}
return ls[0];
}
>归并流程
分析这段代码,前两行没啥好说的,就是做一下健壮性的处理。
下面的两个for循环,仔细想想,其过程是这样的:(下文中的数字为要合并的List[]的index)
1.把0 1,2 3,4 5,....k-2 k-1(假如K是偶数,K是奇数时由于merge2List算法中会直接返回非空的那个,因此原理其实是一样的)两两合并,归并成0,2,4,6,8,....,k-2这样的List集合
2.把0 2,4 6,8 10,..k-4 k-2归并为0,4,8,12,16....k-4
3.把0 4,8 12,....k-8 k-4合并为xxxx
重复这个步骤,直到0与K/2进行合并,最后得到结果ls[0];
>算法复杂度
总体的算法复杂度为:
F(n,K)=SUM(K/i)*F1(n),[i:2~K]。
也就是:
F(n,K)=(K-1)*F1(n)
>优化分析
整体来看,是复杂度为N级别的算法。(N=K)
相比暴力解法K*K*ListAll.size()这种N^2级别的算法,基本上是降了一个数量级。
有人可能要问,那分而治之的算法和下面这个算法比较有何不同:
for(int i=1;i<k;i++){
ls[0]=merge2List(ls[0],ls[i]);//算法II
}
这个算法的复杂度只有F(n,K)=(K-1)*F2(n);看起来好像跟上面那个差不多。
其实不然,主要是因为F1(n)和F2(n)有着非常大的差距。
推导见下:(ai为各个List中元素的数量,log为以2为底的对数)
F1(n)=log(K)*SUM(ai);
F2(n)=SUM(i*ai)=K/2*SUM(ai);其中,ai为第i个链表的大小。[i:1 ~ K]
取R=F1(n)/F2(n)=log(K)*2/K;
对R<1,也就是K-2*log(K)>0画图:
运算可知当K>=0时,R<1恒成立。
也就是说,选择分而治之的归并方法总是很有利的。且随着K的增大,这个算法的利好越明显。
-------------------------------
样例完整代码见下:
import java.util.LinkedList;
import java.util.List;
public class KSortList {
private static int K=4;
@SuppressWarnings("unchecked")
private static List<Integer>[] ls=new LinkedList[K];
private static List<Integer> result=new LinkedList<Integer>();
public static void main(String args[]){
init();
result=mergeKLisT();
print(result);
}
public static List<Integer> mergeKLisT(){
if(K == 0) return null;
if(K == 1) return ls[0];
for(int i=2;i/2<K;i*=2) {//i : 0~2*K
for(int j=0;j<K;j+=i) {
if(j+i/2>=K)break;//越界处理
ls[j] = merge2List(ls[j],ls[j+i/2]);
}
}
return ls[0];
}
public static List<Integer> merge2List(List<Integer> l1, List<Integer> l2) {
//容错性处理
if(l2.size()==0) return l1;
if(l1.size()==0) return l2;
List<Integer> temp=new LinkedList<Integer>();
int i=0,j=0;
while(temp.size()!=(l1.size()+l2.size())){
if(i>=l1.size()){
//l1到头,把剩余的l2里的值,装到temp里去
while(j<l2.size()){
temp.add(l2.get(j++));
}
break;
}
else if(j>=l2.size()){
while(i<l1.size()){
temp.add(l1.get(i++));
}
break;
}
else if(l1.get(i) > l2.get(j)){
//l2的值较小,取l2的值放到temp,同时l2的指针向前推进一格
temp.add(l2.get(j++));
}
else{
temp.add(l1.get(i++));
}
}
return temp;
}
public static void init(){
for(int i=0;i<K;i++)ls[i]=new LinkedList<Integer>();//初始化
setDemoData();//设置测试数据
}
public static void setDemoData(){
ls[0].add(0);ls[0].add(2);ls[0].add(4);ls[0].add(5);ls[0].add(11);
ls[1].add(25);ls[1].add(28);
ls[2].add(0);ls[2].add(1);ls[2].add(5);
ls[3].add(1);ls[3].add(5);
}
public static void print(List<Integer> list){
for(int i=0;i<list.size();i++){
System.out.println(list.get(i)+" ");
}System.out.println();
}
}