面试题链表相交
给你两个单链表的头节点 headA
和 headB
,请你找出并返回两个单链表相交的起始节点。如果两个链表没有交点,返回 null
。
图示两个链表在节点 c1
开始相交:
题目数据 保证 整个链式结构中不存在环。
注意,函数返回结果后,链表必须 保持其原始结构 。
示例 1:
输入:intersectVal = 8, listA = [4,1,8,4,5], listB = [5,0,1,8,4,5], skipA = 2, skipB = 3 输出:Intersected at '8' 解释:相交节点的值为 8 (注意,如果两个链表相交则不能为 0)。 从各自的表头开始算起,链表 A 为 [4,1,8,4,5],链表 B 为 [5,0,1,8,4,5]。 在 A 中,相交节点前有 2 个节点;在 B 中,相交节点前有 3 个节点。
示例 2:
输入:intersectVal = 2, listA = [0,9,1,2,4], listB = [3,2,4], skipA = 3, skipB = 1 输出:Intersected at '2' 解释:相交节点的值为 2 (注意,如果两个链表相交则不能为 0)。 从各自的表头开始算起,链表 A 为 [0,9,1,2,4],链表 B 为 [3,2,4]。 在 A 中,相交节点前有 3 个节点;在 B 中,相交节点前有 1 个节点。
示例 3:
输入:intersectVal = 0, listA = [2,6,4], listB = [1,5], skipA = 3, skipB = 2 输出:null 解释:从各自的表头开始算起,链表 A 为 [2,6,4],链表 B 为 [1,5]。 由于这两个链表不相交,所以 intersectVal 必须为 0,而 skipA 和 skipB 可以是任意值。 这两个链表不相交,因此返回 null 。
这个题和上面一个删除倒数第n个结点有点类似,采用双指针法。上一个删除倒数第n个结点是利用快指针先走,慢指针后走卡出了倒数第n个节点,本题一共有“两个链表”,但是如果同时遍历,可能从头到尾也找不到连在一起的结点,所以让长链表的cur指向一个结点,使这个节点后面的节点数与短链表的节点数都相等,最后再遍历,这样可以实现匹配度。
具体实现方法是计算两个链表的长度,做差值,让长链表的cur移动到差值处,之后长短链表一起遍历,这样能够确保遍历的时候能够配准,如果到最后也没有配准,说明两个链表没有交点。时间复杂度主要在于寻找链表长度上,为O(m+n),代码入下:
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
int lenA=0,lenB=0;
ListNode*curA=headA;
ListNode*curB=headB;
while(curA!=nullptr)
{
curA=curA->next;
lenA++;
}
while(curB!=nullptr)
{
curB=curB->next;
lenB++;
}
curA=headA;
curB=headB;
int public_len=lenA>lenB?(lenA-lenB):(lenB-lenA);
if(lenA>lenB)
{
while(public_len)
{
curA=curA->next;
public_len--;
}
}
else if(lenA<lenB)
{
while(public_len)
{
curB=curB->next;
public_len--;
}
}
while(curA!=nullptr&&curB!=nullptr)
{
if(curA==curB) return curA;
curA=curA->next;
curB=curB->next;
}
return nullptr;
}
};
本题注意的一点就是要让两个链表对准,和上一个寻找倒n结点有异曲同工之妙,都是利用双指针法,不过这道题是快指针先移动完,最后和慢指针一起移动。
142 环形链表
给定一个链表的头节点 head
,返回链表开始入环的第一个节点。 如果链表无环,则返回 null
。
如果链表中有某个节点,可以通过连续跟踪 next
指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos
来表示链表尾连接到链表中的位置(索引从 0 开始)。如果 pos
是 -1
,则在该链表中没有环。注意:pos
不作为参数进行传递,仅仅是为了标识链表的实际情况。
不允许修改 链表。
示例 1:
输入:head = [3,2,0,-4], pos = 1 输出:返回索引为 1 的链表节点 解释:链表中有一个环,其尾部连接到第二个节点。
示例 2:
输入:head = [1,2], pos = 0 输出:返回索引为 0 的链表节点 解释:链表中有一个环,其尾部连接到第一个节点。
示例 3:
输入:head = [1], pos = -1 输出:返回 null 解释:链表中没有环。
这个题到手以后,可以说是一点思路都没有了,能想到可能需要两个指针,但是怎么转圈才能转到一起呢,没有想到可以给指针设置速度。但是重合条件也是有点懵逼的······
本题可以拆解成两个:1.判断是否有环 2.环的入口在哪里
分别定义fast和slow指针,fast每次移动两个节点,slow每次移动一个节点,如果两个指针在中途相遇,那就一定能够证明有环(否则一个速度慢的怎么可能追上一点速度快的呢),并且二者一定在环中相遇。
现在可以知道这个链表里面是否有环了,接下来要找到这个环的入口,如下图所示设置xyz。
那么相遇时,slow走过的距离为x+y,fast走过的距离是x+y+n(y+z),n为fast指针在环内走了n圈才遇到slow指针,y+z为一圈内的结点个数。
这里有个疑问,既然fast里面带n,为什么slow里面不是带有若干环的长度呢(也转好几圈)。fast的速度是slow的二倍,当slow要是想走完一圈,fast一定已经走完两圈了,两圈之内怎么可能追不上slow呢,此时slow还在一圈以内,有人说玩意fast把slow给错开了呢,但是fast相对slow的速度是1,所以不可能跳过去的。
因为fast一次走两个节点,slow走一个,所以fast走过的结点数永远等于slow的二倍,于是有2(x+y)= x+y+n(y+z);要找到的是环形的入口,所以需要提出x,整理一下变为x=n(y+z)-y,最好不要让里面出现负数,所以变为x = (n - 1) (y + z) + z ;注意n一定大于等于1,因为fast指针至少多走一圈才能与slow指针相遇。第一次相遇时,只需令n等于1,公式就变为了x=z。这就意味着,从头结点出发一个指针,从相遇节点也出发一个指针,这两个指针每次只走一个节点,那么当这两个指针相遇的时候的结点就是环形入口的结点,所以在相遇处定义一个index1,在头结点处定义一个index2,动画如下:
寻找相遇结点的过程是经过数学推理出来的,也就是说要保持fast和slow分别为2和1的速度才可以这样,其他的情况肯定不能这么应用的。时间复杂度为n,代码如下:
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode *detectCycle(ListNode *head) {
ListNode*fast=head, *slow=head;
while(fast!=nullptr&&fast->next!=nullptr)
{
fast = fast->next->next;
slow = slow -> next;
if(fast==slow)
{
ListNode*index1=fast;
ListNode*index2=head;
while(index1!=index2)
{
index1=index1->next;
index2=index2->next;
}
return index1;
}
}
return nullptr;
}
};
链表总结篇
链表的头结点是比较难处理的,通常要设置一个虚拟头结点,这样就不用分类,保证了代码的连贯性。
同时,单链表和双链表的增删改查也要牢记于心,建议也要使用虚拟头结点。
反转链表时候,主要有迭代法,递归法,头插法,重点就是一小块一小块的进行改造,要么改变顺序,要么拆网的时候织网,需要注意的一点就是随时设置tmp,以防这个节点没法被找到。
删除链表倒数第n个结点和链表相交,采用的都是双指针法,注意判断双指针的停止条件即可。
最后的环形链表,比较难的部分就是数学证明,同时也要有一些思路,不能看到难题直接发呆。
总之,链表这一章的算法其实并不难,但是要注意不要出现bug,要熟悉各种操作方法,特别抽象的题要想想数学证明(第一次遇到),其他的只要想想逻辑就好。