经典面试题:Java实现-归并K条已排序的链表及复杂度分析、优化

这是一道非常经典的面试题,不少人问过我蛮多次了。

我回答他们,下次去网上搜一搜呀。

然后他们告诉我,面试的是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();
	}
}


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值