这本质上是一个多路并归问题,可以用分治法两两并归,也可以用堆和败者树直接进行多路并归,当数据量较大时,败者树的性能应该是最好的,下面简单介绍下败者树。
败者树是胜者树的一种变体(胜者树每个节点记录胜利者)。在败者树中,用父结点记录其左右子结点进行比赛的败者,而让胜者参加下一轮的比赛。败者树的根结点记录的是败者,需要加一个结点来记录整个比赛的胜利者。采用败者树可以简化重构的过程。
Fig. 3
Fig. 3是一棵败者树。规定数大者败。
- b3 PK b4,b3胜b4负,内部结点ls[4]的值为4;
- b3 PK b0,b3胜b0负,内部结点ls[2]的值为0;
- b1 PK b2,b1胜b2负,内部结点ls[3]的值为2;
- b3 PK b1,b3胜b1负,内部结点ls[1]的值为1;
- 在根结点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用于实际操作。