链表OJ:【环形链表】【环形链表二(找环形链表的头)】【随机链表的复制】

一.环形链表

 简单的说一下这个题要表达的意思,其实就是判断我们的链表里有没有环形。这种链表我们是不能遍历的,因为我们判断出循环的条件是为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题目。整体的代码实现不难,难的是上面的逻辑,为什么要这样写。感谢大家的观看,如有错误,还请多多指出。

  • 26
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值