该题目为 剑指 Offer 52.
输入两个链表,找出它们的第一个公共节点。
如下面的两个链表:
在节点 c1 开始相交。
示例 1:
输入:intersectVal = 8, listA = [4,1,8,4,5], listB = [5,0,1,8,4,5], skipA = 2, skipB = 3
输出:Reference of the node with value = 8
输入解释:相交节点的值为 8 (注意,如果两个列表相交则不能为 0)。从各自的表头开始算起,链表 A 为 [4,1,8,4,5],链表 B 为 [5,0,1,8,4,5]。在 A 中,相交节点前有 2 个节点;在 B 中,相交节点前有 3 个节点。
本文提供4种不同方法进行问题分析
方法一(使用Hash进行查找):
先将其中一个链表List1进行遍历,保存到Map中,然后再从头到尾遍历另一个链表List2的元素,逐个判断其元素是否存在于Map中,若对应元素存在于Map中,则该元素为第一个公共子节点。
(另外,由于可以使用Hash进行解题,相同的,集合中的HashSet也可以实现,具体步骤与Hash相似)
代码实现如下:
//通过Hash辅助查找
public static ListNode findFirstCommonNodeByMap(ListNode pHead1, ListNode pHead2) {
HashMap<ListNode, Integer> listNodeHashMap = new HashMap<>();
if (pHead1 == null || pHead2 == null){
return null;
}
ListNode current1 = pHead1;
while (current1 != null){
listNodeHashMap.put(current1,null);
current1 = current1.next;
}
ListNode current2 = pHead2;
while (current2 != null){
if (listNodeHashMap.containsKey(current2)){
return current2;
}
current2 = current2.next;
}
return null;
}
//方法2:通过集合辅助查找
public static ListNode findFirstCommonNodeBySet(ListNode headA, ListNode headB) {
Set<ListNode> set = new HashSet<>();
ListNode current1 = headA;
ListNode current2 = headB;
while (current1 != null){
set.add(current1);
current1 = current1.next;
}
while (current2 != null){
if (set.contains(current2)){
return current2;
}
current2 = current2.next;
}
return null;
}
方法二(通过栈辅助查找):
可以将两个链表的元素全分别压入两个不同的栈中,再同时出栈,如果其对应出栈的元素相同,就继续出栈,一直找到最晚出栈的那一组(即对应的下一组的栈顶部元素不相同的一组)
代码实现如下:
public static ListNode findFirstCommonNodeByStack(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 preNode = null;
while (stack1.size() != 0 && stack2.size() != 0){
if (stack1.peek() == stack2.peek()){
preNode = stack1.pop();
stack2.pop();
} else {
break;
}
}
return preNode;
}
但是时间复杂度为两个O(n),面试不占优。
方法三(通过序列拼接):
先看看下面的链表A和B:
A:0-1-2-3-4-5
B:a-b-4-5
如果分别拼接AB和BA会怎么样?
AB:0-1-2-3-4-5-a-b-4-5
BA:a-b-4-5-0-1-2-3-4-5
我们可以看到,拼接之后,从最后的4开始,两个链表就是一样的了,因此4就是要找的节点,所以我们可以通过拼接的方式来找交点。这样做的原理是什么呢?
我们先假定A和B有相交的地方,以交点为中心,将分别将两个链表分为两个部分,即left_a和right_a,left_b和right_b,并且right_a和right_b部分全部一样(即为交点+之后的链表部分)
这时候我们拼接AB和BA就是这样的结构:
由于right_a和right_b一样,所以这时候只要分别遍历AB和BA直到到相同的元素的位置就是要找的交点了。
另外,这里还可以进行进一步的优化,建立新链表太浪费空间了,我们只要在每个链表访问完后将链表指针指向另一个链表继续进行遍历即可。
public static ListNode findFirstCommonNodeByCombine(ListNode pHead1, ListNode pHead2) {
if (pHead1 == null || pHead2 == null){
return null;
}
ListNode p1 = pHead1;
ListNode p2 = pHead2;
while (p1 != p2){
p1 = p1.next;
p2 = p2.next;
if (p1 == null){
p1 = pHead2;
} else if (p2 == null){
p2 = pHead1;
}
}
return p1;
}
方法四:通过差值来实现
先遍历两个链表求分别求出它们对应的长度l1,l2,然后让较长的链表先遍历|l1-l2|长度,然后再让两个链表同时遍历,并且比较它们对应元素的值,若对应元素相同,则该元素为交点。
代码实现:
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){
l1++;
current1 = current1.next;
}
while (current2 != null){
l2++;
current2 = current2.next;
}
current1 = pHead1;
current2 = pHead2;
while (l1 != l2){
if (l1 > l2){
l1--;
current1 = current1.next;
}
if (l2 > l1){
l2--;1
current2 = current2.next;
}
}
while (current1 != current2){
current1 = current1.next;
current2 = current2.next;
}
return current1;
}
总结:
方法一、二更多的是使用常用的数据结构及其所提供的的方法,比较容易想到,也应该是大多数人会使用的方法。
然后方法三、四更多在于思维层面的思考,只要逻辑正确代码还是比较容易实现的。