检测单链表中循环的开始?

Posted

技术标签:

【中文标题】检测单链表中循环的开始?【英文标题】:detecting the start of a loop in a singly linked link list? 【发布时间】:2010-12-04 22:47:02 【问题描述】:

有没有办法使用不超过两个指针找出链接列表中循环的开始? 我不想访问每个节点并将其标记为已看到并报告已看到第一个节点。还有其他方法吗?

【问题讨论】:

以前有人问过这个吗? google.com/… Explain how finding cycle start node in cycle linked list work?的可能重复 【参考方案1】:

第一步: 照常进行,你会用找到循环,即 有两个指针,一个一步递增,另一个两步递增,如果它们在某个时间相遇,则有一个循环。

第 2 步: 将一个指针冻结在原来的位置,并逐步增加另一个指针,计算您所做的步骤,当它们再次相遇时,计数将为您提供循环的长度(此与计算循环链表中的元素个数相同)。

步骤 3: 将两个指针重置为链表的开头,将一个指针递增到循环时间的长度,然后启动第二个指针。一步递增两个指针,当它们再次相遇时,它将成为循环的开始(这与从链接列表末尾找到第 nth 元素相同)。

【讨论】:

写得很好!!非常感谢 想了一会儿解决这个问题(我猜没那么久,大约 5 分钟),然后我决定我应该读一下答案,读完之后它看起来很简单!!!喜欢/讨厌这类问题。 第二步完全没有必要。相反,在第一步之后,您只能将一个指针重置为列表的头部,然后一次将两个指针递增一步,当它们相遇时,这将是循环的开始。 我认为第二步是必要的,因为被重置的指针可能到达循环的开头,而另一个指针在循环中的其他位置。 @RoyalleBlue,我在这里聚会有点晚了,但为了其他人的利益:实际上,第二步(以及第三步的开始)显然是不必要的。如果根节点距离循环开始'k'步,则循环内的碰撞点也将恰好距离循环开始'k'步。位置是确定性的。它被称为弗洛伊德算法。【参考方案2】:
void loopstartpoint(Node head)
    Node slow = head.next;;
    Node fast = head.next.next;

    while(fast!=null && fast.next!=null)
        slow = slow.next;
        fast = fast.next.next;

        if(slow==fast)
            System.out.println("Detected loop ");
            break;
        
    

    slow=head;
    while(slow!=fast)
        slow= slow.next;
        fast = fast.next;
    
    System.out.println("Starting position of loop is "+slow.data);

【讨论】:

【参考方案3】:
    检测循环 将每个元素的地址复制到集合中。如果发现重复,那就是循环的开始

【讨论】:

【参考方案4】:

请参阅this链接以获得全面的答案。

【讨论】:

我也有这个书签,但现在链接似乎坏了? @hari 我修复了链接。 帖子的更新链接已更改。请更新新链接:umairsaeed.com/posts/…【参考方案5】:

首先我们尝试找出列表中是否有任何循环。如果循环存在,那么我们尝试找出循环的起点。为此,我们使用两个指针,即 slowPtr 和 fastPtr。在第一次检测(检查循环)中,fastPtr 一次移动两步,而 slowPtr 一次向前移动一步。

slowPtr   1   2   3   4   5   6   7
fastPtr   1   3   5   7   9   5   7

很明显,如果列表中有任何循环,那么它们将在点(上图中的点 7)相遇,因为 fastPtr 指针的运行速度比另一个快两倍。

现在,我们来解决寻找循环起点的第二个问题。

假设,他们在第 7 点相遇(如上图所示)。然后,slowPtr 退出循环并位于列表的开头,即在点 1,但 fastPtr 仍然在汇合点(点 7)。现在我们比较两个指针的值,如果它们相同,则它是循环的起点,否则我们向前移动一步(这里 fastPtr 也是每次移动一步)并再次比较直到找到相同的点。

slowPtr   1   2   3   4
fastPtr   7   8   9   4

现在想到一个问题,这怎么可能。所以有很好的数学证明。

假设:

m => length from starting of list to starting of loop (i.e 1-2-3-4)
l => length of loop (i.e. 4-5-6-7-8-9)
k => length between starting of loop to meeting point (i.e. 4-5-6-7)

Total distance traveled by slowPtr = m + p(l) +k
where p => number of repetition of circle covered by slowPtr

Total distance traveled by fastPtr = m + q(l) + k
where q => number of repetition of circle covered by fastPtr

Since,
fastPtr running twice faster than slowPtr

Hence,
Total distance traveled by fastPtr = 2 X Total distance traveled by slowPtr
i.e
m + q(l) + k = 2 * ( m + p(l) +k )
or, m + k = q(l) - p(l)
or, m + k = (q-p) l
or, m = (q-p) l - k

So,
If slowPtr starts from beginning of list and travels "m" length then, it will reach to Point 4 (i.e. 1-2-3-4)

and
fastPtr start from Point 7 and travels " (q-p) l - k " length then, it will reach to Point 4 (i.e. 7-8-9-4),
because "(q-p) l" is a complete circle length with " (q-p) " times.

More detail here

【讨论】:

数学证明中的小错误。m+q(l)+k=2*(m+p(l)+k) => m+k=q(l)-2*p(l) 这应该被接受,因为它是最简单的实现,也是最漂亮的解决方案。 用证明很好解释【参考方案6】:

这是在链表中查找循环开始的代码:

public static void findStartOfLoop(Node n) 

    Node fast, slow;
    fast = slow = n;
    do 
        fast = fast.next.next;
        slow = slow.next;
     while (fast != slow);       

    fast = n;
    do 
        fast = fast.next;
        slow = slow.next;
    while (fast != slow);

    System.out.println(" Start of Loop : " + fast.v);

【讨论】:

【参考方案7】:

数学证明+解决方案

Let 'k' be the number of steps from HEADER to BEGINLOOP.
Let 'm' be the number of steps from HEADER to MEETPOINT.
Let 'n' be the number of steps in the loop.
Also, consider two pointers 'P' and 'Q'. Q having 2x speed than P.

简单案例:当 k

当指针“P”位于 BEGINLOOP 时(即它会经过“k”步),Q 将经过“2k”步。因此,实际上,当 P 进入循环时,Q 比 P 领先 '2k-k = k' 步,因此,Q 现在比 BEGINLOOP 落后 'n-k' 步。

当 P 从 BEGINLOOP 移动到 MEETPONT 时,它会经过 'm-k' 步。在那个时候,Q 会走 '2(m-k)' 步。但是,由于他们相遇,并且 Q 在 BEGINLOOP 后面开始了“n-k”步,所以,有效地, '2(m-k) - (n-k)' 应该等于 '(m-k)' 所以,

=> 2m - 2k - n + k = m - k
=> 2m - n = m
=> n = m

这意味着,P 和 Q 的交汇点等于循环中的步数(或多个,一般而言,请参见下面提到的情况)。现在,在 MEETPOINT,P 和 Q 都落后了 'n-(m-k)' 步,即落后了 'k' 步,正如我们看到的 n=m。 所以,如果我们再次从 HEADER 开始 P,从 MEETPOINT 开始 Q,但这次的速度等于 P,P 和 Q 现在将只在 BEGINLOOP 相遇。

一般情况:说,k = nX + Y,Y (因此,k%n = Y)

当指针“P”位于 BEGINLOOP 时(即它会经过“k”步),Q 将经过“2k”步。因此,实际上,当 P 进入循环时,Q 比 P 提前了 '2k-k = k' 步。但是,请注意“k”大于“n”,这意味着 Q 会进行多轮循环。因此,实际上,Q 现在比 BEGINLOOP 落后 'n-(k%n)' 步。

当 P 从 BEGINLOOP 移动到 MEETPOINT 时,它会经过“m-k”步。 (因此,实际上,MEETPOINT 现在将比 BEGINLOOP 提前 '(m-k)%n' 步。)在那个时候,Q 将行进 '2(m-k)' 步。但是,由于他们相遇了,并且 Q 在 BEGINLOOP 后面开始了 'n-(k%n)' 步,所以,实际上,Q 的新位置(即 '(2(mk) - (nk/%n))%n ' from BEGINLOOP) 应该等于 P 的新位置(即来自 BEGIN LOOP 的 '(mk)%n')。

所以,

=> (2(m - k) - (n - k%n))%n = (m - k)%n
=> (2(m - k) - (n - k%n))%n = m%n - k%n
=> (2(m - k) - (n - Y))%n = m%n - Y   (as k%n = Y)
=> 2m%n - 2k%n - n%n + Y%n = m%n - Y
=> 2m%n - Y - 0 + Y = m%n - Y    (Y%n = Y as Y < n)
=> m%n = 0
=> 'm' should be multiple of 'n'

【讨论】:

现在我更喜欢这个答案了! @pikooz,这个证明对 n、m 和 k 的任何值都成立吗? @pikoooz,假设,如果循环在 1000 个节点之后开始。因此,k=1000。如果循环非常小,例如 4 个节点。因此,n = 4。因此,m 也将大于 1000。那么,在这种情况下,n = m 将如何呢? (如果我在某处出错,请纠正我)。 @Vikram,感谢您指出这一点!我已经更新了我的答案。看看,现在是否有意义。 很棒的解释! +1【参考方案8】:

我找到的最佳答案在这里:tianrunhe: find-loop-starting-point-in-a-circular-linked-list

'm' 是 HEAD 和 START_LOOP 之间的距离 'L' 是循环长度 'd' 是 MEETING_POINT 和 START_LOOP 之间的距离

p1 在 V 处移动,p2 在 2*V 处移动

当 2 个指针相遇时:跑的距离 = m+ n*L -d = 2*(m+ L -d)

=> 这意味着(这里没有数学证明)如果 p1 从 HEAD 开始 & p2 从 MEETING_POINT 开始并且他们以相同的速度移动,他们将遇到@START_LOOP

【讨论】:

【参考方案9】:

    按照您将用来查找循环的常用方法继续。 IE。有两个指针,一个单步递增,另一个分两步递增,如果它们在某个时间相遇,则有一个循环。

    保持其中一个指针固定得到循环中的节点总数,比如 L。

    1234563

    现在使用循环中同一点的第二个指针(循环中断)遍历链表并计算剩余的节点数,例如 Y

    循环在 ((X+Y)-L)\2 个节点之后开始。或者从第 (((X+Y)-L)\2+1) 个节点开始。

【讨论】:

【参考方案10】:

    按照您将用来查找循环的常用方法继续。 IE。有两个指针,一个一步递增,另一个两步递增,如果它们在某个时间相遇,则有一个循环。

    保持其中一个指针固定得到循环中的节点总数,比如 L。

    1234563

    现在使用循环中同一点的第二个指针(循环中断)遍历链表并计算剩余的节点数,例如 Y

    循环在 ((X+Y)-L)\2 个节点之后开始。或者从第 (((X+Y)-L)\2+1) 个节点开始。

【讨论】:

【参考方案11】:

按照您将用来查找循环的常用方法进行操作。 IE。有两个指针,一个单步递增(slowPointer),另一个分两步递增(fastPointer),如果它们都在某个时间相遇,那就是一个循环。

您可能已经意识到,汇合点位于循环头部之前的 k 步。

其中 k 是列表中非循环部分的大小。

现在慢慢移动到循环的头部

在碰撞点保持快速

它们中的每一个都是从循环开始开始的 k 步(从列表的开始开始很慢,在循环头部之前的 k 步——绘制图片以获得清晰度)

现在以相同的速度移动它们 - 它们必须在循环开始时相遇

例如

slow=head
while (slow!=fast)

     slow=slow.next;
     fast=fast.next;

【讨论】:

【参考方案12】:

好吧,我尝试了一种使用一个指针的方法...我在几个数据集中尝试了该方法...由于链表的每个节点的内存都是按递增顺序分配的,因此在遍历链表从链表的头开始,如果一个节点的地址变得大于它所指向的节点的地址,我们就可以确定有一个循环,以及循环的开始元素。

【讨论】:

一般情况下(地址随N增加)不能保证,所以你的方法行不通。【参考方案13】:

有两种方法可以在链接列表中找到循环。 1.如果有循环,使用两个指针一个前进一步,另一个前进两步,在某些时候两个指针都得到相同的值并且永远不会达到空值。但是,如果没有循环指针在某一点达到 null 并且两个指针永远不会获得相同的值。但是在这种方法中,我们可以在链接列表中找到一个循环,但我们无法确定循环的确切开始位置。这也不是有效的方法。

    使用散列函数,使值应该是唯一的。如果我们试图插入副本,它应该通过异常。然后遍历每个节点并将地址推送到哈希中。如果指针达到 null 并且哈希中没有异常意味着链接列表中没有循环。如果我们从哈希中得到任何异常,则意味着列表中有一个循环,这就是循环开始的链接。

【讨论】:

【参考方案14】:

我听说过这个确切的问题是一个面试测验问题。

最优雅的解决方案是:

将两个指针都放在第一个元素上(称它们为 A 和 B)

然后继续循环::

将 A 前进到下一个元素 再次将 A 前进到下一个元素 将 B 前进到下一个元素 每次更新指针时,检查 A 和 B 是否相同。 如果在某些时候指针 A 和 B 是相同的,那么你有一个循环。 这种方法的问题是您最终可能会在循环中移动两次,但使用指针 A 不超过两次

如果你想真正找到有两个指针指向它的元素,那就更难了。除非你愿意多次重复链表,否则我会说不可能只使用两个指针。

使用更多内存的最有效方法是将指向元素的指针放入数组中,对其进行排序,然后查找重复。

【讨论】:

这被称为“弗洛伊德循环检测算法”又名“龟兔赛跑”en.wikipedia.org/wiki/Cycle_detection#Tortoise_and_hare balki 的解决方案发现它只使用了两个指针并且只循环了列表几次。 一旦找到 A 和 B 的碰撞点,就很容易找到循环的开始。(其他答案很好地解释了这一点。)另外,如果你真的想使用额外的内存,我' d 建议使用哈希集而不是对数组进行排序,因此至少时间复杂度保持 O(N)。 这并没有回答寻找循环起点的问题。

以上是关于检测单链表中循环的开始?的主要内容,如果未能解决你的问题,请参考以下文章

数据结构—约瑟夫环问题(循环单链表)

线性表练习之Example039-删除循环单链表中的所有最小值节点直至链表为空

线性表练习之Example002-删除递增非空单链表中的重复值域节点

02循环单链表

[数据结构与算法] 链表的其他类型

循环单链表 | 循环删除表中所有最小值并输出