判断两个链表是否相交
问题描述:
一个比较经典的问题,判断两个链表是否相交,如果相交找出他们的交点。
思路:
1、碰到这个问题,第一印象是采用hash来判断,将两个链表的节点进行hash,然后判断出节点,这种想法当然是可以的。
2、当然采用暴力的方法也是可以的,遍历两个链表,在遍历的过程中进行比较,看节点是否相同。
3、第三种思路是比较奇特的,在编程之美上看到的。先遍历第一个链表到他的尾部,然后将尾部的next指针指向第二个链表(尾部指针的next本来指向的是null)。这样两个链表就合成了一个链表,判断原来的两个链表是否相交也就转变成了判断新的链表是否有环的问题了:即判断单链表是否有环?
这样进行转换后就可以从链表头部进行判断了,其实并不用。通过简单的了解我们就很容易知道,如果新链表是有环的,那么原来第二个链表的头部一定在环上。因此我们就可以从第二个链表的头部进行遍历的,从而减少了时间复杂度(减少的时间复杂度是第一个链表的长度)。
下图是一个简单的演示:
这种方法可以判断两个链表是否相交,但不太容易找出他们的交点。
4、仔细研究两个链表,如果他们相交的话,那么他们最后的一个节点一定是相同的,否则是不相交的。因此判断两个链表是否相交就很简单了,分别遍历到两个链表的尾部,然后判断他们是否相同,如果相同,则相交;否则不相交。示意图如下:
判断出两个链表相交后就是判断他们的交点了。假设第一个链表长度为len1,第二个问len2,然后找出长度较长的,让长度较长的链表指针向后移动|len1 - len2| (len1-len2的绝对值),然后在开始遍历两个链表,判断节点是否相同即可。
下面给出一个简单的实现:
typedef struct node_t
{
int data;//data
struct node_t *next; //next
}node;
node* find_node(node *head1, node *head2)
{
if(NULL == head1 || NULL == head2)
{
return NULL;//如果有为空的链表,肯定是不相交的
}
node *p1, *p2;
p1 = head1;
p2 = head2;
int len1 = 0;
int len2 =0;
int diff = 0;
while(NULL != p1->next)
{
p1 = p1->next;
len1++;
}
while(NULL != p2->next)
{
p2 = p2->next;
len2++;
}
if(p1 != p2) //如果最后一个节点不相同,返回NULL
{
return NULL;
}
diff = abs(len1 - len2);
if(len1 > len2)
{
p1 = head1;
p2 = head2;
}
else
{
p1 = head2;
p2 = head1;
}
for(int i=0; i<diff; i++)
{
p1 = p1->next;
}
while(p1 != p2)
{
p1 = p1->next;
p2 = p2->next;
}
return p1;
}
通过上面的操作就可以找到两个链表的交点了。
5、总结
上面的几种方法中最后一种是比较不错的,当然hash也是可以的。
问题的延伸:
如果原来的两个链表中有环怎么处理?
题目 :
给出两个链表的头指针,比如h1,h2,判断这两个链表是否相交。
扩展:
(1) 如果链表可能有环呢?
(2) 如何求出两个相交链表的相交的第一个节点。
如果链表没有环
假设两个链表没有环,如果它们相交,那么它们的最后一个元素必定相同。
Java代码
- public boolean isConNLoop(ListNode h1, ListNode h2) {
- if (h1 == null || h2 == null)
- return false;
- ListNode n1 = h1;
- ListNode n2 = h2;
- while (n1.next != null)
- n1 = n1.next;
- while (n2.next != null)
- n2 = n2.next;
- if (n1 == n2)
- return true;
- return false;
- }
public boolean isConNLoop(ListNode h1, ListNode h2) {
if (h1 == null || h2 == null)
return false;
ListNode n1 = h1;
ListNode n2 = h2;
while (n1.next != null)
n1 = n1.next;
while (n2.next != null)
n2 = n2.next;
if (n1 == n2)
return true;
return false;
}
它们的交点为
Java代码
- public ListNode findPointNLoop(ListNode h1, ListNode h2) {
- if (h1 == null || h2 == null)
- return null;
- ListNode n1 = h1;
- ListNode n2 = h2;
- int len1 = 0;
- int len2 = 0;
- while (n1 != null) {
- n1 = n1.next;
- len1++;
- }
- while (n2 != null) {
- n2 = n2.next;
- len2++;
- }
- n1 = h1;
- n2 = h2;
- if (len1 < len2) {
- n1 = h2;
- n2 = h1;
- }
- for (int i = len1-len2; i > 0; i--)
- n1 = n1.next;
- while (n1 != null && n1 != n2) {
- n1 = n1.next;
- n2 = n2.next;
- }
- return n1;
- }
public ListNode findPointNLoop(ListNode h1, ListNode h2) {
if (h1 == null || h2 == null)
return null;
ListNode n1 = h1;
ListNode n2 = h2;
int len1 = 0;
int len2 = 0;
while (n1 != null) {
n1 = n1.next;
len1++;
}
while (n2 != null) {
n2 = n2.next;
len2++;
}
n1 = h1;
n2 = h2;
if (len1 < len2) {
n1 = h2;
n2 = h1;
}
for (int i = len1-len2; i > 0; i--)
n1 = n1.next;
while (n1 != null && n1 != n2) {
n1 = n1.next;
n2 = n2.next;
}
return n1;
}
如果链表有环
如果链表有环且相交,那么这两个链表都是有环的。
找到第一个链表的环点,然后将环断开(当然不要忘记了保存它的下一个节点),然后再来遍历第二个链表,如果发现第二个链表从有环变成了无环,那么他们就是相交的嘛,否则就是不相交的了。
Java代码
- public boolean isConLoop(ListNode h1, ListNode h2) {
- ListNode temp = loopEntry(h1);
- ListNode org = temp.next;
- temp.next = null;
- if (isLoop(h2)) {
- temp.next = org;
- return false;
- } else {
- temp.next = org;
- return true;
- }
- }
- public ListNode loopEntry(ListNode head) {
- if (head == null)
- return null;
- ListNode slow = head;
- ListNode fast = slow.next;
- while (fast != null && fast.next != null && fast != slow) {
- slow = slow.next;
- fast = fast.next.next;
- }
- if (fast == slow) {
- fast = head;
- slow = slow.next;
- while (fast != slow) {
- slow = slow.next;
- fast = fast.next;
- }
- return slow;
- }
- return null;
- }
public boolean isConLoop(ListNode h1, ListNode h2) {
ListNode temp = loopEntry(h1);
ListNode org = temp.next;
temp.next = null;
if (isLoop(h2)) {
temp.next = org;
return false;
} else {
temp.next = org;
return true;
}
}
public ListNode loopEntry(ListNode head) {
if (head == null)
return null;
ListNode slow = head;
ListNode fast = slow.next;
while (fast != null && fast.next != null && fast != slow) {
slow = slow.next;
fast = fast.next.next;
}
if (fast == slow) {
fast = head;
slow = slow.next;
while (fast != slow) {
slow = slow.next;
fast = fast.next;
}
return slow;
}
return null;
}
寻找环点的方法如下:
两个指针,一个走一步,一个走两步,在环中相遇位置为X。然后从头节点和X位置,分别一步一步的走,每次判断是否相遇,相遇点就是所求节点。
证明如下:假设头节点位置为A,第一个节点为M,相遇节点为X,环长为Len。
因为快节点每次比慢节点快一步,慢节点进入后快节点不用一圈就能赶上慢节点了。
慢节点走的路程 = AM + MX
快节点走的路程 = AM + MX + n * Len
=> 2*(AM + MX ) = AM + MX + n *Len
=> AM + MX = n*Len
=> AM = (n-1)*Len + XM,说明从A到M的距离与X到M的距离,模环长同余。因此分别从A和X走,必然相交于X节点。
当两个有环的链表相交时,有以下两种情况:
在这种情况下,两个链表的交点在环点之前,可以将环点切断,这样就变成了两个无环的链表求相交点。可使用以上方法。
另一种情况为:
在这种情况下,不存在所谓的相交点。
有一个单链表,其中可能有一个环,也就是某个节点的next指向的是链表中在它之前的节点,这样在链表的尾部形成一环。
问题:
1、如何判断一个链表是不是这类链表?
2、如果链表为存在环,如何找到环的入口点?
解答:
一、判断链表是否存在环,办法为:
设置两个指针(fast, slow),初始值都指向头,slow每次前进一步,fast每次前进二步,如果链表存在环,则fast必定先进入环,而slow后进入环,两个指针必定相遇。(当然,fast先行头到尾部为NULL,则为无环链表)程序如下:
bool IsExitsLoop(slist *head)
{
slist *slow = head, *fast = head;
while ( fast && fast->next )
{
slow = slow->next;
fast = fast->next->next;
if ( slow == fast ) break;
}
return !(fast == NULL || fast->next == NULL);
}
二、找到环的入口点
当fast若与slow相遇时,slow肯定没有走遍历完链表,而fast已经在环内循环了n圈(1<=n)。假设slow走了s步,则fast走了2s步(fast步数还等于s 加上在环上多转的n圈),设环长为r,则:
2s = s + nr
s= nr
设整个链表长L,入口环与相遇点距离为x,起点到环入口点的距离为a。
a + x = nr
a + x = (n – 1)r +r = (n-1)r + L - a
a = (n-1)r + (L – a – x)
(L – a – x)为相遇点到环入口点的距离,由此可知,从链表头到环入口点等于(n-1)循环内环+相遇点到环入口点,于是我们从链表头、与相遇点分别设一个指针,每次各走一步,两个指针必定相遇,且相遇第一点为环入口点。程序描述如下:
slist* FindLoopPort(slist *head)
{
slist *slow = head, *fast = head;
while ( fast && fast->next )
{
slow = slow->next;
fast = fast->next->next;
if ( slow == fast ) break;
}
if (fast == NULL || fast->next == NULL)
return NULL;
slow = head;
while (slow != fast)
{
slow = slow->next;
fast = fast->next;
}
return slow;
}
扩展问题:
判断两个单链表是否相交,如果相交,给出相交的第一个点(两个链表都不存在环)。
比较好的方法有两个:
一、将其中一个链表首尾相连,检测另外一个链表是否存在环,如果存在,则两个链表相交,而检测出来的依赖环入口即为相交的第一个点。
二、如果两个链表相交,那个两个链表从相交点到链表结束都是相同的节点,我们可以先遍历一个链表,直到尾部,再遍历另外一个链表,如果也可以走到同样的结尾点,则两个链表相交。
这时我们记下两个链表length,再遍历一次,长链表节点先出发前进(lengthMax-lengthMin)步,之后两个链表同时前进,每次一步,相遇的第一点即为两个链表相交的第一个点。
法1、对链表1中的每个节点p1,判断链表2中是否有一个节点p2指向p1
loop:p1从head1到最后一个节点
loop:p2从head2到最后一个节点
if(p2是否指向p1)
相交
break
时间复杂度:O(list1.length * list2.length)
空间复杂度:O(1)
法2、使用hash表
loop:p1从head1到最后一个节点
把p1放入hash表table中
loop:p2从head2到最后一个节点
if(p2在hash表中)
相交
时间复杂度:O(list1.length + list2.length)
空间复杂度:O(list1.length)
法3、将其中一个链表首尾相连,检测另一个链表是否存在环,如果存在,则两个链表相交,而检测出来的依赖环入口点即为相交的第一个点。程序描述如下:
找到list1的最后一个节点tail1
tail1->next=head1
判断head2是否存在环
tail1->next=NULL; //恢复tail1
法4、如果两个链表相交,那么两个链表从相交点到链表结束都是相同的节点。可以先分别遍历找出两个链表的尾节点,如果连个尾节点相同,则两个链表相交。程序描述如下:
//找到list1的最后一个节点p1
p1=head1
while(p1->next不是NULL)
p1=p1->next
找出list2的最后一个节点p2
if(p1==p2)
相交
else
不相交
时间复杂度:O(list1.length + list2.length)
空间复杂度:O(1)
扩展问题4、如果两个链表相交,找出相交的第一个节点?
在判断相交的过程中要分别遍历两个链表,同时记下各自的长度。然后再遍历一次:长链表节点先从头节点出发前进(lengthMax-lenghMin)步,之后两个链表同时前进,每次一步,相遇的第一个节点即为两个链表相交的第一个节点。程序描述如下:
Node *intersection(Node *head1, Node *head2)
if(!head1 || !head2)
return NULL;
int len1 = 1;
int len2 = 1;
bool result = false;
//判断两个链表是否相交,同时记下各个链表的长度
Node *p = head1;
while(p->next)
pLen++; p=p->next
q=head2
while(q->next)
len2++; q=q->next
result=(p==q)
if(result)
int steps = abs(len1 – len2)
Node *head = len1 > len2 ? head1 : head2;
while(steps)
head = head->next
steps –-
len1 > len2 ? p = head,q=head2 ? q = head,p=head1
while(p!=q)
p=p->next
q=q->next
return p
return NULL