环形链表详解

简介: 环形链表详解

一、什么是环形链表

环形链表是一种特殊类型的链表数据结构,其最后一个节点的"下一个"指针指向链表中的某个节点,形成一个闭环。换句话说,链表的最后一个节点连接到了链表中的某个中间节点,而不是通常情况下连接到空指针(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;
}


相关文章
|
2天前
|
存储 缓存 关系型数据库
MySQL事务日志-Redo Log工作原理分析
事务的隔离性和原子性分别通过锁和事务日志实现,而持久性则依赖于事务日志中的`Redo Log`。在MySQL中,`Redo Log`确保已提交事务的数据能持久保存,即使系统崩溃也能通过重做日志恢复数据。其工作原理是记录数据在内存中的更改,待事务提交时写入磁盘。此外,`Redo Log`采用简单的物理日志格式和高效的顺序IO,确保快速提交。通过不同的落盘策略,可在性能和安全性之间做出权衡。
1517 4
|
29天前
|
弹性计算 人工智能 架构师
阿里云携手Altair共拓云上工业仿真新机遇
2024年9月12日,「2024 Altair 技术大会杭州站」成功召开,阿里云弹性计算产品运营与生态负责人何川,与Altair中国技术总监赵阳在会上联合发布了最新的“云上CAE一体机”。
阿里云携手Altair共拓云上工业仿真新机遇
|
5天前
|
人工智能 Rust Java
10月更文挑战赛火热启动,坚持热爱坚持创作!
开发者社区10月更文挑战,寻找热爱技术内容创作的你,欢迎来创作!
501 19
|
2天前
|
存储 SQL 关系型数据库
彻底搞懂InnoDB的MVCC多版本并发控制
本文详细介绍了InnoDB存储引擎中的两种并发控制方法:MVCC(多版本并发控制)和LBCC(基于锁的并发控制)。MVCC通过记录版本信息和使用快照读取机制,实现了高并发下的读写操作,而LBCC则通过加锁机制控制并发访问。文章深入探讨了MVCC的工作原理,包括插入、删除、修改流程及查询过程中的快照读取机制。通过多个案例演示了不同隔离级别下MVCC的具体表现,并解释了事务ID的分配和管理方式。最后,对比了四种隔离级别的性能特点,帮助读者理解如何根据具体需求选择合适的隔离级别以优化数据库性能。
179 1
|
8天前
|
JSON 自然语言处理 数据管理
阿里云百炼产品月刊【2024年9月】
阿里云百炼产品月刊【2024年9月】,涵盖本月产品和功能发布、活动,应用实践等内容,帮助您快速了解阿里云百炼产品的最新动态。
阿里云百炼产品月刊【2024年9月】
|
21天前
|
存储 关系型数据库 分布式数据库
GraphRAG:基于PolarDB+通义千问+LangChain的知识图谱+大模型最佳实践
本文介绍了如何使用PolarDB、通义千问和LangChain搭建GraphRAG系统,结合知识图谱和向量检索提升问答质量。通过实例展示了单独使用向量检索和图检索的局限性,并通过图+向量联合搜索增强了问答准确性。PolarDB支持AGE图引擎和pgvector插件,实现图数据和向量数据的统一存储与检索,提升了RAG系统的性能和效果。
|
9天前
|
Linux 虚拟化 开发者
一键将CentOs的yum源更换为国内阿里yum源
一键将CentOs的yum源更换为国内阿里yum源
451 5
|
7天前
|
存储 人工智能 搜索推荐
数据治理,是时候打破刻板印象了
瓴羊智能数据建设与治理产品Datapin全面升级,可演进扩展的数据架构体系为企业数据治理预留发展空间,推出敏捷版用以解决企业数据量不大但需构建数据的场景问题,基于大模型打造的DataAgent更是为企业用好数据资产提供了便利。
314 2
|
23天前
|
人工智能 IDE 程序员
期盼已久!通义灵码 AI 程序员开启邀测,全流程开发仅用几分钟
在云栖大会上,阿里云云原生应用平台负责人丁宇宣布,「通义灵码」完成全面升级,并正式发布 AI 程序员。
|
25天前
|
机器学习/深度学习 算法 大数据
【BetterBench博士】2024 “华为杯”第二十一届中国研究生数学建模竞赛 选题分析
2024“华为杯”数学建模竞赛,对ABCDEF每个题进行详细的分析,涵盖风电场功率优化、WLAN网络吞吐量、磁性元件损耗建模、地理环境问题、高速公路应急车道启用和X射线脉冲星建模等多领域问题,解析了问题类型、专业和技能的需要。
2608 22
【BetterBench博士】2024 “华为杯”第二十一届中国研究生数学建模竞赛 选题分析