本文主要介绍了有关链表的各种经典面试题目(相交链表、环形链表、环形链表II、随机链表的复制),内容全由作者原创(无AI),同时深度解析了题目的经典解决方法,并带有配图帮助博友们更好的理解,点个关注不迷路,下面进入正文~~
目录
1.相交链表
题目链接:相交链表
**方法1:**遍历链表A的所有节点,并且将A链表的所有节点都与B链表的所有节点进行比较,如果有节点相同,说明他们是相交的;如果没有节点相同,说明这两个链表不相交。
要求:上面的方法的时间复杂度是O(N^2),有没有更快的方法呢?
方法2:(比较推荐)
1.判断是否相交。如果这两个链表相交了,说明他们最后一个节点一定是相同的。我们可以分别遍历两个链表,找出这两个链表的最后一个节点并进行比较。如果不相同,直接返回NULL;如果2相同再找出他们相交的节点。
2.若相交,找出第一个节点。先分别算出这两个链表的长度,再求出这两个链表长度的差值gap。然后找出他们两其中较长的链表,并重新遍历这两个链表。长链表先走gap步,然后两个链表同时开始遍历。当找到两个相同的节点时,返回该节点的地址
我们更推荐使用方法2 ,因为他的时间复杂度是O(N),要优于方法1
下面是用方法2解决这道题的完整代码
cs
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB)
{
struct ListNode* curA = headA;
struct ListNode* curB = headB;
int countA = 1;
int countB = 1;
while(curA->next)
{
curA = curA->next;
countA++;
}
while(curB->next)
{
curB = curB->next;
countB++;
}
if(curA!=curB)
{
return NULL;
}
struct ListNode* fast = headA;
struct ListNode* slow = headB;
int gap = abs(countA - countB);
if(countA < countB)
{
fast = headB;
slow = headA;
}
while(gap--)
{
fast = fast->next;
}
while(fast!=slow)
{
fast = fast->next;
slow = slow->next;
}
return fast;
}
2.环形链表
题目链接:环形链表
解题思路:快慢指针 !
慢指针一次走一步,快指针一次走两步。当slow进环的时候,fast开始追击slow。fast追上slow就有环,如果fast走到NULL就说明没有环。

为什么一定会相遇?有没有可能永远错过?请证明

假设slow进环时,fast和slow的距离是N。每当fast走两步slow走一步时,fast和slow的距离都会缩小1,直到fast和slow的距离为零。因此fast和slow一定会相遇
下面是这道题目的完整代码:
cs
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
bool hasCycle(struct ListNode *head)
{
struct ListNode* fast = head;
struct ListNode* slow = head;
while(fast&&fast->next)
{
slow=slow->next;
fast=fast->next->next;
if(slow==fast)
{
return true;
}
}
return false;
}
拓展:当slow走1步,fast走3步、4步、5步、n步时,fast和slow还一定会相遇吗?
我们先探究当fast走三步时的情况
同样的,我们假设slow进环时,fast和slow的距离是N。每当fast走三步slow走一步时,fast和slow的距离都会缩小2。这里就要分两种情况:
当N为偶数时,每走一次fast和slow的距离都会缩小2,直到N为0。因此,当N为偶数时,fast和slow一定会相遇。
当N为奇数时,每走一次fast和slow的距离都会缩小2,直到N为-1。N为-1的意思也就是fast超前了slow一步,fast和slow并没有相遇。
我们现在假设圆环的长度是C,那么现在fast到slow的距离就是C-1。
当C-1为偶数时,每走一次fast和slow的距离都会缩小2,直到N为0。因此,当C-1为偶数时,fast和slow一定会相遇。
当C-1奇数时,每走一次fast和slow的距离都会缩小2,直到N为-1。此时就陷入了死循环了,fast永远也不会和slow相遇。
总结一下:
1.N是偶数,第一轮就追上了
2.N是奇数,第一轮追击会错过,距离变成C-1
a.如果C-1为偶数,那么下一轮就追上了
b.如果C-1为奇数,那么就永远追不上
此时,我们发现fast追不上slow只有一种情况,就是N为偶数且C-1为奇数
可是,这种情况存在吗? 
同样的,我们假设slow进环时,fast和slow的距离是N,并且我们设slow刚进环时fast已经在环里面转了x圈。此时slow走的距离是L,fast走的距离是L+x*C+C-N。又因为fast走的距离是slow的三倍,那么这时我们就可以列一个等式
3L=L+x*C+C-N
2L=x*C+C-N
现在N为奇数,C为偶数
偶数=(x+1)*偶数 - 奇数
很明显,我们看到这个等式是矛盾的。说明N为偶数且C-1为奇数的情况不存在!
结论:一定能追上
N等于偶数时,第一轮就能追上
N等于奇数时,C-1一定为偶数,第二轮就能追上
那么当fast走4步、5步、n步呢?
这里的分析方法都是一模一样的,欢迎博友们在评论区展开讨论,这里就不详细探究了~
3.环形链表II
题目链接:环形链表II
方法1: 与上一题的思路一样,可以先用快慢指针先找到在环里相遇的一个节点,然后将这个节点与后一个节点断开,这样问题就转化成了相交链表 的问题
完整代码如下:
cs
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB)
{
struct ListNode* curA = headA;
struct ListNode* curB = headB;
int countA = 1;
int countB = 1;
while(curA->next)
{
curA = curA->next;
countA++;
}
while(curB->next)
{
curB = curB->next;
countB++;
}
if(curA!=curB)
{
return NULL;
}
struct ListNode* fast = headA;
struct ListNode* slow = headB;
int gap = abs(countA - countB);
if(countA < countB)
{
fast = headB;
slow = headA;
}
while(gap--)
{
fast = fast->next;
}
while(fast!=slow)
{
fast = fast->next;
slow = slow->next;
}
return fast;
}
struct ListNode *detectCycle(struct ListNode *head)
{
struct ListNode* fast = head;
struct ListNode* slow = head;
while(fast&&fast->next)
{
fast=fast->next->next;
slow=slow->next;
if(slow==fast)
{
struct ListNode* newnode = slow->next;
slow->next=NULL;
return getIntersectionNode(newnode,head);
}
}
return NULL;
}
方法二: 这个方法比较巧妙,实现起来比方法一更快,也是更推荐 使用的方法。
我们可以设进环的位置到相遇节点的距离为N。那么slow走的路程就是L+N,而fast走的路程就是L+x*C+N,根据fast走的距离时slow的两倍,我们可以得出一个等式
2(L+N)= L+x*C+N
L = x*C-N
L = (x-1)*C+C-N
根据这个等式我们可以得出,起点到进入环的第一个节点的距离等于(x-1)个圆环长度加上meet到起点到进入环的第一个节点的距离。此时我们让两个指针从head和meet开始遍历链表,最后相遇的节点肯定是进入环的第一个节点。
题目完整代码如下:
cs
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
struct ListNode *detectCycle(struct ListNode *head)
{
struct ListNode* fast = head;
struct ListNode* slow = head;
while(fast&&fast->next)
{
fast=fast->next->next;
slow=slow->next;
if(slow==fast)
{
struct ListNode* meet = slow;
while(meet!=head)
{
meet=meet->next;
head=head->next;
}
return meet;
}
}
return NULL;
}
4.随机链表的复制
题目链接:随机链表的复制

这题也是这四道题目难度最大 的一题,也是链表掌握程度的一块试金石。
简单来说,深拷贝 就是拷贝一个值和指针指向跟当前链表一模一样的链表。
下面一起跟着我的思路看看这道题目。
这道题目的难点关键在于怎么找到random指向的节点。因为我们要创建一个全新的链表,又不能使用原来链表的节点,所以我们不能直接拷贝random的值。那我们如果在每个random指向的节点后面创建一个与random指向节点一样的节点,是不是就可以通过random->next->next来找到我们新链表random指向的节点。为了方便,我们干脆在每个节点后都创建一个一模一样的节点,如下图所示
这样做的好处也是让原链表和新链表建立起了一种关联关系。
如果节点的random指向为空,那我们就让新节点的random也指向为NULL,接着让指针指向下一个新创建的节点。
如果节点的random指向不为空,那我们就让新节点的random指向原节点random的next指针,也就是对应random所指向的新创建节点,接着让指针指向下一个新创建的节点。
当我们把所有新节点的random都设置好后,就可以把新链表串起来,并把原链表恢复原样。
具体的实现思路:
1.拷入节点插入在原节点后面
2.用两个指针分别遍历新链表和原链表,给新节点的random赋值
3.把拷贝节点取下来尾插成为新链表,然后恢复原链表(不恢复也可以)
下面是这道题目的完整代码:
cs
/**
* Definition for a Node.
* struct Node {
* int val;
* struct Node *next;
* struct Node *random;
* };
*/
struct Node* copyRandomList(struct Node* head)
{
struct Node* cur = head;
while(cur)
{
struct Node* copy = (struct Node*)malloc(sizeof(struct Node));
copy->val=cur->val;
copy->next = cur->next;
cur->next=copy;
cur=copy->next;
}
cur = head;
while(cur)
{
struct Node* copy = cur->next;
if(cur->random==NULL)
{
copy->random=NULL;
}
else
{
copy->random=cur->random->next;
}
cur=copy->next;
}
struct Node* copyhead = NULL;
struct Node* copytail = NULL;
cur=head;
while(cur)
{
struct Node* copy = cur->next;
struct Node* next = copy->next;
if(copyhead==NULL)
{
copyhead=copytail=copy;
}
else
{
copytail->next=copy;
copytail=copy;
}
cur->next=next;
cur=next;
}
return copyhead;
}
这道题目综合链表的多种方法,逻辑也比较复杂,也有不少易错点,需要勤加练习。
结语:
这篇文章全文由作者手写,图片由画图软件所制,无AI制作,希望各位博友能有所收获
欢迎各位博友的讨论,觉得不错的小伙伴,别忘了点赞关注哦~
