如何检测链表中的循环?

时间:2010-04-18 17:08:54

标签: java algorithm data-structures linked-list

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

class Node {
    Node next;
    // some user data
}

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

最好的写作方式是什么

boolean hasLoop(Node first)

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

这是一张带循环的列表的图片:

alt text

26 个答案:

答案 0 :(得分:509)

您可以使用Floyd's cycle-finding algorithm,也称为龟龟和野兔算法

 我们的想法是对列表进行两次引用,并以不同的速度移动它们。通过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;
    }
}

答案 1 :(得分:107)

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

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
}

答案 2 :(得分:49)

Turtle and 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;

测试代码:

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);
}

答案 3 :(得分:46)

优于Floyd的算法

理查德布伦特描述了一个alternative cycle detection algorithm,它非常像野兔和乌龟[弗洛伊德的周期],除此之外,这里的慢节点不会移动,但后来被“传送”到了该位置。固定间隔的快速节点。

说明可在此处找到:http://www.siafoo.net/algorithm/11 Brent声称他的算法比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;
}

答案 4 :(得分:28)

Tortoise and hare

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

(如果你很懒,你可以查看cycle detection - 查看关于乌龟和野兔的部分。)

这只需要线性时间和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;
}

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

答案 5 :(得分:13)

用户unicornaddict上面有一个很好的算法,但遗憾的是它包含了一个奇怪长度&gt; = 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;
    }
}

答案 6 :(得分: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;
}

<强>复杂性

Time ~ O(n)
Space ~ O(n)

答案 7 :(得分: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++;
}

答案 8 :(得分:3)

如果我们允许嵌入课程Node,我会解决问题,因为我已经在下面实施了它。 hasLoop()在O(n)时间内运行,仅占用counter的空格。这看起来是一个合适的解决方案吗?或者有没有办法在不嵌入Node的情况下执行此操作? (显然,在实际实现中会有更多方法,如RemoveNode(Node n)等)

public class LinkedNodeList {
    Node first;
    Int count;

    LinkedNodeList(){
        first = null;
        count = 0;
    }

    LinkedNodeList(Node n){
        if (n.next != null){
            throw new error("must start with single node!");
        } else {
            first = n;
            count = 1;
        }
    }

    public void addNode(Node n){
        Node lookingAt = first;

        while(lookingAt.next != null){
            lookingAt = lookingAt.next;
        }

        lookingAt.next = n;
        count++;
    }

    public boolean hasLoop(){

        int counter = 0;
        Node lookingAt = first;

        while(lookingAt.next != null){
            counter++;
            if (count < counter){
                return false;
            } else {
               lookingAt = lookingAt.next;
            }
        }

        return true;

    }



    private class Node{
        Node next;
        ....
    }

}

答案 9 :(得分:3)

public boolean hasLoop(Node start){   
   TreeSet<Node> set = new TreeSet<Node>();
   Node lookingAt = start;

   while (lookingAt.peek() != null){
       lookingAt = lookingAt.next;

       if (set.contains(lookingAt){
           return false;
        } else {
        set.put(lookingAt);
        }

        return true;
}   
// Inside our Node class:        
public Node peek(){
   return this.next;
}

请原谅我的无知(我对Java和编程仍然相当陌生),但为什么上述工作不起作用?

我想这并不能解决恒定的空间问题......但它至少在合理的时间到达那里,对吗?它只占用链表的空间加上具有n个元素的集合的空间(其中n是链表中的元素数,或者直到它到达循环的元素数)。对于时间,我认为最坏情况分析会建议O(nlog(n))。 contains()的SortedSet查找是log(n)(检查javadoc,但我很确定TreeSet的底层结构是TreeMap,而它又是一个红黑树),在最坏的情况下(没有循环,或者在最后循环),它必须进行n次查找。

答案 10 :(得分:2)

在这种情况下,到处都有文本材料的负担。我只是想发布一个图表表示形式,确实有助于我理解该概念。

快点和慢点在p点相遇时,

快速移动的距离= a + b + c + b = a + 2b + c

以慢速行驶的距离= a + b

因为快的速度比慢的速度快2倍。 因此 a + 2b + c = 2(a + b),则我们得到 a = c

因此,当另一个慢指针从 head再次运行到q 时,同时,快指针将从 p到q 运行,因此它们在<一起strong> q 。

enter image description here

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;
}

答案 11 :(得分:1)

检测链表中的循环可以用最简单的方法之一完成,这会导致O(N)的复杂性。

当您从头开始遍历列表时,创建一个已排序的地址列表。插入新地址时,请检查已排序列表中的地址是否已存在,这会导致O(logN)复杂性。

答案 12 :(得分:1)

boolean hasCycle(Node head) {

    boolean dec = false;
    Node first = head;
    Node sec = head;
    while(first != null && sec != null)
    {
        first = first.next;
        sec = sec.next.next;
        if(first == sec )
        {
            dec = true;
            break;
        }

    }
        return dec;
}

使用上面的函数来检测java中链表中的循环。

答案 13 :(得分:1)

 // To detect whether a circular loop exists in a linked list
public boolean findCircularLoop() {
    Node slower, faster;
    slower = head;
    faster = head.next; // start faster one node ahead
    while (true) {

        // if the faster pointer encounters a NULL element
        if (faster == null || faster.next == null)
            return false;
        // if faster pointer ever equals slower or faster's next
        // pointer is ever equal to slower then it's a circular list
        else if (slower == faster || slower == faster.next)
            return true;
        else {
            // advance the pointers
            slower = slower.next;
            faster = faster.next.next;
        }
    }
}

答案 14 :(得分:1)

你甚至可以在持续的O(1)时间内完成它(虽然它不会非常快或有效):计算机内存可以容纳的节点数量有限,比如N个记录。如果遍历的记录超过N条,那么就有一个循环。

答案 15 :(得分:0)

我处理这个帖子的时间可能非常晚。但还是......

为什么不能将节点的地址和指向的“next”节点存储在表

如果我们可以这样列表

node present: (present node addr) (next node address)

node 1: addr1: 0x100 addr2: 0x200 ( no present node address till this point had 0x200)
node 2: addr2: 0x200 addr3: 0x300 ( no present node address till this point had 0x300)
node 3: addr3: 0x300 addr4: 0x400 ( no present node address till this point had 0x400)
node 4: addr4: 0x400 addr5: 0x500 ( no present node address till this point had 0x500)
node 5: addr5: 0x500 addr6: 0x600 ( no present node address till this point had 0x600)
node 6: addr6: 0x600 addr4: 0x400 ( ONE present node address till this point had 0x400)

因此形成了一个循环。

答案 16 :(得分:0)

这是我的可运行代码。

我所做的是通过使用跟踪链接的三个临时节点(空间复杂度O(1))来尊重链表。

关于这样做的有趣事实是帮助检测链表中的循环,因为当你继续前进时,你不希望回到起点(根节点),其中一个临时节点应该去null,除非你有一个循环,这意味着它指向根节点。

此算法的时间复杂度为O(n),空间复杂度为O(1)

以下是链表的类节点:

public class LinkedNode{
    public LinkedNode next;
}

以下是主要代码,其中包含三个节点的简单测试用例,最后一个节点指向第二个节点:

    public static boolean checkLoopInLinkedList(LinkedNode root){

        if (root == null || root.next == null) return false;

        LinkedNode current1 = root, current2 = root.next, current3 = root.next.next;
        root.next = null;
        current2.next = current1;

        while(current3 != null){
            if(current3 == root) return true;

            current1 = current2;
            current2 = current3;
            current3 = current3.next;

            current2.next = current1;
        }
        return false;
    }

以下是最后一个节点指向第二个节点的三个节点的简单测试用例:

public class questions{
    public static void main(String [] args){

        LinkedNode n1 = new LinkedNode();
        LinkedNode n2 = new LinkedNode();
        LinkedNode n3 = new LinkedNode();
        n1.next = n2;
        n2.next = n3;
        n3.next = n2;

        System.out.print(checkLoopInLinkedList(n1));
    }
}

答案 17 :(得分:0)

我看不出任何使这需要固定时间或空间的方法,两者都会随着列表的大小而增加。

我会使用IdentityHashMap(假设还没有IdentityHashSet)并将每个Node存储到地图中。在存储节点之前,您将在其上调用containsKey。如果节点已经存在,则您有一个周期。

ItentityHashMap使用==而不是.equals,以便您检查对象在内存中的位置,而不是它是否具有相同的内容。

答案 18 :(得分:0)

此代码已经过优化,并且会比选择最佳答案的代码更快地生成结果。此代码可以避免进入一个非常长的追逐前向和后向节点指针的过程,如果我们按照以下情况将会出现这种情况最佳答案&#39;方法。看看下面的干运行,你就会意识到我想说的话。然后通过下面给出的方法看问题并测量一下。找到答案的步骤。

1→2→9-→3 ^ -------- ^

以下是代码:

boolean loop(node *head)
{
 node *back=head;
 node *front=head;

 while(front && front->next)
 {
  front=front->next->next;
  if(back==front)
  return true;
  else
  back=back->next;
 }
return false
}

答案 19 :(得分:0)

这是我在java中的解决方案

boolean detectLoop(Node head){
    Node fastRunner = head;
    Node slowRunner = head;
    while(fastRunner != null && slowRunner !=null && fastRunner.next != null){
        fastRunner = fastRunner.next.next;
        slowRunner = slowRunner.next;
        if(fastRunner == slowRunner){
            return true;
        }
    }
    return false;
}

答案 20 :(得分:0)

您也可以按照上述答案中的建议使用Floyd的乌龟算法。

此算法可以检查单链表是否具有闭合周期。 这可以通过使用将以不同速度移动的两个指针迭代列表来实现。这样,如果有一个循环,两个指针将在未来的某个时刻相遇。

请随时查看链接列表数据结构中的blog post,其中还包含一个代码片段,其中包含上述java语言算法的实现。

此致

Andreas(@xnorcode)

答案 21 :(得分:0)

这是检测周期的解决方案。

public boolean hasCycle(ListNode head) {
            ListNode slow =head;
            ListNode fast =head;

            while(fast!=null && fast.next!=null){
                slow = slow.next; // slow pointer only one hop
                fast = fast.next.next; // fast pointer two hops 

                if(slow == fast)    return true; // retrun true if fast meet slow pointer
            }

            return false; // return false if fast pointer stop at end 
        }

答案 22 :(得分:0)

//链表查找循环功能

int findLoop(struct Node* head)
{
    struct Node* slow = head, *fast = head;
    while(slow && fast && fast->next)
    {
        slow = slow->next;
        fast = fast->next->next;
        if(slow == fast)
            return 1;
    }
 return 0;
}

答案 23 :(得分:0)

如果链表结构实现了java.util.List。我们可以使用列表大小来跟踪我们在列表中的位置。

我们可以遍历节点,比较我们当前的位置和最后一个节点的位置。如果我们当前的位置超过了最后一个位置,我们就检测到列表在某处有一个循环。

此解决方案占用恒定的空间量,但伴随着随着列表大小的增加而线性增加完成时间的代价。


class LinkedList implements List {
    Node first;
    int listSize;
    
    @Override
    int size() {
        return listSize;
    }

    [..]

    boolean hasLoop() {
        int lastPosition = size();
        int currentPosition = 1;
        Node next = first;
        while(next != null) {
           if (currentPosition > lastPosition) return true;
           next = next.next;
           currentPosition++;
        }
        return false;
    }
}

或作为实用程序:

static boolean hasLoop(int size, Node first) {
    int lastPosition = size;
    int currentPosition = 1;
    Node next = first;
    while(next != null) {
       if (currentPosition > lastPosition) return true;
       next = next.next;
       currentPosition++;
    }
    return false;
}

答案 24 :(得分:-1)

这种方法有空间开销,但实现更简单:

可以通过在Map中存储节点来识别循环。在放置节点之前;检查节点是否已存在。如果节点已存在于地图中,则表示链接列表已循环。

public boolean loopDetector(Node<E> first) {  
       Node<E> t = first;  
       Map<Node<E>, Node<E>> map = new IdentityHashMap<Node<E>, Node<E>>();  
       while (t != null) {  
            if (map.containsKey(t)) {  
                 System.out.println(" duplicate Node is --" + t  
                           + " having value :" + t.data);  

                 return true;  
            } else {  
                 map.put(t, t);  
            }  
            t = t.next;  
       }  
       return false;  
  }  

答案 25 :(得分:-2)

public boolean isCircular() {

    if (head == null)
        return false;

    Node temp1 = head;
    Node temp2 = head;

    try {
        while (temp2.next != null) {

            temp2 = temp2.next.next.next;
            temp1 = temp1.next;

            if (temp1 == temp2 || temp1 == temp2.next) 
                return true;    

        }
    } catch (NullPointerException ex) {
        return false;

    }

    return false;

}