一.环形链表
简单的说一下这个题要表达的意思,其实就是判断我们的链表里有没有环形。这种链表我们是不能遍历的,因为我们判断出循环的条件是为NULL,用这个条件的话会导致死循环。
解决这个题的思想就是要清楚如果是环形我们需要什么条件(因为我们都是在一个圆圈里面循环的走,用快慢指针一直在圆圈里面转圈,如果存在相等那么这个链表就存在循环链表)。这个题要写代码的话非常简单,先看一下代码,后面在解释为什么:
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
bool hasCycle(struct ListNode *head)
{
struct ListNode* slow=head;//慢指针
struct ListNode* fast=head;//快指针
while(fast&&fast->next)//如果有NULL,则直接跳出循环,返回的值是false。
{
slow=slow->next;
fast=fast->next->next;//快指针一次比慢指针一次多走一步
if(slow==fast)
return true;//这里判断相等的条件,后面我会细致讲,这里是个结论
}
return false;
}
在这里给大家画个图:
这里的直线就代表不是环形链表的一部分,圆就代表是环形链表的一部分。刚开始的slow和fast都指向头结点,fast比slow移动的要快一步,所以在后面肯定也是先到达环形的部分。当slow指针也进入了环形链表,此时就开始进行快慢指针的追赶。
问题1:为什么快慢指针一定会相遇,有没有可能错过?
那么有一个问题:为什么在我的代码中,fast和slow走到的位置会刚好相等?它们有没有可能会刚好错过?导致永远不相等呢?
答案是一定会相等的,因为我这里的快指针一次只是走两步,比慢指针快了一步。
此时的fast已经在循环里面了,而slow才刚进去。然后slow走1,fast走2。它们是同时走的,根据相对位移,fast相对于slow走了1。它们的距离相对的减少了1。依次递减,本来它们的距离为N,而总有一个时刻它们的距离会递减到0。
所以说,它们之间如果fast一次走两步,一定可以判断出来是不是循环链表。
问题2:fast指针一次走3步,4步,N步呢?
那么还有一个问题:我不让fast指针一次走两步了,一次走3步可不可以达到我们的目的呢?
根据我们上面写的一步,当fast一次走两步的时候,我们可以得出,slow和fast的距离一次缩小1。相同的,我们一次走三步,slow和fast指针的距离就一次的缩小2。
也就是说我们有两种情况需要来分析,一种是N为偶数,fast和slow就刚好可以遇到,一种是奇数,fast在接近slow是刚好跳过slow,走到了slow前面的位置。
奇数时,当N等于1时,fast的位置在slow后面的一位。再往后走三位刚好走到了slow前面的一位,此时是刚好错过的状态。但是它们还是会继续走,此时的fast与slow的距离就变成了C-1。我们就又需要讨论一下C-1的奇偶性。
总结一下这里的情况:
到这里,也许我们就了解了,当N为奇数和C为偶数的时候,它们是永远不可能相遇的。
但是!这种情况成立吗?
先告诉大家,这是不成立的。
fast走的距离是slow的3倍。
所以3*L = L+x*C+C-N
2*L = (x+1)*C-N
我们知道,2乘以任何数都是偶数,则后面的结果也必然是偶数,上面我们说过当永远不可能相遇的情况是N为奇数,C为偶数,现在带进去试一下,我们就可以知道(x+1)*C必然是奇数,但是我们的结论是C为偶数,所以这是不成立的。则上面我们所说的这种情况是不存在的。
所以我们还可以总结出一个关于fast走三步的结论:N如果为偶数,第一轮就可以追上,如果N不是偶数,后面的第二轮也可以追上。
后面还有fast走四步,走五步等等的步数,解答起来非常的麻烦,意义也不大了。
二.环形链表二(找环形链表的头)
还是先来看代码:
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
struct ListNode *detectCycle(struct ListNode *head)
{
struct ListNode* slow=head,*fast=head;//快慢指针
while(fast&&fast->next)
{
slow=slow->next;
fast=fast->next->next;
if(fast==slow)//相等了就说明这是一个环
{
struct ListNode* meet=slow;//在相等的这个地方创建一个新的变量
while(meet!=head)
{
meet=meet->next;
head=head->next;//头指针和meet指针一次走一步,相遇的地方就是进入环形链表的地方
}
return meet;
}
}
return NULL;
}
这里我们知道当meet指针与head指针走到相同的地方了,也就是说这个地方就是环形链表的进入的地方。
那么为什么meet指针和head指针相遇时这个地方就是进入环形链表的入口呢?
为什么meet指针和head指针相遇的地方就是进入环形链表的入口?
如图所示,还是用数学的方式来解决。
因为slow走了:L+N
fast走了:L+N+x*C(x*C的意思是在slow进入循环链表之前,fast已经转了x圈)。
因为fast走的距离就是slow的二倍,所以:2*L+2*N=L+N+x*C
化简后就是:L=x*C-N。为了好理解,这个又可以换成L=(x-1)*C+C-N。
因为如果fast想要追上slow的话,就必须要多比slow走一圈,所以这里的x必定是>=1的。又因为(x-1)*C表达的意思就是fast转了多少圈,后面的C-N才是重点,此时的L就等于C-N。那么我们就可以得出为什么meet从此处出发的话,走的距离就刚好是head走的距离,它们的相遇点就是环的头。
三.随机链表的复制
这道题简单的说就是把一个链表完全的复制下来。只不过这个链表比我们以前的常用的的链表多了一个随机指针random,这个题目主要就是关于随机指针的复制不好弄。
同样的,先来看代码
/**
* 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)//在每个节点的后面复制一个与前面节点相同val的节点
{
struct Node* copy=(struct Node*)malloc(sizeof(struct Node));
copy->val=cur->val;
copy->next=cur->next;
cur->next=copy;
cur=cur->next->next;//每一次移动两位,跳过我们自己复制的节点
}
cur = head;
while(cur)
{
struct Node* copy=cur->next;//把copy指针指向我们刚才复制的地方
if(cur->random==NULL)
{
copy->random=NULL;//如果random指针指向的是NULL,我们复制的节点也指向NULL
}
else
{
copy->random=cur->random->next;//我们复制的节点里的random指针指向它上一个节点里的random指向的下一个节点
}
cur=cur->next->next;//也是指向跳过两位
}
struct Node* newhead=NULL;
struct Node* newtail=NULL;//创建我们新链表的头和尾
cur=head;
while(cur)
{
struct Node* copy=cur->next;
struct Node* next=copy->next;//把原链表的第二个节点存起来
//尾插的方式
if(newtail==NULL)
{
newhead=newtail=copy;
}
else
{
newtail->next=copy;
newtail=newtail->next;
}
cur->next=next;//把原来的链表复原
cur=next;//往后移动,这里是原链表的第二个节点
}
return newhead;
}
在前两个while循环里,我们做的就是复制,举个例子,如图所示:
其实就是在原链表的每一个“空隙”里复制前面的那个节点,大家可以看一下它们是怎么样实现的,也是非常巧妙。
到这里我分享了三个关于链表的OJ题目。整体的代码实现不难,难的是上面的逻辑,为什么要这样写。感谢大家的观看,如有错误,还请多多指出。