如何检测链表中的循环?

jju*_*uma 413 java algorithm linked-list data-structures

假设您在Java中有一个链表结构.它由节点组成:

class Node {
    Node next;
    // some user data
}
Run Code Online (Sandbox Code Playgroud)

每个节点都指向下一个节点,最后一个节点除外.假设列表有可能包含一个循环 - 即最终的节点,而不是具有空值,具有对列表中的一个节点的引用.

什么是最好的写作方式

boolean hasLoop(Node first)
Run Code Online (Sandbox Code Playgroud)

true如果给定的Node是带循环的列表的第一个,它将返回,false否则?你怎么写,这需要一个恒定的空间和合理的时间?

这是一个循环列表的图片:

替代文字

cod*_*ict 520

您可以使用Floyd的循环寻找算法,也称为乌龟和野兔算法.

我们的想法是对列表进行两次引用,并以不同的速度移动它们.按1节点向前移动一个,按节点向前移动另一个2.

  • 如果链表有一个循环,他们肯定会遇到.
  • 否则两个引用(或它们next)中的任何一个都将成为null.

实现该算法的Java函数:

boolean hasLoop(Node first) {

    if(first == null) // list does not exist..so no loop either
        return false;

    Node slow, fast; // create two references.

    slow = fast = first; // make both refer to the start of the list

    while(true) {

        slow = slow.next;          // 1 hop

        if(fast.next != null)
            fast = fast.next.next; // 2 hops
        else
            return false;          // next node null => no loop

        if(slow == null || fast == null) // if either hits null..no loop
            return false;

        if(slow == fast) // if the two ever meet...we must have a loop
            return true;
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 还需要在再次调用`next`之前对`fast.next`进行空检查:`if(fast.next!= null)fast = fast.next.next;` (29认同)
  • 你应该引用你的参考资料.这个算法是由罗伯特弗洛伊德在60年代发明的,它被称为弗洛伊德的循环寻找算法,又称.乌龟与野兔算法. (21认同)
  • 我错了:快速不能跳得慢,因为下一步快速跳过慢应该有同样的pos慢:) (13认同)
  • 你应该检查不仅(慢= =快),但:(慢= =快||慢.下= =快)以防止快速跳过慢 (12认同)
  • 除非列表只有一个节点,否则检查slow == null是多余的.您还可以删除对Node.next的一次调用.这是一个更简单,更快速的循环版本:http://pastie.org/927591 (4认同)
  • 此代码不适用于奇数长度列表.如果`fast.next == null`那么元素`fast`指的是永远不会提前.一段时间后,`slow`引用将赶上并错误地报告循环. (3认同)
  • @Tim:谢谢你的指点.我已经更新了我的答案. (3认同)
  • [我在这里描述了这是如何工作的](http://i.imgur.com/GcTjwJr.png) (3认同)
  • @oraz:即使他们跳了,他们终于会在以后再见面.但是,我们可以将此检查作为优化. (2认同)
  • 蒂姆是对的,不要按原样使用这个解决方案!可怕的是,这样一个破碎的解决方案会被投票如此之高. (2认同)

Dav*_* L. 117

以下是快速/慢速解决方案的改进,可正确处理奇数长度列表并提高清晰度.

boolean hasLoop(Node first) {
    Node slow = first;
    Node fast = first;

    while(fast != null && fast.next != null) {
        slow = slow.next;          // 1 hop
        fast = fast.next.next;     // 2 hops 

        if(slow == fast)  // fast caught up to slow, so there is a loop
            return true;
    }
    return false;  // fast reached null, so the list terminates
}
Run Code Online (Sandbox Code Playgroud)

  • @ arachnode.net那不是优化.如果`slow == fast.next`那么`slow`将在下一次迭代时等于`fast`; 它最多只能保存一次迭代,但每次迭代都需要额外的测试. (10认同)
  • 很好,简洁.可以通过检查slow == fast ||来优化此代码 (fast.next!= null && slow = fast.next); :) (2认同)

mer*_*ike 49

对于Turtle和Rabbit的替代解决方案,并不像我暂时更改列表那样好:

我们的想法是走在列表中,并在您前进时将其反转.然后,当您第一次到达已经访问过的节点时,其下一个指针将指向"向后",从而导致迭代first再次继续,在此处终止.

Node prev = null;
Node cur = first;
while (cur != null) {
    Node next = cur.next;
    cur.next = prev;
    prev = cur;
    cur = next;
}
boolean hasCycle = prev == first && first != null && first.next != null;

// reconstruct the list
cur = prev;
prev = null;
while (cur != null) {
    Node next = cur.next;
    cur.next = prev;
    prev = cur;
    cur = next;
}

return hasCycle;
Run Code Online (Sandbox Code Playgroud)

测试代码:

static void assertSameOrder(Node[] nodes) {
    for (int i = 0; i < nodes.length - 1; i++) {
        assert nodes[i].next == nodes[i + 1];
    }
}

public static void main(String[] args) {
    Node[] nodes = new Node[100];
    for (int i = 0; i < nodes.length; i++) {
        nodes[i] = new Node();
    }
    for (int i = 0; i < nodes.length - 1; i++) {
        nodes[i].next = nodes[i + 1];
    }
    Node first = nodes[0];
    Node max = nodes[nodes.length - 1];

    max.next = null;
    assert !hasCycle(first);
    assertSameOrder(nodes);
    max.next = first;
    assert hasCycle(first);
    assertSameOrder(nodes);
    max.next = max;
    assert hasCycle(first);
    assertSameOrder(nodes);
    max.next = nodes[50];
    assert hasCycle(first);
    assertSameOrder(nodes);
}
Run Code Online (Sandbox Code Playgroud)


Ash*_*ath 47

比弗洛伊德的算法更好

理查德布伦特描述了另一种循环检测算法,它非常类似野兔和乌龟[弗洛伊德的循环],除了这里的慢节点不移动,但后来被"传送"到固定的快节点位置间隔.

可在此处获得描述:http://www.siafoo.net/algorithm/11 布伦特声称他的算法比Floyd的循环算法快24%到36%.O(n)时间复杂度,O(1)空间复杂度.

public static boolean hasLoop(Node root){
    if(root == null) return false;

    Node slow = root, fast = root;
    int taken = 0, limit = 2;

    while (fast.next != null) {
        fast = fast.next;
        taken++;
        if(slow == fast) return true;

        if(taken == limit){
            taken = 0;
            limit <<= 1;    // equivalent to limit *= 2;
            slow = fast;    // teleporting the turtle (to the hare's position) 
        }
    }
    return false;
}
Run Code Online (Sandbox Code Playgroud)


Lar*_*rry 28

乌龟和野兔

看看Pollard的rho算法.这不是同一个问题,但也许你会理解它的逻辑,并将它应用于链表.

(如果你很懒,你可以查看周期检测 - 检查关于乌龟和野兔的部分.)

这只需要线性时间和2个额外指针.

在Java中:

boolean hasLoop( Node first ) {
    if ( first == null ) return false;

    Node turtle = first;
    Node hare = first;

    while ( hare.next != null && hare.next.next != null ) {
         turtle = turtle.next;
         hare = hare.next.next;

         if ( turtle == hare ) return true;
    }

    return false;
}
Run Code Online (Sandbox Code Playgroud)

(大多数解决方案不检查两者nextnext.next空值.此外,由于乌龟总是落后,你不必检查它是否为空 - 野兔已经这样做了.)


Nei*_*eil 18

在这种情况下,到处都有文本材料的负载。我只是想发布一个真正帮助我掌握概念的图表表示。

当快慢在 p 点相遇时,

快速行驶的距离 = a+b+c+b = a+2b+c

慢速行驶的距离 = a+b

因为快比慢快2倍。所以a+2b+c = 2(a+b),那么我们得到a=c

因此,当另一个慢指针再次从head运行到 q 时,同时快速指针将从p运行到 q,因此它们在点q处相遇。

在此处输入图片说明

public ListNode detectCycle(ListNode head) {
    if(head == null || head.next==null)
        return null;

    ListNode slow = head;
    ListNode fast = head;

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

        /*
        if the 2 pointers meet, then the 
        dist from the meeting pt to start of loop 
        equals
        dist from head to start of loop
        */
        if (fast == slow){ //loop found
            slow = head;
            while(slow != fast){
                slow = slow.next;
                fast = fast.next;
            }
            return slow;
        }            
    }
    return null;
}
Run Code Online (Sandbox Code Playgroud)

  • 一张图片胜过千言万语。感谢您的简洁明了的解释! (2认同)
  • 网上最好的解释。只需补充一点,这证明快指针和慢指针在线性时间后收敛 (2认同)

Car*_*sak 13

用户unicornaddict上面有一个很好的算法,但遗憾的是它包含一个奇怪长度> = 3的非循环列表的错误.问题是fast可能会在列表结束之前"卡住",slow赶上它,并且(错误地)检测到循环.

这是修正后的算法.

static boolean hasLoop(Node first) {

    if(first == null) // list does not exist..so no loop either.
        return false;

    Node slow, fast; // create two references.

    slow = fast = first; // make both refer to the start of the list.

    while(true) {
        slow = slow.next;          // 1 hop.
        if(fast.next == null)
            fast = null;
        else
            fast = fast.next.next; // 2 hops.

        if(fast == null) // if fast hits null..no loop.
            return false;

        if(slow == fast) // if the two ever meet...we must have a loop.
            return true;
    }
}
Run Code Online (Sandbox Code Playgroud)


Kha*_*d.K 9

算法

public static boolean hasCycle (LinkedList<Node> list)
{
    HashSet<Node> visited = new HashSet<Node>();

    for (Node n : list)
    {
        visited.add(n);

        if (visited.contains(n.next))
        {
            return true;
        }
    }

    return false;
}
Run Code Online (Sandbox Code Playgroud)

复杂

Time ~ O(n)
Space ~ O(n)
Run Code Online (Sandbox Code Playgroud)

  • 这不会测试循环,而是使用元素`equals`和`hashCode`测试重复值.这不是一回事.它在最后一个元素上取消引用"null".问题没有说明将节点存储在`LinkedList`中. (2认同)
  • @Lii它是一个伪代码,这不是Java代码,这就是我用**算法命名的原因** (2认同)

Spa*_*rky 8

以下可能不是最好的方法 - 它是O(n ^ 2).但是,它应该有助于完成工作(最终).

count_of_elements_so_far = 0;
for (each element in linked list)
{
    search for current element in first <count_of_elements_so_far>
    if found, then you have a loop
    else,count_of_elements_so_far++;
}
Run Code Online (Sandbox Code Playgroud)