数据结构算法——链表带环问题——数学深度解析

2024-05-02 06:20

本文主要是介绍数据结构算法——链表带环问题——数学深度解析,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

        前言:本节内容主要是讲解链表的两个问题 :1、判断链表是否带环; 2、一个链表有环, 找到环的入口点。 本节内容适合正在学习链表或者链表基础薄弱的友友们哦。

        我们先将问题抛出来,友友们可以自己去力扣或者牛客网去找相应题目, 这里直接贴链接:(没有做过这两个题的友友 千万! 千万! 千万! 要先自己做一下这两个题。)

        判断链表是否带环:141. 环形链表 - 力扣(LeetCode)

        带环链表的入环节点:LCR 022. 环形链表 II - 力扣(LeetCode)

        

目录

判断链表是否带环

题目解析

算法原理

算法演示

算法原理

原理扩展

环形链表的入口节点

题目解析

算法原理

算法演示

算法原理


        我们先来讲解第一道题

判断链表是否带环

题目解析

题目:

        

代码框:

        题目非常的简单, 就是要求我们设计一个算法, 判断这个链表中是否右带环结构就可以了。 如果有带环结构, 那么就返回true, 如果没有带环结构, 那么就返回false。 

算法原理

算法演示

        解决这个问题需要用到快慢双指针算法, 我们利用题中所给示例进行演示:

        先定义两个指针slow, fast。并且slow和fast要指向同时指向头节点, 否则在第二道题的时候处理起来会变的复杂

        然后, 我们向后进行遍历, 遍历的过程是这样的: slow指针一次向后移动一个节点。 fast一次向后移动两个节点。当两个节点相遇的时候就说明我们的链表是带环的。

        而如果我们的fast指向了空节点, 那么就说明我们的链表是不带环的。

        我们演示一遍是这样的:

代码贴图如下:

bool hasCycle(struct ListNode *head) 
{//先判断下链表为空的情况if (head == NULL) return false;//1、创建两个指针, slow, fast同时指向链表头节点。struct ListNode* slow = head;struct ListNode* fast = head;//2、遍历整个链表, 判断是否有环while (fast != NULL && fast->next != NULL){slow = slow->next;fast = fast->next->next;if (slow == fast) return true;  //如果两个指针指向同一个位置, 说明有环。}return false;             //退出循环说明无环。
}

        这就是算法的基本思路, 我们接下来进行剖析这个算法:

算法原理

        

        为了证明我们的结论的普遍性, 我们利用上面的抽象图来进行演示。

         这里的环周长就相当于C个节点, 前面的直线L就相当于没有进入环之前的L个节点。 然后slow每次走一个节点, fast一次走两个节点。 

        fast走的比slow要快, 所以, fast一定是在slow前面的。如果没有环的话, fast是肯定会先一步指向空的。 fast和slow也就无法相遇了。 所以如果fast指向空, 那么就一定没有环。

         但是如果有环, 这个时候fast一定会先进入环。如图所示:

        然后继续遍历, slow再进入环。如图所示:

        那么, 重点就来了。 slow进入环之后, 如果fast和slow再继续遍历, 那么是不是fast和slow之间的距离在不断的缩小, 是不是就相当于fast在追击slow。 我们假设fast到slow的距离为N, 此时我们就将问题转化为了一个追击相遇问题。  

        fast每次走两步, slow每次走一步。 那么每回合fast和slow之间的距离就减少1。 循环下来就是N - 1  - 1 - 1 - 1 - 1, 直到N为零位置。 此时fast和slow相遇。并且当这两个指针相遇的时候, 只有两种情况, 一个是在环的入口点相遇, 一个是在环的其他位置相遇。 但这两种情况可以归到一类里面——slow和fast会在环内相遇

        

 综上, slow和fast只要相遇了, 他们就会在环内, 说明链表有环。

        然后到这里这个题的算法原理基本结束了, 但是, 这里还有一个经常考的探究性问题, 很重要的扩展知识。 接下来进行分析:

原理扩展

        从上文我们知道, 当slow指针和fast指针相遇的时候一定再环内。 但是有没有可能slow和fast不会相遇, 每次fast指针都越过slow指针, 导致两个指针永远无法相遇?

        答案是我们前面分析的fast走两步, slow的情况不会。

        但是如果fast一次走三步, slow一次走一步以及一些其他情况就可能fast直接越过slow, 永远不会相遇。这里需要具体情况具体分析。

        分析过程如下:

        如果我们slow每回合走一步, fast每回合走三步。 那么fast和slow的步数就相差2。 假设slow进环的时候slow和fast之间的距离相差N。那么循环下来就是N - 2 - 2 - 2. 这里如果N是偶数, N就恰好减少到零了, 这个时候slow和fast指针就相遇了。

        但是如果N是奇数呢? N - 2 - 2 - 2就有可能得到-1,我们假设圆环的长度为C,这个时候就是如图这种情况:

        那么, fast和slow之间的距离此时变成了C - 1

        接下来再进行分类讨论, 如果C为奇数, 那么C - 1就为偶数, 这样 C - 1 - 2 - 2 - 2……就有可能减少到零; 如果C为偶数, 那么C - 1就是奇数, 这样 C - 1 - 2 - 2 ……就会重新减少到-1, 然后fast和slow之间的距离又变成C - 1, 这个时候就陷入了死循环。

         所以,综上我们可以得出小结论: 当fast一次走三步, slow一次走一步。如果N为偶数, 那么fast和slow一定相遇。 如果N为奇数, C为奇数。 那么fast和slow也会相遇。 如果N为奇数, C为偶数, 那么fast和slow永远不会相遇。

        将上面的推导过程总结为一个算数表达式就是 : (N + x * C)% 2  ? 0//距离N + x圈后模上fast和slow步数差是不是等于0.

        所以, 我们得出的大结论就是:假设slow和fast每回合的步数相差sub.进入环的时候fast指针与slow指针之间的距离为N。圆环的长度为C。如果有 :  (N + x * C)% sub  ==  0。就说明fast和slow会相遇, 否则不会相遇。

        以上就是本道题的所有知识点。

        ps:代码中的细节问题,属于代码编写的范畴, 不属于算法原理。 本篇内容只讲算法原理。 代码友友们自行编写调试。



环形链表的入口节点

题目解析

题目:

代码框: 

        

 这道题就是上面那道题的提高版本。 需要先对链表判断是否有环, 然后再判断入环节点。

算法原理

要找到环的入口点同样是有结论的, 这里我们先用结论进行代码的展示。 再进行分析

算法演示

        寻找环的入口点的算法就是先利用上面一题的方法先判断是否存在环。 这个时候如果存在环的话fast指针和slow指针会在环内的某一个点相遇。

        然后,我们定义一个meet指针指向这个相遇节点。 然后再重新定义一个指针指向链表的头节点。 让meet指针和指向头节点的指针同时向后遍历。 最后相遇的节点就是我们环的入口节点。 (原理是数学证明, 后面会进行证明。)

        如图为代码贴图:

struct ListNode *detectCycle(struct ListNode *head) 
{//1、定义快慢指针struct ListNode* slow = head;struct ListNode* fast = head;//2、循环判断是否有环while (fast != NULL && fast->next != NULL){//fast走两步, slow走一步。slow = slow->next;fast = fast->next->next;//如果相遇, 说明有环。 然后寻找入环节点if (slow == fast){//meet节点指向fast和slow相遇节点。struct ListNode* meet = slow;//让slow重新指向头节点slow = head;//如果两个指针不相等, 就让他们向后遍历。while (slow != meet){slow = slow->next;meet = meet->next;}//最后返回相遇节点return meet;}}return NULL;  //从循环中出来说明fast走向了空, 说明没有环。
}

算法原理

        要证明为什么从相遇位置和头节点的两个指针同时向后遍历, 相遇节点就是入环节点。我先给一张抽象图, 方便观察与理解:

假设我们从图中时刻开始向后遍历。 首先, fast先进环。如下图:

        然后, fast继续向后走。 一直到slow进环:

        好, 在这里停住, 这里有很重要的问题。 就是这个fast此时在环中已经走了几圈? 

        这个圈数确定吗?答案是不确定。 因为我们并不知道这个圈的大小, 如果这个圈很小很小。 然后前面的直链很长, 那么从fast进环到slow进环这一段时间中fast就可能在环中转了很多很多圈。

        我在这画出一个例子就好懂了:

        从这个例子我们可以看出, 在fast到slow这段时间内, fast在环中走的圈数是不确定的。

        知道了这点后, 我们继续向下遍历, 一直到slow和fast相遇。

        现在, 另外一个重要的问题就是:从slow进环到被fast追上, slow转了几圈?

        我们看这样一个例子:

        如果当slow进环的时候, fast恰好在slow前面一个位置。 如图:

        那么到fast追上slow的时候, slow转的了一圈吗?

        我们利用方程算一下:假设slow行走的路程是s, 那么fast就是2s。 此时就有:2s - s = C - 1;

得到的就是s == C - 1; 很显然就算在这种极端情况下slow都没有走一圈, 那么其他情况下更不可能走一圈。 那么上面的问题的答案就是: 从slow进环到被fast追上, slow一圈也没有转。

        有了这些的铺垫后。 我们再从整体出发看 : 从两个指针开始遍历到两个指针相遇。 设slow行走的距离是X。 那么fast指针行走的距离就是2 * X;  我们设从入环到节点到slow和fast相遇的位置的距离为N。如图:

        那么就有N + L = X;所以slow行走的距离就是N + L;fast行走的距离就是2 *(N + L)

        但是, 对于fast来说, 还有另外一个式子: fast在从入环到slow入环期间,我们分析过了。 fast可能行走了几圈。 我们设为k圈。  然后从入环节点到相遇位置为N。 那么就有了fast的行走距离又可以有 : L + k * C + N;

        那么就有  2 *(N + L)== L + k * C + N

        计算后就是k* C - N == L;  也可以写成 : (k - 1) * C + (C - N) == L

        而我们的相遇节点到入环节点的距离恰好是 C - N; 所以,根据这个式子。 我们就可以证明如果从相遇节点和头节点同时向后遍历, 那么最后再次相遇时的节点就是入环节点。 

---------------------------------------------------------------------------------------------------------------------------------

        以上, 就是本节的全部内容。

       

这篇关于数据结构算法——链表带环问题——数学深度解析的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/953569

相关文章

PostgreSQL的扩展dict_int应用案例解析

《PostgreSQL的扩展dict_int应用案例解析》dict_int扩展为PostgreSQL提供了专业的整数文本处理能力,特别适合需要精确处理数字内容的搜索场景,本文给大家介绍PostgreS... 目录PostgreSQL的扩展dict_int一、扩展概述二、核心功能三、安装与启用四、字典配置方法

怎样通过分析GC日志来定位Java进程的内存问题

《怎样通过分析GC日志来定位Java进程的内存问题》:本文主要介绍怎样通过分析GC日志来定位Java进程的内存问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录一、GC 日志基础配置1. 启用详细 GC 日志2. 不同收集器的日志格式二、关键指标与分析维度1.

深度解析Java DTO(最新推荐)

《深度解析JavaDTO(最新推荐)》DTO(DataTransferObject)是一种用于在不同层(如Controller层、Service层)之间传输数据的对象设计模式,其核心目的是封装数据,... 目录一、什么是DTO?DTO的核心特点:二、为什么需要DTO?(对比Entity)三、实际应用场景解析

Java 线程安全与 volatile与单例模式问题及解决方案

《Java线程安全与volatile与单例模式问题及解决方案》文章主要讲解线程安全问题的五个成因(调度随机、变量修改、非原子操作、内存可见性、指令重排序)及解决方案,强调使用volatile关键字... 目录什么是线程安全线程安全问题的产生与解决方案线程的调度是随机的多个线程对同一个变量进行修改线程的修改操

深度解析Java项目中包和包之间的联系

《深度解析Java项目中包和包之间的联系》文章浏览阅读850次,点赞13次,收藏8次。本文详细介绍了Java分层架构中的几个关键包:DTO、Controller、Service和Mapper。_jav... 目录前言一、各大包1.DTO1.1、DTO的核心用途1.2. DTO与实体类(Entity)的区别1

Java中的雪花算法Snowflake解析与实践技巧

《Java中的雪花算法Snowflake解析与实践技巧》本文解析了雪花算法的原理、Java实现及生产实践,涵盖ID结构、位运算技巧、时钟回拨处理、WorkerId分配等关键点,并探讨了百度UidGen... 目录一、雪花算法核心原理1.1 算法起源1.2 ID结构详解1.3 核心特性二、Java实现解析2.

Redis出现中文乱码的问题及解决

《Redis出现中文乱码的问题及解决》:本文主要介绍Redis出现中文乱码的问题及解决,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录1. 问题的产生2China编程. 问题的解决redihttp://www.chinasem.cns数据进制问题的解决中文乱码问题解决总结

使用Python绘制3D堆叠条形图全解析

《使用Python绘制3D堆叠条形图全解析》在数据可视化的工具箱里,3D图表总能带来眼前一亮的效果,本文就来和大家聊聊如何使用Python实现绘制3D堆叠条形图,感兴趣的小伙伴可以了解下... 目录为什么选择 3D 堆叠条形图代码实现:从数据到 3D 世界的搭建核心代码逐行解析细节优化应用场景:3D 堆叠图

深度解析Python装饰器常见用法与进阶技巧

《深度解析Python装饰器常见用法与进阶技巧》Python装饰器(Decorator)是提升代码可读性与复用性的强大工具,本文将深入解析Python装饰器的原理,常见用法,进阶技巧与最佳实践,希望可... 目录装饰器的基本原理函数装饰器的常见用法带参数的装饰器类装饰器与方法装饰器装饰器的嵌套与组合进阶技巧

解析C++11 static_assert及与Boost库的关联从入门到精通

《解析C++11static_assert及与Boost库的关联从入门到精通》static_assert是C++中强大的编译时验证工具,它能够在编译阶段拦截不符合预期的类型或值,增强代码的健壮性,通... 目录一、背景知识:传统断言方法的局限性1.1 assert宏1.2 #error指令1.3 第三方解决