【数据结构】研究链表带环问题

简介: 我们只要判断它们是否相遇了,就能确定是否带环,因为如果没有带环的话,那就不可能相遇,快指针肯定先走出去。

💯💯💯💯


本篇主要研究的是链表带环问题,快慢指针的应用,分析不同解法对带环链表的处理,梳理完本篇你将对链表的理解更加透彻


Ⅰ.研究链表带环问题


链表带环是什么意思呢?

就是一个链表有一个结点指向了前面结点导致又链接回来的问题

如果进行遍历就会变成死循环


9c7f21e497324606bfbbcc74a9f16a3c.png


那么如何判断一个链表是否带环呢?


链表带环问题Ⅰ


de59e59e2c474a709b0a81801d0f9802.png


思路:


我们可以利用快慢指针,慢指针走一步,快指针走两步,当快指针进入环中时,慢指针肯定还在外面,当慢指针进环时,快指针可能已经走了好几圈,但一旦两个指针进环后,肯定会相遇,我们可以将带环问题,转换为相遇问题,也就是小学经常看到的,小明先跑50,速度为5m/s,小亮后跑速度为10m/s,问何时相遇。


我们只要判断它们是否相遇了,就能确定是否带环,因为如果没有带环的话,那就不可能相遇,快指针肯定先走出去。


快慢指针,即慢指针一次走一步,快指针一次走两步,两个指针从链表其实位置开始运行,如果链表带环则一定会在环中相遇,否则快指针率先走到链表的末尾。


190cd3f39fe948f4b849074d10965cbf.png


bool hasCycle(struct ListNode *head)
 {
    struct ListNode*fast,*slow;
    fast=slow=head;//快慢指针都从开头开始走
    while(fast&&fast->next)
    {
        slow=slow->next;
        fast=fast->next->next;
        if(fast==slow)//如果慢指针等于快指针则一定在环里
        return true;
    }
    return false;//否则就不在环里
}


Ⅱ.扩展带环问题


1.为什么慢指针和快指针一定会相遇?


假设链表带环,快慢指针同时走,快指针先入环,慢指针后入环,当慢指针刚入环时,有可能与快指针相遇,也有可能与快指针相差很远,但这不是问题,关键的关键是它们两都在环里,并且快指针每次走两步,慢指针每次走1步,而它们每次走一步时,它们之间的距离就减少1,每走一步,快指针就追上一步,所以它们之间的距离在不断的缩小,最后肯定能追上。


2d37b5457ca3434eb5e088498698968b.png


并且不会出现套圈的现象,因为两个结点之间最小的距离也就是1了,每次缩小1,最后要么重合,要么还是重合,不可能从头上跳过去的。


还有慢指针在慢指针走到一圈之前,快指针肯定是可以追得上慢指针的,即相遇

慢指针刚入环时,与快指针最远距离可能为一个环距离吧,但想一想,当慢指针走了一环距离时,快指针走了多少?快指针走的是慢指针的2倍呀,那肯定是2圈了都,慢指针走1圈,快指针都要走两圈了,这里面肯定追上慢指针了。


2.快指针一次走3步,4步…n步可以吗?


其实这种本质上看的是相对位移量,只要我们算出相对位移量就可以判断了。上面快指针每次走2步慢指针每次走1步,它们每次走一步都会缩小1距离,而因为1就是最小距离单位,所以最后肯定会相遇。


但如果一次走三步,就不一定了


假设快慢指针都入环后,它们之间的距离为N


快指针每次走3步,慢指针每次走1步,那它们每次走,快指针都会缩小2个距离,那它们之间的距离就变成了N-2,再走就变成N-4,N-6……,这里就涉及N是奇数还是偶数了,当N是偶数时,那么最后它们是可以相遇的,如果是奇数,那这圈是不会相遇的,不过最后走完快慢指针之间的距离就变成-1了,也就是这个环的周长减1距离,这又要取决于环的周长是偶数还是奇数了……。


9e111c26c65c4fb58d23bdc6af8ce25f.png


如果快指针每次走4步,道理也是一样的,在快慢指针都入环后,假设它们的距离为N


则每次距离都会缩小3,则最好是否相遇取决于N是否是3的倍数了。


6abeb4daebe34651bf842043eed000c2.png


3.慢指针走的距离和快指针走的距离?


faddf17a41244cad89093b7dfc1b2be0.png


你想一想快指针和慢指针在相遇前总共走了多远距离呢?


快慢指针是同时从开始位置走的,快指针走的快,先入环,慢指针走的慢,后入环。


前面我们知道fast在slow走满一圈之前追上,所以slow不可能走过一圈的,所以慢指针走的总距离为L+M


而快指针就不一定了,快指针走的快,先入环,谁知道它在环里走了多少圈了都,而且走的圈数越多说明这个环越小


7f3848b9b2fa46eca29c593915779dde.png


所以快指针走的总距离为


L+M+nC (n至少为1)*


n为走过的圈数


Ⅲ.总结归纳


因为快慢指针之间存在着关系,所以我们可以列出一个表达式,也就是快指针走的距离是慢指针走的距离的2倍


2*(L+M)=L+M+n*C

所以L=n*C-M


当最好情况下n=1时,L=C-M


L是什么?L就是开始点到入口点的距离


C-M是什么?C-M就是相遇点到入口点的距离。


也就是:一个指针从链表的起始位置开始运行,一个指针从相遇点位置绕环,每次都走一步,两个指针最终会在入口点的位置相遇


8946973c7877442b9afd4b534974a85c.png


结论:带环定理


让一个指针从链表的起始位置开始遍历,同时让一个指针从带环的相遇点位置开始遍历,两个指针都是每次走一步,最终一定会在入环处相遇!


Ⅳ.带环链表真题


给定一个链表,返回链表开始入环的第一个结点,如果链表无环,则返回NULL


aef513ebc7ad4a509ba12791146a238a.png


《链表带环问题Ⅱ》


要求判断是否有环,如果有请返回入环的第一个结点。


方法1:利用带环定理


这就是我上面两个例子的结合,第一步判断是否有环,第二步利用上面的定理:在有环的链表中,从开始位置和相遇点同时出发,将会在入环点相遇。


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(fast==slow)
        {
            //fast与slow现在就是相遇点
            //这时从开始点和相遇点同时走,当相等时,就是入环点
            while(head!=slow)//如果不相同就一直走
            {
                head=head->next;
                slow=slow->next;
            }
            return head;//当跳出来时就是入环点
        }
    }
    return NULL;
}


dbe48d5bfc664fe1b756ca7f0a74f89c.png


方法2:转换为相交链表问题


还有一种方法可以快速解决这个问题,我们可以将相遇点与后面那个结点断开


011be71c7c734a239f936ff893638180.png


那从断开的结点开始是不是就相当于一个链表了,那这道题目就转变成了相交链表的问题了,求两个链表相交的结点问题,我在上一篇博客中有写过这种方法。


如果求两个相交链表的结点呢?


其实很简单:


第一步求出两个链表的长度

第二步让长度较长的链表先走长度差步

第三两个链表一起走,并进行比较,当两个链表的结点相同时就是相交结点。


2f8f886214ea420b8b8ac257377da372.png


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(fast==slow)
      {
          //fast和slow现在就是相遇点,断开它和下一个结点并保存下一个结点的地址
          struct ListNode*newnode=slow->next;
          slow->next=NULL;
          //newnode就是新链表的头指针啦
    //开始求两个链表长度!
    int len1=0,len2=0;
    struct ListNode*tail1=head,*tail2=newnode;
    while(tail1)//计算链表1的长度
    {
        tail1=tail1->next;
        ++len1;
    }
    while(tail2)//计算链表2的长度
    {
        tail2=tail2->next;
        ++len2;
    }
    //两个链表长度是求出来来了,但不知道哪个是长链表哪个是短链表呀,所以还需要讨论下
    int gap=abs(len1-len2);
    struct ListNode*longlist=head,*shortlist=newnode;
    if(len1<len2)
    {
        longlist=newnode;
        shortlist=head;
    }
    //让长的链表先走长度差
    while(gap--)
    {
        longlist=longlist->next;
    }
    //然后两个链表再一起走,比较相同的时就是相交结点
    while(longlist!=shortlist)
    {
       longlist=longlist->next;
       shortlist=shortlist->next;
    }
    return longlist;//最后返回相交结点
      }
  }  
  return NULL;
}

7ad38040f911469aabf678617a7bb6f6.png


相关文章
|
5月前
|
存储 监控 安全
企业上网监控系统中红黑树数据结构的 Python 算法实现与应用研究
企业上网监控系统需高效处理海量数据,传统数据结构存在性能瓶颈。红黑树通过自平衡机制,确保查找、插入、删除操作的时间复杂度稳定在 O(log n),适用于网络记录存储、设备信息维护及安全事件排序等场景。本文分析红黑树的理论基础、应用场景及 Python 实现,并探讨其在企业监控系统中的实践价值,提升系统性能与稳定性。
176 1
|
5月前
|
存储 监控 算法
基于跳表数据结构的企业局域网监控异常连接实时检测 C++ 算法研究
跳表(Skip List)是一种基于概率的数据结构,适用于企业局域网监控中海量连接记录的高效处理。其通过多层索引机制实现快速查找、插入和删除操作,时间复杂度为 $O(\log n)$,优于链表和平衡树。跳表在异常连接识别、黑名单管理和历史记录溯源等场景中表现出色,具备实现简单、支持范围查询等优势,是企业网络监控中动态数据管理的理想选择。
175 0
|
存储 算法 Perl
数据结构实验之链表
本实验旨在掌握线性表中元素的前驱、后续概念及链表的建立、插入、删除等算法,并分析时间复杂度,理解链表特点。实验内容包括循环链表应用(约瑟夫回环问题)、删除单链表中重复节点及双向循环链表的设计与实现。通过编程实践,加深对链表数据结构的理解和应用能力。
230 4
|
10月前
|
存储 机器学习/深度学习 算法
C 408—《数据结构》算法题基础篇—链表(下)
408考研——《数据结构》算法题基础篇之链表(下)。
403 30
|
10月前
|
存储 算法 C语言
C 408—《数据结构》算法题基础篇—链表(上)
408考研——《数据结构》算法题基础篇之链表(上)。
488 25
|
11月前
|
机器学习/深度学习 存储 C++
【C++数据结构——线性表】单链表的基本运算(头歌实践教学平台习题)【合集】
本内容介绍了单链表的基本运算任务,涵盖线性表的基本概念、初始化、销毁、判定是否为空表、求长度、输出、求元素值、按元素值查找、插入和删除数据元素等操作。通过C++代码示例详细解释了顺序表和链表的实现方法,并提供了测试说明、通 - **任务描述**:实现单链表的基本运算。 - **相关知识**:包括线性表的概念、初始化、销毁、判断空表、求长度、输出、求元素值、查找、插入和删除等操作。 - **测试说明**:平台会对你编写的代码进行测试,提供测试输入和预期输出。 - **通关代码**:给出了完整的C++代码实现。 - **测试结果**:展示了测试通过后的预期输出结果。 开始你的任务吧,祝你成功!
554 5
|
12月前
|
数据库
数据结构中二叉树,哈希表,顺序表,链表的比较补充
二叉搜索树,哈希表,顺序表,链表的特点的比较
数据结构中二叉树,哈希表,顺序表,链表的比较补充
|
存储 缓存 算法
在C语言中,数据结构是构建高效程序的基石。本文探讨了数组、链表、栈、队列、树和图等常见数据结构的特点、应用及实现方式
在C语言中,数据结构是构建高效程序的基石。本文探讨了数组、链表、栈、队列、树和图等常见数据结构的特点、应用及实现方式,强调了合理选择数据结构的重要性,并通过案例分析展示了其在实际项目中的应用,旨在帮助读者提升编程能力。
371 5
|
存储 算法
非递归实现后序遍历时,如何避免栈溢出?
后序遍历的递归实现和非递归实现各有优缺点,在实际应用中需要根据具体的问题需求、二叉树的特点以及性能和空间的限制等因素来选择合适的实现方式。
307 59
|
6月前
|
编译器 C语言 C++
栈区的非法访问导致的死循环(x64)
这段内容主要分析了一段C语言代码在VS2022中形成死循环的原因,涉及栈区内存布局和数组越界问题。代码中`arr[15]`越界访问,修改了变量`i`的值,导致`for`循环条件始终为真,形成死循环。原因是VS2022栈区从低地址到高地址分配内存,`arr`数组与`i`相邻,`arr[15]`恰好覆盖`i`的地址。而在VS2019中,栈区先分配高地址再分配低地址,因此相同代码表现不同。这说明编译器对栈区内存分配顺序的实现差异会导致程序行为不一致,需避免数组越界以确保代码健壮性。
140 0
栈区的非法访问导致的死循环(x64)

热门文章

最新文章