相交链表
相交链表问题也就是两个链表的第一个公共子节点问题。
LeetCode题目链接 160. 相交链表
👹 题目描述
👹 题目分析
- 首先明确要求:我们已知两个链表的头节点,求两个链表的相交节点的第一个公共节点。
- 分析题目:单链表的每一个节点都只能指向唯一的下一个节点next(也就是只能拥有唯一的后继),但是可以有多个指针指向同一个节点。例如上面的a2,b3同时指向c1.
- 明确了题目要求和前提之后,我们就可以开始解题了~
👹 解题方法
很多时候我们了解链表的基本概念和题目的意思,但面对题目还是大眼瞪小眼,一脸懵啊😶~~
一脸无辜.jpg
那么我们如何解决这个问题呢?
一个屡试不爽的方法:套!!!
套什么?
当然是把常用的数据结构和算法思想都套用一遍;
~常用的数据结构,数组、队列、栈、Hash、树、堆、集合等;
常用算法思想,查找、排序、双指针、递归、迭代、分治、贪心、dp、回溯等。
- 首先我们脑中第一想到的肯定是暴力解决法,将第一个链表中的每一个结点一次与第二个链表的进行比较,当出现相等的结点指针时,即为相交节点。虽然很简单,但是时间复杂度非常高,排除此方法!
- 要找到相交的链表,我们可以进行比较。我们可以通过栈(先进后出)保存元素,然后进行比较。我们将两个链表分别压入到两个栈里面,如果栈顶元素一致则出栈,然后继续找,最晚出栈的那组一致的节点就是要找的位置。ok,第二种方法出来。
- 通过Hash,先将一个链表的元素全部添加到Map里面,然后遍历第二个链表并检查当前元素是否在Hash中,如果找到了,则存在交点。okay,第三种方法找到了。同理,既然Hash可以,那集合呢?可想而知同Hash一样,也能解决,ok,第四种方法我们也找到了。
- 同样双指针也适合,这个后续介绍~
👹 具体解决方案+代码(java)
🤖 法一(使用栈)
这里我们需要使用两个栈,分别将两个链表的节点压入栈,然后比较栈顶元素,相同则出栈(相同说明是公共元素),一直找到最后出栈的那一组。
🤡解释:最后出栈的那一组,即最后一组相同的元素,因为栈是一种先入后出的思想,所以最后一组相同的元素,就是相交链表的第一个节点。
这种方法,我们需要创建两个O(n)空间大小的栈
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
// 创建两个栈
Stack<ListNode> stack1 = new Stack<>();
Stack<ListNode> stack2 = new Stack<>();
// 将两个链表的元素分别压入栈
while (headA != null) {
stack1.push(headA);
headA = headA.next;
}
while (headB != null) {
stack2.push(headB);
headB = headB.next;
}
// 定义一个变量,用来保存相交的元素
ListNode node = null;
// 当两个栈都不为空的时候才进行比较判断栈顶元素
// 解释:当一个栈为空一个栈不为空的时候,肯定不存在相交元素
while (!stack1.empty() && !stack2.empty()) {
// 判断栈顶元素是否一致,一致则出栈
if (stack1.peek() == stack2.peek()) {
node = stack1.pop();
stack2.pop();
} else {
// 不同则退出循环
break;
}
}
// 返回一致元素的最后一组,即相交链表的第一个公共节点
return node;
}
🤖 法二(哈希和集合)
先将一个链表元素全部存到集合中,然后遍历第二个链表,并判断集合中是否存在当前节点,如果存在,即相交节点。使用Map也可以,不过本题更适合用集合。
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
// set
Set<ListNode> set = new HashSet<>();
// 将链表A的元素全部添加到集合中
while (headA != null) {
set.add(headA);
headA = headA.next;
}
// 遍历链表B
while (headB != null) {
// 判断集合中是否包含当前节点
if (set.contains(headB)) {
// 包含则直接返回
return headB;
}
headB = headB.next;
}
return null;
}
🤖 法三(差和双指针)
首先计算两链表的长度的差值k,让长的链表先走k步,然后再与短的链表一起向后走,直到找到相同节点或者结束为止。
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
// 先分别计算两链表的长度
int size1 = size(headA);
int size2 = size(headB);
// 定义两个指针,分别指向A、B链表的头节点
ListNode cur1 = headA;
ListNode cur2 = headB;
// 计算差值
// 保证差值为正数,并且保证 cur1 始终指向长一些的链表,cur2指向短一些的链表
int count = size1 - size2;
if (size1 < size2) {
// size1 < size2 说明链表A短
count = size2 - size1;
cur1 = headB;
cur2 = headA;
}
// 让长一点的链表先走count步
while (count > 0 ) {
cur1 = cur1.next;
count--;
}
// 此时cur1与cur2 到末尾节点长度相等
// cur1 = cur2 退出循环
while (cur1 != cur2) {
cur1 = cur1.next;
cur2 = cur2.next;
}
return cur1; // cur1即相交节点
}
public int size(ListNode head) {
ListNode cur = head;
int count = 0;
while (cur != null) {
cur = cur.next;
count++;
}
return count;
}
🤖 法四(双指针)拼接
listA : [4, 1, 8, 4, 5]
listB :[5, 6, 1, 8, 4, 5]
将listA与listB拼接起来:
AB [4, 1, 8, 4, 5, 5, 6, 1, 8, 4, 5]
BA [5, 6, 1, 8, 4, 5, 4, 1, 8, 4, 5]
会发现,从8开始后面的节点就一样了,显然8就是我们要找的节点~
注意:
比较的是两个节点是否相同,不是值是否相等。
也就是说1
节点,两链表的值都是1,但是不是同一个节点
思路:
- 定义两个指针分别指向链表的头部
p -> headA
,q -> headB
- 当
p
走到链表尾部后,回到headB
;q
走到链表尾部后回到headA
- 继续向后走,直到p、q相遇(相交)
- 当两链表没有交点时,p、q会同时等于空
代码:
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
// 定义两个指针分别指向链表的头部
// p 指向链表A,当p走到尾部后,回到headB
// q 指向链表B,当q走到尾部后,回到headA
// 如果p、q相等且不为空即 相交
ListNode p = headA;
ListNode q = headB;
while (p != q) {
p = (p != null) ? p.next : headB;
q = (q != null) ? q.next : headA;
}
return p;
}
复杂度分析:
- 时间复杂度 O(a+b) : 最差情况下(即 ∣a−b∣=1 , c=0 ),此时需遍历 a+b 个节点。
- 空间复杂度 O(1) : 节点指针
A
,B
使用常数大小的额外空间。