LeetCode 160. 相交链表 - 完整解法详解
一、问题理解
问题描述
给你两个单链表的头节点 headA 和 headB,请你找出并返回两个单链表相交的起始节点。如果两个链表没有交点,返回 null。
示例
text
输入:intersectVal = 8, listA = [4,1,8,4,5], listB = [5,6,1,8,4,5], skipA = 2, skipB = 3
输出:Intersected at '8'
解释:相交节点的值为 8(注意,如果两个链表相交则不能为 0)。
从各自的表头开始算起,链表 A 为 [4,1,8,4,5],链表 B 为 [5,6,1,8,4,5]。
在 A 中,相交节点前有 2 个节点;在 B 中,相交节点前有 3 个节点。
要求
-
时间复杂度:O(m+n),其中 m 和 n 是两个链表的长度
-
空间复杂度:O(1)
-
链表不允许修改
-
链表中不存在环
二、核心思路:双指针法
基本思想
两个指针分别从两个链表的头节点开始遍历,当一个指针到达链表末尾时,将其重定向到另一个链表的头节点。如果两个链表相交,那么这两个指针最终会在相交节点相遇;如果不相交,两个指针会同时到达 null。
数学原理
设链表 A 的非公共部分长度为 a,链表 B 的非公共部分长度为 b,公共部分长度为 c。
-
指针 p 走过的路径:a + c + b(如果相交)
-
指针 q 走过的路径:b + c + a(如果相交)
两个指针走过相同的总路径长度,因此会在相交点相遇。
三、代码逐行解析
方法一:双指针法(最优解)
Python 解法
python
# Definition for singly-linked list.
class ListNode:
def __init__(self, x):
self.val = x
self.next = None
class Solution:
def getIntersectionNode(self, headA: ListNode, headB: ListNode) -> ListNode:
# 初始化两个指针
p, q = headA, headB
# 当两个指针不相同时继续循环
while p is not q:
# 如果p到达链表末尾,重定向到headB
p = p.next if p else headB
# 如果q到达链表末尾,重定向到headA
q = q.next if q else headA
# 返回p(此时p和q相同,要么是相交节点,要么是None)
return p
Java 解法
java
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
// 初始化两个指针
ListNode p = headA, q = headB;
// 当两个指针不相同时继续循环
while (p != q) {
// 如果p到达链表末尾,重定向到headB
p = (p == null) ? headB : p.next;
// 如果q到达链表末尾,重定向到headA
q = (q == null) ? headA : q.next;
}
// 返回p(此时p和q相同)
return p;
}
}
方法二:计算长度法
Python 解法
python
class Solution:
def getIntersectionNode(self, headA: ListNode, headB: ListNode) -> ListNode:
# 步骤1:计算两个链表的长度
def getLength(head):
length = 0
while head:
length += 1
head = head.next
return length
lenA = getLength(headA)
lenB = getLength(headB)
# 步骤2:让长链表的指针先走差值步
p, q = headA, headB
if lenA > lenB:
for _ in range(lenA - lenB):
p = p.next
else:
for _ in range(lenB - lenA):
q = q.next
# 步骤3:同时前进,直到找到相交节点或到达末尾
while p and q and p != q:
p = p.next
q = q.next
return p
Java 解法
java
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
// 计算两个链表的长度
int lenA = getLength(headA);
int lenB = getLength(headB);
// 让长链表的指针先走差值步
ListNode p = headA, q = headB;
if (lenA > lenB) {
for (int i = 0; i < lenA - lenB; i++) {
p = p.next;
}
} else {
for (int i = 0; i < lenB - lenA; i++) {
q = q.next;
}
}
// 同时前进,直到找到相交节点
while (p != null && q != null && p != q) {
p = p.next;
q = q.next;
}
return p;
}
private int getLength(ListNode head) {
int length = 0;
while (head != null) {
length++;
head = head.next;
}
return length;
}
}
四、Java 与 Python 语法对比
1. 链表节点定义
| 操作 | Java | Python |
|---|---|---|
| 节点类 | 需要预定义 | 需要预定义 |
| 创建节点 | new ListNode(val) |
ListNode(val) |
2. 空值判断
| 操作 | Java | Python |
|---|---|---|
| 检查null | node == null |
node is None |
| 三元运算符 | p = (p == null) ? headB : p.next; |
p = p.next if p else headB |
3. 循环控制
| 操作 | Java | Python |
|---|---|---|
| while循环 | while (p != q) |
while p is not q: |
| 指针移动 | p = p.next; |
p = p.next |
五、实例演示
示例:两个链表相交
text
链表A: 4 → 1 → 8 → 4 → 5
链表B: 5 → 6 → 1 ↗
8 → 4 → 5
相交节点:值为8的节点
双指针法步骤:
-
初始化 :
p = 节点4(A头),q = 节点5(B头) -
第1轮:
-
p ≠ q,继续 -
p = p.next = 节点1 -
q = q.next = 节点6
-
-
第2轮:
-
p ≠ q,继续 -
p = p.next = 节点8 -
q = q.next = 节点1
-
-
第3轮:
-
p ≠ q,继续 -
p = p.next = 节点4 -
q = q.next = 节点8
-
-
第4轮:
-
p ≠ q,继续 -
p = p.next = 节点5 -
q = q.next = 节点4
-
-
第5轮:
-
p ≠ q,继续 -
p到达A末尾,重定向到headB = 节点5 -
q = q.next = 节点5
-
-
第6轮:
-
p ≠ q,继续 -
p = p.next = 节点6 -
q到达B末尾,重定向到headA = 节点4
-
-
第7轮:
-
p ≠ q,继续 -
p = p.next = 节点1 -
q = q.next = 节点1
-
-
第8轮:
-
p ≠ q,继续 -
p = p.next = 节点8 -
q = q.next = 节点8
-
-
第9轮:
p = q = 节点8,返回节点8
路径长度分析:
-
指针p路径:A(4,1,8,4,5) + B(5,6,1,8) = 9步
-
指针q路径:B(5,6,1,8,4,5) + A(4,1,8) = 9步
-
在节点8相遇
示例:两个链表不相交
text
链表A: 1 → 2 → 3
链表B: 4 → 5
不相交
双指针法步骤:
-
第1-3轮:分别遍历各自链表
-
第4-5轮:交换链表继续遍历
-
最终 :
p和q同时到达null,返回null
六、关键细节解析
1. 为什么双指针法能保证相遇?
设链表A长度为a+c,链表B长度为b+c(c为公共部分长度)
-
指针p路径:a + c + b(A链表 + 交换到B链表)
-
指针q路径:b + c + a(B链表 + 交换到A链表)
-
两个指针走过相同的总长度,因此会同时到达相交点或null
2. 如何处理不相交的情况?
当两个链表不相交时:
-
指针p路径:m + n(遍历A链表 + 遍历B链表)
-
指针q路径:n + m(遍历B链表 + 遍历A链表)
-
两个指针最终会同时到达null,循环结束
3. 时间复杂度为什么是O(m+n)?
-
每个指针最多遍历两个链表各一次
-
总遍历次数:m + n
-
时间复杂度:O(m+n)
4. 空间复杂度为什么是O(1)?
-
只使用了两个指针变量
-
没有使用额外的数据结构
-
空间复杂度:O(1)
5. 如果链表有环怎么办?
题目假设链表没有环。如果有环,双指针法会陷入无限循环,需要先判断是否有环。
七、复杂度分析
方法一:双指针法
-
时间复杂度:O(m+n)
- 每个指针最多遍历两个链表各一次
-
空间复杂度:O(1)
- 只使用了常数个指针变量
方法二:计算长度法
-
时间复杂度:O(m+n)
-
计算长度:O(m) + O(n)
-
对齐后遍历:O(min(m,n))
-
-
空间复杂度:O(1)
- 只使用了常数个指针变量
八、其他解法
解法一:哈希表法
python
class Solution:
def getIntersectionNode(self, headA: ListNode, headB: ListNode) -> ListNode:
# 使用集合存储链表A的所有节点
visited = set()
# 遍历链表A,将所有节点加入集合
p = headA
while p:
visited.add(p)
p = p.next
# 遍历链表B,检查节点是否在集合中
q = headB
while q:
if q in visited:
return q
q = q.next
return None
解法二:栈方法(从后向前比较)
python
class Solution:
def getIntersectionNode(self, headA: ListNode, headB: ListNode) -> ListNode:
# 使用两个栈存储节点
stackA, stackB = [], []
# 将链表A的所有节点压栈
p = headA
while p:
stackA.append(p)
p = p.next
# 将链表B的所有节点压栈
q = headB
while q:
stackB.append(q)
q = q.next
# 从栈顶开始比较(即从链表尾部开始比较)
last_common = None
while stackA and stackB:
nodeA = stackA.pop()
nodeB = stackB.pop()
if nodeA != nodeB:
break
last_common = nodeA
return last_common
解法三:连接成环法(修改链表结构,不推荐)
python
class Solution:
def getIntersectionNode(self, headA: ListNode, headB: ListNode) -> ListNode:
if not headA or not headB:
return None
# 找到链表A的尾节点
tailA = headA
while tailA.next:
tailA = tailA.next
# 将链表A的尾节点连接到链表B的头节点,形成环
tailA.next = headB
# 使用快慢指针检测环的入口(即相交节点)
slow, fast = headA, headA
while fast and fast.next:
slow = slow.next
fast = fast.next.next
if slow == fast:
# 找到环,寻找环的入口
entry = headA
while entry != slow:
entry = entry.next
slow = slow.next
# 恢复链表结构
tailA.next = None
return entry
# 没有环,恢复链表结构
tailA.next = None
return None
九、常见问题与解答
Q1: 如果两个链表长度相差很大怎么办?
A1: 双指针法能很好地处理这种情况。无论长度差多大,两个指针最终都会走过相同的总路径长度。
Q2: 为什么不能用值比较来判断相交节点?
A2: 因为链表中可能存在重复的值,但节点是唯一的(通过内存地址区分)。相交节点必须是同一个节点对象,而不只是值相同。
Q3: 如果链表有环怎么办?
A3: 题目假设链表没有环。如果可能有环,需要先判断是否有环,再使用相应的方法。
Q4: 双指针法是否会陷入无限循环?
A4: 不会。因为当两个链表不相交时,两个指针最终会同时到达null,循环结束。
Q5: 如何测试代码的正确性?
A5: 可以测试以下情况:
-
两个链表相交于中间节点
-
两个链表相交于头节点
-
两个链表不相交
-
一个链表为空
-
两个链表都为空
十、相关题目
1. LeetCode 141. 环形链表
python
class Solution:
def hasCycle(self, head: ListNode) -> bool:
# 快慢指针法
if not head or not head.next:
return False
slow, fast = head, head.next
while slow != fast:
if not fast or not fast.next:
return False
slow = slow.next
fast = fast.next.next
return True
2. LeetCode 142. 环形链表 II
python
class Solution:
def detectCycle(self, head: ListNode) -> ListNode:
# 快慢指针找到相遇点
slow, fast = head, head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
if slow == fast:
# 找到环,寻找环的入口
entry = head
while entry != slow:
entry = entry.next
slow = slow.next
return entry
return None
3. LeetCode 206. 反转链表
python
class Solution:
def reverseList(self, head: ListNode) -> ListNode:
prev, curr = None, head
while curr:
next_node = curr.next
curr.next = prev
prev = curr
curr = next_node
return prev
十一、总结
核心要点
-
双指针法:利用路径长度相等的原理,让两个指针走过相同的总路径
-
交换遍历:当一个指针到达链表末尾时,重定向到另一个链表的头节点
-
时间复杂度优化:O(m+n)的时间复杂度和O(1)的空间复杂度
算法步骤(双指针法)
-
初始化两个指针p和q,分别指向headA和headB
-
当p不等于q时循环:
-
如果p不为空,p移动到下一个节点;否则,p重定向到headB
-
如果q不为空,q移动到下一个节点;否则,q重定向到headA
-
-
返回p(此时p和q相同,要么是相交节点,要么是None)
时间复杂度与空间复杂度
-
时间复杂度:O(m+n),最多遍历两个链表各一次
-
空间复杂度:O(1),只使用常数个指针变量
适用场景
-
需要找到两个链表的相交节点
-
空间复杂度要求严格
-
链表不允许修改
扩展思考
双指针法是解决链表问题的常用技巧,这种思想可以应用于:
-
环形链表检测
-
链表中点查找
-
链表倒数第k个节点
-
两个链表的合并与比较
掌握相交链表问题的解法不仅能够解决这个特定问题,还能加深对链表操作和双指针技巧的理解,是面试中常见的经典题目。