环形链表详解

简介: 环形链表详解

一、什么是环形链表

环形链表是一种特殊类型的链表数据结构,其最后一个节点的"下一个"指针指向链表中的某个节点,形成一个闭环。换句话说,链表的最后一个节点连接到了链表中的某个中间节点,而不是通常情况下连接到空指针(null)。如图:

由于链表最后一个节点的下一个指针没有指向NULL,而是指向前面的某一个节点,所以我们不能再用 “current->next==NULL” 作为判断条件来遍历链表(这样会造成死循环),通常使用快慢指针来控制条件,下面的两个题目都是要借助快慢指针来实现。


二、判断是否有环

题目给我们一个链表让我们判断链表中是否存在环,那么我们是否可以直接遍历一遍呢?没有环就会遍历到NULL,那我们就可以直接返回FALSE,否则如果存在环的话就会走到重复的节点,那就返回TRUE不就好啦?


一开始肯定会有同学会这样想,但是实际上,如果不存在环的话我们很容易判断,可如果有环的话我们又怎么判断这个节点是已经走过的节点呢?如果要把地址储存起来又很麻烦。


我们需要换一种解题方式,这里我们只用一个指针时肯定无法判断一个节点是否已经走过的,那么我们是否可以再增加一个指针,两个指针走的速度不同,走的快的指针先入环,走的慢的指针后入环。

之后这个题目就变成了追及与相遇问题,两个指针都在环中走,走的快的肯定可以追上走的慢的,当它们相遇时就说明存在环,否则当走的快的遇到NULL时,就说明不存在环。


代码示例如下:

struct ListNode* fast, *slow;
    fast = slow = head;
    while(fast && fast->next)
    {
        slow = slow->next;
        fast = fast->next->next;
        if(slow == fast)
            return true;
    }
    return false;


分析:

1. 为什么循环的结束条件是fast或者fast->next指向NULL?

  1. 当一个链表中有环时,fast和fast->next永远不会指向NULL。
  2. 当一个链表中没有环时,fast一定会移动到链表的结尾;又因为fast一次移动两个节点,所以有三种情况:
  1. fast移动两次后,刚好指向NULL,结束循环。
  2. fast移动一次后就已经指向NULL,此时再进行移动,就会出现对NULL的解引用。
  3. 链表本身就是空链表,fast->next会对NULL解引用。


2. 为什么要求快指针fast一次移动两个节点,慢指针slow一次移动一个节点?

  • 快指针fast一次移动两个节点,慢指针slow一次移动一个节点,两者一定会相遇吗?

移动的过程中有一下三种状态:

1)快指针fast和慢指针slow都没有进入环的入口点


2)快指针fast进入环的入口点(或已在环内)、慢指针slow没有进入环的入口点

3)快指针fast和慢指针slow都在环内

如果快指针fast和慢指针slow相遇,一定是在环内相遇,所以我们主要分析一下第三种状态。


此时fast和slow的距离设为N,当fast和slow每次移动时,速度差为1,两者的距离就会变成N-1(fast向前移动两个,slow向前移动一个,所以距离就近一个),移动一次两者的距离就-1,直到N会被减小到零,两者就相遇了。


快指针fast一次移动两个节点,慢指针slow一次移动一个节点时,两者一定会相遇。


  • fast一次不能移动三个/四个节点吗?

可以用同样的分析方法,来分析一下fast一次移动三个/四个节点的情况。

1)fast一次移动三个节点的情况:

此时fast和slow的距离设为N,当fast和slow每次移动时,两者的距离就会减小2。

不难发现,如果是每移动一次距离减2的话,可能会出现N不会减到0的情况,所以这里有以下两种情况:

  1. N为2的倍数时,N可以被减到零,快慢指针会相遇;(在未减到0之前,fast一直在slow的后面,fast追slow)
  2. N不为2的倍数时(奇数), N不会被减到0,N最后一次会被减到1,此时fast再向前移动2个节点、slow向前移动1个节点时,fast会领先slow一圈,这时fast会跑到slow的前面一个节点,此后可能会重复上述过程,也就是说fast不会相遇。

所以fast一次移动三个节点,fast和slow不一定会相遇,它有两个影响点:

  • 与环的长度

(C-1)是否是2的倍数

  • 环入口点之前节点的个数有关

N是否是2的倍数-->在slow还没有到达环入口点前,fast会在环内一直循环,环入口点之前节点的个数不同,当slow到达环入口点时,fast的与slow的相对位置会不同,这样N就不同

2)fast一次移动四个节点的情况

移动四个节点的分析方法与三个的相同

  1. N为3的倍数时,N可以被减到零,快慢指针会相遇;(在未减到0之前,fast一直在slow的后面,fast追slow)
  2. N不为3的倍数时,N不会被减到0,N最后一次会被减到1或2,此时fast再向前移动3个节点、slow向前移动1个节点时,fast会领先slow一圈,这时fast会跑到slow的前面一个或两个节点,此后可能会重复上述过程,也就是说fast不会相遇。


三、寻找环的入口

方法:

  1. 用之前的快慢指针方式找到相遇点
  2. 将一个指针指向开头
  3. 将指向开头的指针和指向相遇点的指针以相同的速度向后移动
  4. 再次相交的结果就是相遇点

代码示例如下:

struct ListNode *detectCycle(struct ListNode *head)
{
    struct ListNode* fast, *slow;
    fast = slow = head;
    while(fast && fast->next)
    {
        slow = slow->next;
        fast = fast->next->next;
        if(slow == fast)
        {
            struct ListNode* meet = slow;
            struct ListNode* cur = head;
            while(meet != cur)
            {
                meet = meet->next;
                cur = cur->next;
            }
            return meet;
        }
    }
    return NULL;
}

分析:

(设环的长度为C)当慢指针slow到达相遇点时,slow走过的距离设为L+X,则快指针fast走过的距离为L+X+n*C (n是slow为到达环入口点时,fast所走过环的圈数, n大于等于1)


1. slow走过的距离为什么是 L+X, 而不是L+X+圈数(即slow不会走环的一圈+X)?

最坏的情况是,如图

我们知道,如果有相同的起点,相同的时间,fast的速度是slow速度的二倍,所以fast走过的距离是slow的二倍,所以当slow回到起点时,fast刚好第二次回到起点,此时两者相遇,而slow也只是走了一圈。


由于走过前面 L 长度的节点后,fast和slow之间一定有距离,所以当slow第一次到达环的入口点时,fast可能不在环的入口点。如果slow再走完一圈,fast会走两圈,速度差为1,在此期间肯定会相遇。


2. fast走过的距离为什么是 L+X+n*C,并且n大于等于1?

这里我解释一下 “n*C,并且N大于等于1”,由于slow一次移动一个节点,fast一次移动两个节点,相同的时间下,fast的速度大于slow的速度,两者走过的距离肯定不相同,所以第一圈内,slow和fast不会相遇,只有当fast超过slow一圈或几圈后,两者才能相遇。


3. 为什么头结点到入口的距离等于相遇点到入口的距离

由数学推导可知:

因为fast走过的距离是slow走过距离的二倍,所以有  L+X+n*C = 2*(L+X)


进一步化简得,n*C = L+X,即(n-1)*C + C = L + X

又因为fast在环上转了(n-1)圈,但是fast与slow的相对位置不变,所以得到下式:

C = L + X


所以:

L=  C - X = N (N是相遇点与环的入口点的距离)

其他方法:

当然,寻找环的入口点还可以用另一种方法:

让快慢指针相遇点与下一个节点断开,然后将问题转化为两个链表的相交,环的入口点其实就是两个链表的相交点。


代码示例如下:

    struct ListNode* cur1 = headA;
    struct ListNode* cur2 = headB;
    int la = 1,lb = 1;
    if(!headA || !headB)
        return NULL;
 
    while(cur1->next)
    {
        cur1 = cur1->next;
        la++;
    }
    while(cur2->next)
    {
        cur2 = cur2->next;
        lb++;
    }
    if(cur1 != cur2) 
        return NULL;
 
    struct ListNode *LongList = headA;
    struct ListNode *ShortList = headB;
    if(la < lb)
    {
        LongList = headB;
        ShortList = headA;
    }
    else
    {
        LongList = headA;
        ShortList = headB;
    }  
    int n = la > lb? la-lb:lb-la;
    while(n--) 
        LongList = LongList->next;
        
    while(LongList != ShortList)
    {
        LongList = LongList->next;
        ShortList = ShortList->next;
    }
    return LongList;
}


相关文章
|
9月前
|
机器学习/深度学习 人工智能 算法
AI Agent驱动下的金融智能化:技术实现与行业影响
本文探讨了AI Agent在金融领域的技术实现与行业影响,涵盖智能投顾、风险控制、市场分析及反欺诈等应用场景。通过感知、知识管理、决策和行动四大模块,AI Agent推动金融从自动化迈向智能化。文中以Python代码展示了基于Q-learning的简易金融AI Agent构建过程,并分析其带来的效率革命、决策智能化、普惠金融和风控提升等变革。同时,文章也指出了数据安全、监管合规及多Agent协作等挑战,展望了结合大模型与增强学习的未来趋势。最终,AI Agent有望成为金融决策中枢,实现“智管钱”的飞跃。
AI Agent驱动下的金融智能化:技术实现与行业影响
|
Rust 前端开发 算法
java中如何实现单链表反转
本文介绍了单向链表的创建及其反转的三种实现方法。首先,通过`DataNode`类构建了一个包含10个节点的单向链表,并提供了链表的打印功能。接着,分别使用递归、遍历和借助栈的方式实现了链表反转。递归方法简单但受限于栈深度(最大约12000个节点),遍历方法通用且效率最高,而借助栈的方法虽然易于理解但效率较低。通过对不同方法的时间性能测试,得出遍历方式在处理大规模数据时表现最佳。
647 1
Python函数:函数的定义和调用
本文详细介绍了Python函数的定义和调用方法,包括基本函数定义、参数传递、返回值、文档字符串、作用域、嵌套函数和闭包。通过一个综合详细的学生成绩管理系统的例子,我们展示了如何在实际编程中应用这些函数概念。希望本文对您理解和应用Python函数有所帮助。
|
Linux 文件存储 Windows
linux软连接详解!!!
本文介绍了Linux文件类型、文件属性、文件存储机制以及软链接和硬链接的概念。主要内容包括:Linux文件类型及其识别方法、文件属性的组成及查看方式、inode和block的作用、软链接和硬链接的区别及应用场景。通过具体示例,帮助读者理解Linux文件系统的运作原理。
902 2
linux软连接详解!!!
|
缓存 安全 算法
高性能无锁并发框架Disruptor,太强了!
高性能无锁并发框架Disruptor,太强了!
272 0
高性能无锁并发框架Disruptor,太强了!
|
Java 调度
HashMap为什么会死循环?
本文分析了在Java中HashMap导致死循环的原因,主要由于在JDK 1.7及之前版本中,多线程环境下进行扩容操作时,头插法导致的链表反转,以及线程调度问题,从而形成循环链表。
1091 1
HashMap为什么会死循环?
|
人工智能 编解码 API
【选择”丹摩“深入探索智谱AI的CogVideoX:视频生成的新前沿】
【选择”丹摩“深入探索智谱AI的CogVideoX:视频生成的新前沿】
393 1
|
机器学习/深度学习 数据采集 人工智能
一文看尽LLM对齐技术:RLHF、RLAIF、PPO、DPO……
【8月更文挑战第27天】本文全面回顾了近期大型语言模型(LLMs)领域内提升模型与人类价值观一致性的重要进展与挑战。尽管自监督学习及大规模预训练等技术推动了LLMs的快速发展,但如何避免生成不当内容仍是难题。文中系统地将现有研究分为奖励模型、反馈机制、强化学习策略及优化方法四大主题,并深入探讨各技术路径的创新点与局限性,如RLHF、RLAIF等方法。旨在为读者提供清晰的领域概览,促进未来研究发展。[论文链接](https://arxiv.org/pdf/2407.16216)
858 3
|
搜索推荐 算法 编译器
【数据结构】八大排序之快速排序算法
【数据结构】八大排序之快速排序算法
695 4
|
存储 算法 Java
【经典算法】Leetcode 141. 环形链表(Java/C/Python3实现含注释说明,Easy)
【经典算法】Leetcode 141. 环形链表(Java/C/Python3实现含注释说明,Easy)
208 2