两个可能有环链表可能相交求交点问题
面试题
给定两个可能有环也可能无环的单链表,头节点head1和head2。请实现一个函数,如果两个链表相交,请返回相交的 第一个节点。如果不相交,返回null 【要求】 如果两个链表长度之和为N,时间复杂度请达到O(N),额外空间复杂度 请达到O(1)。
思考
1)首先确定一点:一个单链表最多只能有一个环,因为走向是一定的, 只能往一边跑,一旦成环就会无限循环出不来了 2)两个可能有环可能无环的单链表有如下情况
-
两个都无环
一旦相交后会朝着一个方向走,所以若是相交只会是这样
-
两个都有环
由于一个链表只能有一个环,相交后只能往一边走,所以如果相交且有环,那么只会有如下情况,脑补的乱七八糟的玩意儿都不成的。
其实第二和第四可以看做一种情况。共用一个环的时候如果两个链表入环的点不同那么随便返回一个链表的入环点都是它们第一次相交的点。
-
一个有环一个没环
可以假设一下,1)在相交前有环(成环后就出不来了,还怎么相交)、2)在相交之后成环,额,相交之后走的同一条路,那么就不可能一个有一个没有了。所以这种情况下是不可能相交的。
通过观察这些个相交的情况,我发现,链表相交点形成一个汇流的作用,就是多进单出,不管多少条小溪流到江口都会汇成一条江流下去。这就更能验证我们前面研究的情况了,要么是汇成一条后再成环,要么就是一个汇进另一个的环里。
一个成环链表想要和另一个链表相交,交点只能在入环点前,或者是在环中,因为它只能在环里不停的转了。
3)为了分辨情况,最首先需要判断一个链表是否成环,成环就返回那个入环点。
思路:
1)是否有环:使用快慢指针,因为快指针一次走两步,慢指针每次走一步,所以假如有环,那么快指针一定会碰上慢指针。而若是走到null了还没碰上,说明没有环,有环它会一直转出不来的。
2)入环点:有这么个通过追及问题推出来的一个定理,额,没证明咋推出来的。就是前面快慢指针碰面之后把快指针给它又拉回起始位置,然后让快慢指针都每次走一步,最后它们相遇的地方就是入环点。哈哈,听起来有点玄乎好像,但是确实是有科学依据的。
证明:
好吧,我还是好奇去查找博客研究了一下这看起来玄乎的很的玩意怎么个原理:其实挺简单就能推理的到。
为了好说,先上一个找来的图。
慢指针假设走了s步,快指针每次走两步那么就走了2s步。于是想要相遇就会有这条式子成立。s + ny = 2s(n>=1) ,这个意思就是,快指针比慢指针多走了n圈它们就会相遇。s = x + d + n1y①,2s = x + d +n2y②,这里我就没有研究到底相遇的时候慢指针能不能走完一圈,也没啥必要。n2肯定大于n1,那么联立上面两试,咱们需要求的就是d。2①-②整理得:
这公式看出门道了没,奇妙的事情发生了,(n2-2n1)y表示走了整数圈,(n2-2n1)y-d表示从某个位置走了整数圈后回到开始点然后向反方向走d步,相遇点往回走d步不就刚好回到入口点吗。这公式表明一个指针从头开始走,一个指针从相遇点开始走,那么它们会在入口点相遇。没那么玄乎,是有科学依据的。
java实现找入环点:
public static Node checkIsLoop(Node head) {
if (head == null) return null;
Node slow = head;
Node fast = head;
boolean isLoop = false;
while (fast.next != null && fast.next.next != null) {
slow = slow.next;
fast = fast.next.next;
if (slow == fast) {
isLoop = true;
break;
}
}
if (!isLoop) return null;
//如果相遇了表示有环且slow和fast处于相遇点
fast = head;
while (fast != slow) {
slow = slow.next;
fast = fast.next;
}
//必定相遇在入环点
return slow;
}
根据前面的探讨,若是两个可能成环的链表想要有交点,那么只能都有环或者都无环,所以使用这个方法来进行调控,不同情况不同处理,若是只有一个链表有环就直接返回null。
分情况讨论
思考
1)若是两个都无环,那么相交与否就看交点是否相同,不同绝不相交。
2)若是两个都有环,那么这个环肯定是共用的同一个
2.1)若是入环点相同就绝对是有交点,交点在入环点或者在入环之前
2.2)若是入环点不同,那么可能是用的同一个环,但是是从不同地方入的环,那么随便返回一个入环点都是第一个相交的点。
public static Node getIntersectNode(Node head1, Node head2) {
if (head1 == null || head2 == null) return null;
Node loopEntry1 = checkIsLoop(head1);
Node loopEntry2 = checkIsLoop(head2);
if (loopEntry1 == null && loopEntry2 == null) {
return noLoop(head1, head2);//1)解决
}
//若是都有环且还想要有交点(必然共一个环),那环必定是在交点之后
//但是考虑到可能会有共有一个环且入环点不同的情况,所以入环点是否相同不能绝对的作为划分是否相交的条件
//但是入环点相同必然有交点,交点在入环点或者在其之前
if (loopEntry2 != null && loopEntry1 != null) {
if (loopEntry1 == loopEntry2) {
return bothLoop1(head1, head2, loopEntry1);//2.1)解决
} else {
return bothLoop2(head1, head2, loopEntry1, loopEntry2);//2.2)解决
}
}
return null;//只有一个有环必不会有交点,返回null
}
解决情况1)
思考
如何判断两个无环链表是否相交?
相交后会走一样的路,那么肯定会走到同一个终点,那么若是终点不同是不是就证明没有交点。
对于无环且相交的情况如何求交点?
首先明确一点,相交之后大家走的路都是一样长的,所以若是能找到两个链表的长度差那么让长的先走个长度差距离,然后短的也开始走最后会在交点处相遇。
链表长度差怎么求?
就是求出一个然后减去另一个嘛,若是能够知道它们两个哪部分是相同的那可以跳过那部分,比如说一起入环了,那么入环点后面部分肯定相同了,长度差不就是两个走到入环点的距离差嘛。
综上所述可以编写出情况一的解决方法。如下:
private static Node noLoop(Node head1, Node head2) {
Node head1Help = head1;
int i = 0;
while (head1Help.next != null) {
head1Help = head1Help.next;
i++;
}
//head1Help到链表1的结尾,然后i记录了链表的长度
Node head2Help = head2;
while (head2Help.next != null) {
head2Help = head2Help.next;
i--;
}
if (head1Help != head2Help) return null;//两个不同结尾肯定没交点
//head2Help到链表2的结尾,然后i的绝对值表示两个链表的长度差,i为负表示链表2长
return getCrossFrontLoop(head1, head2, i);
}
/**
* @param i head1的长度减去head2的长度
* @return 返回两个链表在成环之前相交情况下的交点,若是没交点别用这个方法,会死循环的
*/
private static Node getCrossFrontLoop(Node head1, Node head2, int i) {
Node head1Help;
Node head2Help;
head1Help = head1;
head2Help = head2;
if (i < 0) {
//链表2先走i绝对值步
for (int j = 0; j < Math.abs(i); j++)
head2Help = head2Help.next;
} else {
//链表1先走i绝对值步
for (int j = 0; j < Math.abs(i); j++)
head1Help = head1Help.next;
}
//同时走到相遇即为交点
while (head1Help != head2Help) {
head1Help = head1Help.next;
head2Help = head2Help.next;
}
return head1Help;
}
解决情况2)
2.1)
/*
入环点相同:必然有交点,在入环点或者在入环点之前
找交点方式和之前那种noLoop的方式一样,由于交点后它们的长度都相同,所以长的先走个长度差然后短的也开始走就
会在交点相遇
*/
private static Node bothLoop1(Node head1, Node head2, Node loopEntry) {
Node head1Help = head1,head2Help = head2;
int i = 0;
while(head1Help!=loopEntry){
head1Help = head1Help.next;
i++;
}
//i为head1到入环点的距离
while (head2Help!=loopEntry){
head2Help = head2Help.next;
i--;
}
//i为距离差
return getCrossFrontLoop(head1,head2,i);
}
2.2 )
如何判断入环点不同但是用的同一个环?
从一个入环点开始移动走一圈,如果在返回这个入环点之前没碰到另一个入环点是不是就说明没在一个环里?这样就能分出两种情况了。
实现:
/**
* @param loopEntry1 链表1的入环点
* @param loopEntry2 链表2的入环点
* @return
*/
private static Node bothLoop2(Node head1, Node head2, Node loopEntry1, Node loopEntry2) {
//让一个入环点开始走,若是在走回自己之前没碰到另一个入环点表示它们不是公用一个环
Node loopEntry2Help = loopEntry2.next;
while (loopEntry2Help != loopEntry2) {
if (loopEntry2Help == loopEntry1) {
//表示共用了一个环
return loopEntry1;//随便返回一个入环点都是第一个相交点
}
loopEntry2Help = loopEntry2Help.next;
}
return null;
}