这是一道经典的链表问题,剑指offer52 先看一下题目:输入两个链表,找出它们的第一个公共节点。例如下面的两个链表:
方法一:哈希和集合
先将一个链表元素全部存到Map里,然后一边遍历第二个链表,一边检测Hash中是否存在当前结点,如果有交点,那么一定能检测出来。
代码如下:
/**
* 使用集合查找两个链表的第一个公共节点。
* @param headA 第一个链表的头节点。
* @param headB 第二个链表的头节点
* @return 第一个公共节点,如果不存在公共节点则返回null。
*/
public ListNode findFirstCommonNodeBySet(ListNode headA, ListNode headB) {
// 创建一个集合来存储链表A的节点
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; // 未找到公共节点
}
第二种方式:使用栈
当给定以下两个链表作为输入时:
链表A: 1 -> 2 -> 3 -> 4 -> 5 -> 6
链表B: 9 -> 8 -> 5 -> 6
在这个例子中,链表A和链表B在节点5和节点6处相交。
现在我们将遍历这两个链表,并执行给定的代码。
首先,我们将链表A的节点逐个入栈 stackA 中,栈顶元素位于链表的末尾:
stackA: 6 -> 5 -> 4 -> 3 -> 2 -> 1
然后,我们将链表B的节点逐个入栈 stackB 中,栈顶元素位于链表的末尾:
stackB: 6 -> 5 -> 8 -> 9
接下来,我们开始循环遍历。在第一次迭代中,我们比较 stackA 的栈顶元素6和 stackB 的栈顶元素6,它们相等。我们从 stackA 中弹出栈顶元素6,并将其赋值给 preNode,表示我们找到了第一个公共节点。
stackA: 5 -> 4 -> 3 -> 2 -> 1
stackB: 5 -> 8 -> 9
preNode: 6
然后,我们从 stackB 中弹出栈顶元素6,因为链表的公共节点必须在这之前的位置。
stackA: 5 -> 4 -> 3 -> 2 -> 1
stackB: 8 -> 9
preNode: 6
在第二次迭代中,我们比较 stackA 的栈顶元素5和 stackB 的栈顶元素8,它们不相等。于是我们跳出循环,表示找到了链表的分叉点。
因此,我们最终得到的 preNode 是节点6,它是链表A和链表B的第一个公共节点。
import java.util.Stack;
/**
* 使用栈查找两个链表的第一个公共节点。
* @param headA 第一个链表的头节点。
* @param headB 第二个链表的头节点
* @return 第一个公共节点,如果不存在公共节点则返回null。
*/
public ListNode findFirstCommonNodeByStack(ListNode headA, ListNode headB) {
// 创建两个栈,分别用于存储链表A和链表B的节点
Stack<ListNode> stackA = new Stack<>();
Stack<ListNode> stackB = new Stack<>();
// 遍历链表A,将每个节点压入栈中
while (headA != null) {
stackA.push(headA);
headA = headA.next;
}
// 遍历链表B,将每个节点压入栈中
while (headB != null) {
stackB.push(headB);
headB = headB.next;
}
// 从栈顶开始比较两个栈的节点,找到第一个不相等的节点即为分叉点,其前一个节点即为第一个公共节点
ListNode preNode = null;
while (!stackA.isEmpty() && !stackB.isEmpty()) {
if (stackA.peek() == stackB.peek()) {
preNode = stackA.pop();
stackB.pop();
} else {
break;
}
}
return preNode;
}
第三种方法:拼接两个字符串:
先看下面的链表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 ListNode findFirstCommonNode(ListNode pHead1, ListNode pHead2) {
// 检查输入的参数是否为空
if (pHead1 == null || pHead2 == null) {
return null;
}
// 创建两个指针,分别指向链表 pHead1 和 pHead2 的头节点
ListNode p1 = pHead1;
ListNode p2 = pHead2;
// 循环遍历链表,直到找到公共节点或到达链表末尾
while (p1 != p2) {
// 将指针向前移动到下一个节点
p1 = p1.next;
p2 = p2.next;
// 检查指针是否为空,并继续遍历另外一个链表
if (p1 != p2) {//这里起到的作用是排除p1和p2同时为空的情况,避免造成死循环
// 如果链表 p1 已经遍历完,则将其指向链表 pHead2 的头节点
if (p1 == null) {
p1 = pHead2;
}
// 如果链表 p2 已经遍历完,则将其指向链表 pHead1 的头节点
if (p2 == null) {
p2 = pHead1;
}
}
}
// 返回公共节点或 null(如果没有公共节点)
return p1;
}
第四种方法:差和双指针
我们再看另一个使用差和双指针来解决问题的方法。假如公共子节点一定存在第一轮遍历,假设La长度为L1,Lb长度为L2.则|L2-L1|就是两个的差值。第二轮遍历,长的先走|L2-L1|,然后两个链表同时向前走,结点一样的时候就是公共结点了。
public ListNode findFirstCommonNode(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++;
}
//将current的值变回头节点
current1=pHead1;
current2=pHead2;
int sub=l1>l2?l1-l2:l2-l1;
//长的先走sub步
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;
}