刷过链表题目的小伙伴都应该有这样的体会,链表题最容易出错的就是 空指针异常 。做着做着“链断了”。因此,对于链表的题目来说,Coding 能力非常重要,通过大量题目的训练,练习对于 边界条件 判断的处理能力。
下面我们练习一道 非常综合 的题目,一题顶三题!
给定两个可能 有环 可能 无环 的单链表,头节点分别为 head1 和 head2 。
实现如下功能:如果两个链表相交,返回相交的第一个节点;如果不相交,返回 null 。
要求 :如果两个链表长度之和为 N,时间复杂度 O(N),空间复杂度 O(1)。
// 单链表结构体定义
public static class Node {
public int value;
public Node next;
public Node(int value) {
this.value = value;
}
}
要想完美解答该题,我们先来做另外几道题。
给定一个链表的头节点 head ,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。不允许修改链表。
直接给出 力扣官方题解 的算法思路,使用 快慢指针 解题:
两个指针 fast 与 slow, 起始指向链表的头部。slow 每次向后移动一个位置, fast 指针向后移动两个位置。如果链表中存在环,则 fast 指针最终将再次与 slow 指针在环中相遇。
设链表中环外部分的长度为 a。slow 指针进入环后,又走了 b 的距离与 fast 相遇。此时,fast 指针已经走完了环的 n 圈,因此它走过的总距离为:
a + n ( b + c ) + b = a + ( n + 1 ) b + n c a+n(b+c)+b=a+(n+1)b+nc a+n(b+c)+b=a+(n+1)b+nc
根据题意,任意时刻,fast 指针走过的距离都为 slow 指针的 2 倍。因此,我们有
a + ( n + 1 ) b + n c = 2 ( a + b ) ⟹ a = c + ( n − 1 ) ( b + c ) a+(n+1)b+nc=2(a+b) ⟹ a=c+(n−1)(b+c) a+(n+1)b+nc=2(a+b)⟹a=c+(n−1)(b+c)
的等量关系,我们会发现:从相遇点到入环点的距离加上 n−1 圈的环长,恰好等于从链表头部到入环点的距离。
因此,当 slow 与 fast 相遇时,fast 重新指向链表头部;fast 和 slow 每次向后移动一个位置。最终,它们会在入环点相遇。
private static Node getLoopNode(Node head) {
if (head == null || head.next == null || head.next.next == null) {
return null;
}
Node slow = head.next;
Node fast = head.next.next;
while (slow != fast) {
if (fast.next == null || fast.next.next == null) {
return null;
}
slow = slow.next;
fast = fast.next.next;
}
fast = head;
while (slow != fast) {
slow = slow.next;
fast = fast.next;
}
return slow;
}
给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点,返回 null 。
如果两个链表相交且长度相同,那么两个链表都从头结点开始遍历,相遇时就找到了相交点;
如果两个链表长度不相同时,若能够也从到达结尾长度相同的点出发时,也一定能够给找到相交点。
因此,只需要将长链表走过一个长度差后,两链表再同时出发即可。
private static Node noLoop(Node h1, Node h2) {
if (h1 == null || h2 == null) {
return null;
}
Node cur1 = h1;
Node cur2 = h2;
int n = 0;
while (cur1.next != null) {
cur1 = cur1.next;
n++;
}
while (cur2.next != null) {
cur2 = cur2.next;
n--;
}
if (cur1 != cur2) {
return null;
}
cur1 = n > 0 ? h1 : h2;
cur2 = cur1 == h1 ? h2 : h1;
n = Math.abs(n);
while (n != 0) {
n--;
cur1 = cur1.next;
}
while (cur1 != cur2) {
cur1 = cur1.next;
cur2 = cur2.next;
}
return cur1;
}
用 n
来计算两个链表的长度差,cur1 = n > 0 ? h1 : h2;
为了后续代码简单起见,将长链表重新用 cur1
指向,较短的链表用 cur2
指向。 cur1
先走过长度差 n
步后,两个链表指针同时出发,当 cur1 == cur2
时,指针指向同一片内存空间,即链表相交了。
上面两道题分别给出了 有环链表找入环点 问题和 无环链表找相交点 问题的解答。相信大家都学会了,接下来,我们来解决 文章开头提出的问题:
给定两个可能 有环 可能 无环 的单链表,头节点分别为 head1 和 head2 。实现如下功能:如果两个链表相交,返回相交的第一个节点;如果不相交,返回 null 。
假设两个链表分别是 A 和 B,则可以将该问题分为以下几种情况:
A B 均无环。如果 A B两链表相交,则转化为了 无环链表找相交点 的问题。
A 无环,B 有环;A 有环,B 无环;不可能相交。
A B 均有环:
有了上面两道题的解答,再来解决本问题就轻松很多,首先来看主函数:
public static Node getIntersectNode(Node h1, Node h2) {
if (h1 == null || h2 == null) {
return null;
}
Node loop1 = getLoopNode(h1);
Node loop2 = getLoopNode(h2);
if (loop1 == null && loop2 == null) {
return noLoop(h1, h2);
}
if (loop1 != null && loop2 != null) {
return bothLoop(h1, loop1, h2, loop2);
}
return null;
}
如果两个链表一个为空,一定不会有环,直接返回空。
调用 getLoopNode
函数分别得到两个链表的入环结点。
若 loop1
和 loop2
均为空,说明两个链表均为 无环单链表 。转化为了 情况 1,即 LeetCode:160.相交链表 问题。
若 loop1
和 loop2
均不为空,说明两个链表均为 有环单链表 。转化为了 情况 3,此时就要判断是 情况 3 中的哪一种:
private static Node bothLoop(Node h1, Node loop1, Node h2, Node loop2) {
Node cur1 = null;
Node cur2 = null;
if (loop1 == loop2) {
cur1 = h1;
cur2 = h2;
int n = 0;
while (cur1 != loop1) {
cur1 = cur1.next;
n++;
}
while (cur2 != loop2) {
cur2 = cur2.next;
n--;
}
cur1 = n > 0 ? h1 : h2;
cur2 = cur1 == h1 ? h2 : h1;
n = Math.abs(n);
while (n != 0) {
n--;
cur1 = cur1.next;
}
while (cur1 != cur2) {
cur1 = cur1.next;
cur2 = cur2.next;
}
return cur1;
} else {
cur1 = loop1.next;
while (cur1 != loop1) {
if (cur1 == loop2) {
return loop1;
}
cur1 = cur1.next;
}
return null;
}
}
我们将 2)和 1)3)分成两大类进行判断:
if
条件进行的是 2)A 环 B 环相交,有同一个入口点
情况的判断。
以 入口点 作为两个链表的结束,红色范围 左侧 就转化为了 两个单链表找交点 的题目。
因此代码就很好理解了,先计算两个链表长度的差值,长链表先走过差值,之后两指针一起走,相遇即找到 入环点 。
else
条件进行的是 1)不相交 3)相交于不同的入口点
情况的判断。
1)如果两个环相交,那么 loop1
一定会在 一圈未走完 的情况下与 loop2
相遇。
2)若走了一圈又回到了 loop1
,则说明两个环一定未相交。
至此,本题就得到了完美的解答。
本题属于多道题目的杂糅,情况较多,需要理清思路一一破解。
通过对本题的分析,既熟练的掌握了 两个单链表的相交 问题,也掌握了 链表有无环 的判断方法。 一举多得!
一定要记得自己写一遍哦!注意边界条件的判断~