Leetcode_23. Merge k Sorted Lists

本文深入探讨了败者树的概念及其在多路归并中的应用,通过对比堆和败者树的性能,详细介绍了败者树的构建与调整过程,并提供了具体的代码实现。

        这本质上是一个多路并归问题,可以用分治法两两并归,也可以用堆和败者树直接进行多路并归,当数据量较大时,败者树的性能应该是最好的,下面简单介绍下败者树。

       败者树是胜者树的一种变体(胜者树每个节点记录胜利者)。在败者树中,用父结点记录其左右子结点进行比赛的败者,而让胜者参加下一轮的比赛。败者树的根结点记录的是败者,需要加一个结点来记录整个比赛的胜利者。采用败者树可以简化重构的过程。

Fig. 3

Fig. 3是一棵败者树。规定数大者败。

  1. b3 PK b4,b3胜b4负,内部结点ls[4]的值为4;
  2. b3 PK b0,b3胜b0负,内部结点ls[2]的值为0;
  3. b1 PK b2,b1胜b2负,内部结点ls[3]的值为2;
  4. b3 PK b1,b3胜b1负,内部结点ls[1]的值为1;
  5. 在根结点ls[1]上又加了一个结点ls[0]=3,记录的最后的胜者。             

    败者树的建立:

1.用一个数组External[k+1]来保存k路归并的头数据,多出一个External[k]的位置设为MINKEY(可能的最小值)

  再用一个数组LoserTree[k]来保存各个头数据在External中的索引,初始化都设为k,这样刚好对应MINKEY(绝对的胜者)

2.从各叶子结点溯流而上,调整败者树中的值。拿胜者s(初始为叶结点值)与其父结点中值比较,谁败(较大的)谁上位(留着父结点中),最后的胜者被记录在LoserTree[0]中。(决出胜者,记录败者,胜者向上走)

     当建立完败者树后,每次只要在胜者被取出的位置插入新的值,并进行向上调整,就能不断地实现多路归并了。

     值得注意的是,在不断更新的过程中,虽然胜者LoserTree[0]一定是最小值,但LoserTree[1]却不一定是第二小值。如果LoserTree[1]一定是第二小值,新插入的位置向上调整显然能得到最小值。但在明知不是的情况下,为什么简单地向上调整仍能得到最小值呢?很多地方都没有证明这种方案的可行性。稍微思考下其实可以通过数学归纳法证明。

    比较败者树和堆的性能:

       败者树在维护的时候,比较次数是logn+1,  败者树从下往上维护,每上一层,只需要和父节点比较一次,而堆是自上往下维护,每一层需要和左右子节点都比较,需要比较两次,从这个角度,败者树比堆更优一点,但是,败者树每一次维护,必然是从叶子节点到根节点的一条路径,而堆维护的时候有可能在中间某个层次停止,这样败者树虽然每层比堆比较的次数少,但是堆比较的层数可能比较少。

下面是具体实现代码:

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) { val = x; }
 * }
 */
public class Solution {
	public static int k,count;//k记录总链表数量,count记录当前还未处理完的链表数量
	public static int[] loserTree;
	public static int[] External;
	public static ListNode head=null;
	public static ListNode first=null;
	public static ListNode mergeKLists(ListNode[] lists) {
         	head=null;//由于函数会多次调用,每次开始都要清空全局变量
         	first=null;
		 	if(lists==null||lists.length<=0)  
	            return null;  
	        if(lists.length==1)  
	            return lists[0];
	        k=lists.length;
	        count=k;
	        loserTree=new int[k];
	        External=new int[k+1];
	        return K_Merge(lists);
	 }
	 public static ListNode K_Merge(ListNode[] lists){
		 ListNode temp;
		 int p;//记录本次被选中的链表索引
		 //初始化External数组
		 for(int i=0;i<k;i++){
			 if(lists[i]==null){
				 count--;
				 External[i]=Integer.MAX_VALUE;
			 }
			 else{
				 External[i]=lists[i].val;
			 }
		 }
		 CreateLoserTree();
		 while(count>0){
			 p=loserTree[0];
			 if(head==null){ 
				 //head=new ListNode(External[p]);
				 head=lists[p];//不生成新的节点,重用原来链表的节点
				 first=head;
			 }
			 else{
				 //temp=new ListNode(External[p]);
				 temp=lists[p];//不生成新的节点,重用原来链表的节点
				 first.next=temp;
				 first=temp;
			 }
			 if(lists[p].next==null) {
				 External[p]=Integer.MAX_VALUE;
				 count--;//当一个链表用尽时count--
			 }
			 else{
				 lists[p]=lists[p].next;
				 External[p]=lists[p].val;
			 }
			 Adjust(p);
		 }
		 return head;
	 }
	 
	 public static void CreateLoserTree(){
		 //配合后一个循环将loserTree的初始值都设为最小值
		 External[k]=Integer.MIN_VALUE;
		 for(int i=0;i<k;i++){
			 loserTree[i]=k;
		 }
		 //对每个数都向上调整
		 for(int i=k-1;i>=0;i--){
			 Adjust(i);
		 }
	 }
	 
	 //s指External数组中的下标
	 public static void Adjust(int s) {
		 int t=(s+k)>>1;//将External的下标转换成loserTree中的坐标
		 int temp;
		 while(t>0){
			 if(External[s] > External[loserTree[t]])
				{
					temp = s;
					s = loserTree[t];
					loserTree[t]=temp;
				}
				t=t>>1;
		 }
		 loserTree[0]=s;
	}
}

 

写代码时自己犯过的一些傻逼问题:

1.全局变量在每次函数调用开头没有初始化,导致系统多次调用时出错。

2.ListNode[] list=new ListNode[2];   定义了一个类数组后,系统只是分配了一个引用空间,并没有实际分配内存空间给数组中的元素,因此类数组中的元素还是需要使用new运算符来实例化。

3.异或运算可以交换整数,主要用到两次异或相同的数,值不发生改变的原理,但速度其实还不如中间变量交换。

4.链表如果能重用原来的空间,就不要new新对象。

5.链表总习惯用head表示头,first用于实际操作。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值