判断链表是否有环

判断一个单向链表是否有环。(指向表头结点的指针为head)

方法一:

(1)用两个指针p1和p2分别指向表头结点,即p1=p2=head

(2)p1和p2分别采用1和2作为步长遍历该链表。(注意,p2应该检查当前结点的下一个结点是否为NULL)

(3)如果p1或者p2遇到了NULL,则证明该链表没有环;若p1和p2在某时刻指向同一结点,则说明该链表有环。

bool I***itsLoop(slist * head) { slist * slow = head , * fast = head; while ( fast && fast -> next ) { slow = slow -> next; fast = fast -> next -> next; if ( slow == fast ) break ; } return ! (fast == NULL || fast -> next == NULL); }

(4)若该表有环,

(a)设从表头结点(包括)开始到环开始的结点(不包括)共 有l1个结点;设从环开始结点(包括)到它们相遇的结点(不包括)共有l2个结点;设从他们第一次相遇的结点开始(包括)到环开始结点(不包括)共有l3个结点;设整个环共有c个结点。则有c=l2+l3,且l1+l2即为它们第一次相遇时,p1所遍历的结点个数。

(b)当它们第一次相遇时,固定p2,然后p1以1为步长继续遍历此表,则他们再次相遇时,p1从上次相遇到这次相遇所经过的总步长即为环中结点的个数c。

(c)可以证明,当他们第一次相遇时,p1不可能经过环开始结点两次,即不可能开始第二次遍历环。设当它们第一次相遇时,p2已经把环遍历了k遍(k>=1)则有:2(l1+l2) = l1+l2+kc,即l1+l2=kc

(d)l1+l2=kc=>l1=(k-1)c+l3

(e)固定p2在它们第一次相遇的结点,然后p1回到表头,然后它们均以1为步长遍历链表,则它们第一次相遇时,即为环开始结点

方法二:

(a)p从表头结点开始以1为步长遍历表,边遍历边将表反向

(b)如果p遇到NULL,则说明表没有环

(c)如果p最后等于head,则说明表有环,且记此时p所经过的表的结点数为l(l=2l1+c,l1和c的定义见方法一)

(d)p再从表头结点开始以1为步长遍历表,边遍历边反向,当遍历到l/2时,停止,设两个指针p1,p2均指向当前结点,然后分别从两个方向同时以1为步长遍历表(其中一个需要边遍历,边反向链表),当他们第相遇时,当前结点即为环头结点。且此时链表还原成原来的链表。

第三种方法(通过修改链表,最终可还原链表,且可去掉链表中的环)

或许可以再构造了一个双向链,但不存储原来的数据而存储节点指针:

typedef struct _PtrLinkNode { LinkNode* theNode; struct _PtrLinkNode* prev; struct _PtrLinkNode* next; } PtrLinkNode;

然后在逆转链表时把当前节点指针加入到这个双向链表中。当逆转结束时如果这个双向链表的首尾的theNode不相等,则说明没有死链,再逆转回来就可以了;如果相等,则存在死链,再在这个双向链表从两端向中间进行首尾比较,直到遇到不相等的两个节点,这两个节点形成的闭区间就是原来形成死链的节点,顺序与现在在双向链表中的顺序相同。把双向链表中位于这个区间之后的节点支掉,然后按双向链表的顺序重建链表就可以恢复出原来的链表并去除死链。时间复杂度和空间复杂度都是O(N)。

=====================================================

几大派系的算法:

herryhuang(Herry) 派:

『圆子示踪法』

每隔N个接点,插入一个示踪圆子。如果找到示踪圆子,那么就说明在该示踪圆子之前存在死链,再怎么找到确却的死链位置,有待探讨;找完死链以后再把示踪圆子删除;

plainsong (短歌)派:

『查表法』

顺着接点往下走,每走一个,查找记录接点值是否存在,若存在,则有死链,且该接点就是死链位置,否则,记录接点值。

老迈派:

『指针追赶法』

用两个指针指向头接点,然后顺次遍历连表,一个步进1,一个步进2,相遇且不是null,则有死链。相遇时都是null,则没有。如果没有死莲,两个指针是不会相遇的,除非在两头。

注:原子示踪法中 示踪原子的插入方法

void* flags[MAX];

使用的时候这样

比如原来是:

a1->next == a2

那么现在就是

a1->next == &flags[k]; flags[k] == a2;

这样,拿到一个指针p后,只需要判断

if(p >= flags && p <= &flags[MAX])

即可判断他是不是一个标志节点,这个跟具体结构没有任何关系

方法时间空间复杂度:

再一个死循环里发现结论的最大时间为 K + N, (K 步长, N死循环长度,M死循环位置)

而在这之前的复杂度为M, 所以复杂度最大为M + K + N, K越小时间越小

但是空间复杂度至少为(M + N)/K, 就要看怎么均匀这两者.

而且如果要求对链表只能读,也不能够应用

老迈的算法,时间复杂不如你的,但是它的空间复杂度几乎为0

老迈的总结:

大家讨论得差不多了吧,其实如果仔细看看上面的发言,就会发现后来的各位所讨论的东西已经有充分的讨论了,其实如果仅仅要判断链表是不是死链(这就是楼主的要求嘛),老迈的方法肯定比我的快,因为我访问的每一个节点至少要做三到四次比较(各位写出代码来接明白了),而老迈的只需要两次。另外,我的方法申请空间肯定是要费时间的。如高要找出那个出问题的节点,则我的方法就比较快了,因为将插入的节点放在线形表中。

如下,插入节点的步长为Setp,存放这些插入的节点的线形表为p[]

如果在插入p[m]之后,碰到了曾经插入的节点p[n],则可以断定,出问题的节点位于p[n-1]到p[m]之间,而它指向的位置,位于p[m – 1]到p[m]之间,这是个常量,最多再进行2*Step*step次比较即可找到这个节点。这是个常数。

所以,我的方法的关键问题在于找到一个合理的方法存放线形表,刚才编了半天,很麻烦,呵呵,下午还有事,各位继续发言,有时间的话写一个出来大家看看,我先撤了。

============================================

三个派系的比较:

本帖的三大派别:

plainsong (短歌)派:

『查表法』

顺着接点往下走,每走一个,查找记录接点值是否存在,若存在,则有死链,且该接点就是死链位置,否则,记录接点值。

—>这个似乎,虽然查找复杂度 最大仅 为 N,但是空间..汗汗..的确比较大

herryhuang(Herry) 派:

『圆子示踪法』

每隔N个接点,插入一个示踪圆子。如果找到示踪圆子,那么就说明在该示踪圆子之前存在死链,再怎么找到确却的死链位置,有待探讨;找完死链以后再把示踪圆子删除;

—>如果可以更改链表指针,个人认为这个办法是最好的,最大时间复杂度 N+K(K为步长),如果K设置成一个比较合理的值,应该能在时间和空间上都取得很好的效果(在前两个K点前加两个针指应该可以把确定链表的范围缩小到1K到2K之间吧

老迈派:

『指针追赶法』

用两个指针指向头接点,然后顺次遍历连表,一个步进1,一个步进2,相遇且不是null,则有死链。相遇时都是null,则没有。如果没有死莲,两个指针是不会相遇的,除非在两头。

–>这个办法,有最小的空间占用吧,确定有死链最大应该是2*N吧,确定位置应该有相应算法吧,估计是3N左右吧,如果是小型链表,这个办法应该比较好,但在巨型链表时,这个从速度上而言,反而不如第二种办法了吧.

更多解法请见:http://topic.csdn.net/t/20040906/09/3343269.html#

扩展问题:

判断两个单链表是否相交,如果相交,给出相交的第一个点(两个链表都不存在环)。

比较好的方法有两个:

一、将其中一个链表首尾相连,检测另外一个链表是否存在环,如果存在,则两个链表相交,而检测出来的依赖环入口即为相交的第一个点。

二、如果两个链表相交,那个两个链表从相交点到链表结束都是相同的节点,我们可以先遍历一个链表,直到尾部,再遍历另外一个链表,如果也可以走到同样的结尾点,则两个链表相交。

这时我们记下两个链表length,再遍历一次,长链表节点先出发前进(lengthMax-lengthMin)步,之后两个链表同时前进,每次一步,相遇的第一点即为两个链表相交的第一个点。

原文发布于微信公众号 - 顶级程序员(TopCoding)

原文发表时间:2016-11-20

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Android机器圈

递归 —— 二分查找法 —— 归并排序

二分法就是把一个数组折半查找,再折半直到找到数据位置,或者无数据位置。比如说1-100,你选的值是23,那么范围写法就是(索引写法类似)

1764
来自专栏杨熹的专栏

【LEETCODE】模拟面试-39. Combination Sum

和subset区别:规定了子集的sum==target 注意,这里传递的起始位置是i,而不是position+1,but why??? helper(res, ...

2845
来自专栏恰同学骚年

剑指Offer面试题:12.在O(1)时间删除链表结点

  在单向链表中删除一个结点,最常规的做法无疑是从链表的头结点开始,顺序遍历查找要删除的结点,并在链表中删除该结点。这种思路由于需要顺序查找,时间复杂度自然就是...

701
来自专栏郭耀华‘s Blog

Java集合框架(五)—— Map、HashMap、Hashtable、Properties、SortedMap、TreeMap、WeakHashMap、IdentityHashMap、EnumMap

Map Map用于保存具有映射关系的数据,因此Map集合里保存着两组值,一组值用于保存Map里的key,另一组值用于保存Map里的value,key和v...

2917
来自专栏个人随笔

Java 关于集合框架那点事儿

 1.引入集合框架   采用数组存在的一些缺陷:    1.数组长度固定不变,不能很好地适应元素数量动态变化的情况。    2.可通过数组名.length获取数...

29110
来自专栏cmazxiaoma的架构师之路

Java数据结构和算法(2)--《Java数据结构和算法》第二版 Robert lafore第二章【数组】编码作业

2073
来自专栏云霄雨霁

字符串排序----低位优先的字符串排序

2730
来自专栏mySoul

​单链表 C++

1012
来自专栏软件开发 -- 分享 互助 成长

解释器模式

一、简介 1、解释器模式给定一个语言,定义它的文法的一种表示,并定义一个解释器,这个解释器使用该表示来解释语言中的句子。 2、类成员 (1)AbstractEx...

2217
来自专栏老马说编程

(55) 容器类总结 / 计算机程序的思维逻辑

从38节到54节,我们介绍了多种容器类,本节进行简要总结,我们主要从三个角度进行总结: 用法和特点 数据结构和算法 设计思维和模式 用法和特点 我们在52节...

2227

扫码关注云+社区

领取腾讯云代金券