目录
两个链表第一个公共子结点
方法一 哈希/集合
方法二 栈
方法三 序列拼接方法
方法四 双指针
首先要明确一点,由于单链表结点只能指向一个结点,故在找到两个链表的第一个公共子节点后,其后面的结点也是相同的,这点很重要!也成为后面解题的关键!
我们可以先把其中一个链表放到一个Map或者集合里,然后遍历另一个链表,一边遍历,一边看Map或者集合里是否存在当前结点,如果发现存在,那么即为我们所找的第一个公共子节点。如下分别为哈希和集合的实现方式,其思路都是一样的。
// HashMap方法
public static ListNode findFirstCommonNodeByMap(ListNode pHead1, ListNode pHead2) {
if (pHead1 == null || pHead2 == null) {
return null;
}
ListNode current1 = pHead1;
ListNode current2 = pHead2;
HashMap hashMap = new HashMap();
// 将第一个链表存入HashMap中
while (current1 != null) {
hashMap.put(current1, null);
current1 = current1.next;
}
// 遍历第二个链表,看是否在HashMap中存在
while (current2 != null) {
if (hashMap.containsKey(current2))
return current2;
current2 = current2.next;
}
return null;
}
// set 集合
public static ListNode findFirstCommonNodeBySet(ListNode pHead1, ListNode pHead2) {
Set set = new HashSet<>();
while (pHead1 != null) {
set.add(phead1);
pHead1 = pHead1.next;
}
while (pHead2 != null) {
if (set.contains(pHead2))
return pHead2;
pHead2 = pHead2.next;
}
return null;
}
我们可以构造两个栈,将两个链表分别入栈,然后分别出栈。我们前面说了,在两个单链表第一个公共结点之后的结点一定是相同的,又先出栈的是链表尾,所以一开始两栈所出的结点一定是一样的,直到最晚出栈且相等的那一组就是我们所找的第一个公共子结点。
public static ListNode findFirstCommonNodeByStack(ListNode headA, ListNode headB) {
// 构造两个栈
Stack stackA = new Stack();
Stack stackB = new Stack();
// 将两个链表分别入栈
while (headA != null) {
stackA.push(headA);
headA = headA.next;
}
while (headB != null) {
stackB.push(headB);
headB = headB.next;
}
ListNode preNode = null;
while (stackB.size() > 0 && stackA.size() > 0) {
int a = stackA.peek().val;
int b = stackB.peek().val;
// 如果栈顶的结点相同,那么则一起出栈
if (stackA.peek() == stackB.peek()) {
preNode = stackA.pop();
stackB.pop();
} else {
break;
}
}
return preNode;
}
由于两个链表长度不能保证相等,而前面我们知道在第一个公共结点之后的结点都是相同的,所以我们可以想办法让链表长度相等,那这应该怎么做呢?那这时不难想到,如果链表一拼接链表二,链表二拼接链表一,那他们长度不就想等了吗!而且,两个拼接后的链表的尾部恰好就是两原链表的尾部,那这不和我们前面说的方法对应上了吗,因为在第一个公共结点之后的结点都是相同的,所以尾部的结点都是相同的,离尾部最远一组相同的结点或者说是我们从头遍历最早得到的一组相同结点,即为我们所找的第一个公共结点。
看看下面这个例子,就可以很好理解了。链表A和B如下:
A: 1 - 2 - 3 - 4 - 5 B: a - b - 4 - 5
AB: 1 - 2 - 3 - 4 - 5 - a - b - 4 - 5
BA: a - b - 4 - 5 - 1 - 2 - 3 - 4 - 5
那么4就是我们所找的第一个公共子结点
具体实现代码如下:
public static ListNode findFirstCommonNodeByCombine(ListNode pHead1, ListNode pHead2) {
if (pHead1 == null || pHead2 == null) {
return null;
}
ListNode p1 = pHead1;
ListNode p2 = pHead2;
while (p1 != p2) {
int a = p1.val;
int b = p2.val;
p1 = p1.next;
p2 = p2.next;
// if (p1 != p2) 这个判断是为了跳出死循环,后面详细说到
if (p1 != p2) {
if (p1 == null) {
p1 = pHead2;
}
if (p2 == null) {
p2 = pHead1;
}
}
}
return p1;
}
! 注: 要什么循环体中要加一个判断 if ( p1 != p2)? 其实愿因很简单,因为如果不加这个判断,那如果两个链表不存在公共结点时,每当链表遍历到表尾便会接着遍历另一个列表,但由于没有公共结点,所以根本停不下来。但加上这个判断之后,如果两个链表遍历完自己一次和对方一次后没有找到公共结点,那这时他们会同时成为空结点,这时候便满足p1 == p2, 便会跳出循环了。
按简单的方法那必然是选择集合,按巧妙的方法那序列拼接绝对称得上一个“巧”,但是如果想要非常直观的方法,那用双指针是最好的选择了。我们知道两个链表长度不一定相等,但是在第一个公共子结点之后的长度是相等的,那么我们可不可以让两个链表长度相等呢,因为长度相等后,我们就可以将两者的结点对应上了,共同遍历知道找到第一个公共子节点,这便是这个方法的思路。假设第一轮遍历后得到链表La长度尾L1,Lb长度尾L2,则 |L2-L1| 就是两链表长度的差值。第二轮遍历,长的先走 |L2-L1| ,此时两链表便对应上了,同时向前走,结点一样时就是公共结点了。具体代码实现如下:
public static ListNode findFirstCommonNodeBySub(ListNode pHead1, ListNode pHead2) {
if (pHead1 == null || pHead2 == null) {
return null;
}
ListNode current1 = pHead1;
ListNode current2 = pHead2;
int l1 = 0, l2 = 0;
// 第一轮遍历,得到两链表长度从而得到插值
while (current1 != null) {
current1 = current1.next;
l1++;
}
while (current2 != null) {
current2 = current2.next;
l2++;
}
current1 = pHead1;
current2 = pHead2;
// 长的链表先走差值步数,然后一起走
int sub = l1 > l2 ? l1 - l2 : l2 - l1;
if (l1 > l2) {
int a = 0;
while (a < sub) {
current1 = current1.next;
a++;
}
}
if (l1 < l2) {
int a = 0;
while (a < sub) {
current2 = current2.next;
a++;
}
}
// 当结点相同时得到答案
while (current2 != current1) {
current2 = current2.next;
current1 = current1.next;
}
return current1;
}