如何检测链表中的循环?


434

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

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

每个节点都指向下一个节点,但最后一个节点除外,后者的下一个为空。假设列表有可能包含一个循环-即最终节点(而不是null)具有对列表中位于其之前的节点之一的引用。

最好的写作方式是什么

boolean hasLoop(Node first)

true如果给定的Node是带有循环的列表的第一个,则将返回false什么,否则返回?您怎么写才能占用恒定的空间和合理的时间?

这是带有循环的列表的图片:

替代文字


50
哇..我很想为这个雇主工作finite amount of space and a reasonable amount of time?:)
codaddict

10
@SLaks-循环不必循环回到第一个节点。它可以循环回到一半。
jjujuma

109
下面的答案值得一读,但是像这样的面试问题却很糟糕。您要么知道答案(即您已经看过Floyd算法的变体),要么不知道,并且它对测试推理或设计能力没有任何作用。
GaryF

3
公平地说,大多数“知道算法”都是这样的-除非您正在做研究级的工作!
拉里

12
@GaryF然而,当他们不知道答案时,知道他们将做什么将是一件令人难忘的事情。例如,他们将采取什么步骤,与谁一起工作,他们将采取什么措施来克服对算法知识的缺乏?
克里斯·奈特

Answers:


538

您可以利用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;
    }
}

29
还需要fast.nextnext再次调用之前进行null检查:if(fast.next!=null)fast=fast.next.next;
cmptrgeekken

12
您不仅应该检查(slow == fast),还应该检查:(slow == fast || slow.next == fast),以防止快车跳过慢车
Oleg Razgulyaev 2010年

13
我错了:快速不能跳过慢速,因为在下一步快速跳过慢速应该具有与慢速相同的pos :)
Oleg Razgulyaev 2010年

4
除非列表中只有一个节点,否则对慢== null的检查是多余的。您也可以摆脱对Node.next的调用。这是循环的一个更简单,更快速的版本:pastie.org/927591
凯·萨拉罗特

22
您应该真正引用您的参考文献。该算法由罗伯特·弗洛伊德(Robert Floyd)在20世纪60年代发明,又称弗洛伊德(Floyd)的循环查找算法。草龟和野兔算法。
joshperry 2010年

127

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

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
简洁明了。可以通过检查慢==快速||来优化此代码。(fast.next!= null && slow = fast.next); :)
arachnode.net

11
@ arachnode.net这不是优化。如果是,slow == fast.next则在下一个迭代中slow等于fast;它最多只能节省一次迭代,而每次迭代都需要额外的测试。
杰森C

@ ana01 slowfast遵循相同的引用路径之前不能为null (除非您同时修改列表,在这种情况下所有下注均关闭)。
Dave L.

出于好奇,这对奇数如何起作用?野兔仍不能通过奇数长度链表上的海龟吗?
theGreenCabbage

1
@theGreenCabbage循环的每次迭代中,野兔都比乌龟快1步。因此,如果野兔落后3步,那么下一次迭代将花费2步,而乌龟则进行1跳,现在野兔落后2步。在下一次迭代之后,野兔落后了1跳,然后被准确捕获。如果野兔花了3跳而乌龟花了1跳,那么它可能会跳过,因为它每次都会增加2,但是由于每次迭代只增加1,所以它不能跳过。
Dave L.

52

比弗洛伊德算法更好

理查德·布伦特(Richard Brent)描述了一种替代的周期检测算法,该算法与野兔和乌龟[弗洛伊德的周期]非常相似,不同之处在于,此处的慢速节点不移动,但后来“传送”到固定位置的快速节点的位置。间隔。

此处提供了描述:http : //www.siafoo.net/algorithm/11 布伦特声称他的算法比弗洛伊德循环算法快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;
}

这个答案真棒!
valin077 2014年

1
非常喜欢您的回答,并在我的博客-k2code.blogspot.in/2010/04/…中包含了它。
kinshuk4 2014年

为什么需要检查slow.next != null?据我所知slow总是落后或等于fast
TWiStErRob 2015年

很久以前,当我开始学习算法时就这样做了。编辑了代码。谢谢:)
Ashok Bijoy Debnath 2015年

50

我暂时更改列表后,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);
}

当循环指向除first以外的任何节点时,反向操作是否正常工作?如果初始链表是这样的1-> 2-> 3-> 4-> 5-> 2(循环从5到2),那么反向列表看起来像是1-> 2 <-3 <-4 <-5?如果相反,最终的重建名单会搞砸吗?
Zenil 2015年

1
@Zenil:这就是为什么我写了最后一个测试用例,其中最后一个节点指向列表的中间。如果重建无法正常进行,则该测试将失败。关于您的示例:1-> 2-> 3-> 5-> 2的反转将是1-> 2-> 5-> 4-> 3-> 2,因为循环仅在列表末尾停止已达到,而不是到达循环的末尾(我们无法轻易检测到)。
meriton

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

(大多数解决方案都不会同时检查next和检查是否next.next为空。此外,由于乌龟始终在后面,因此您不必检查是否为空-兔子已经这样做了。)


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

10

在这种情况下,文本材料无处不在。我只是想发布一个图表表示形式,这确实帮助我理解了这个概念。

当快慢点在p点相遇时,

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

慢速行驶的距离= a + b

因为快是慢的2倍。所以a + 2b + c = 2(a + b),则得到a = c

因此,当另一个慢速指针再次从头运行到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;
}

2
一幅图片价值超过数千个单词。感谢您简洁明了的解释!
Calios

1
互联网上最好的解释。只需补充一点,即可证明线性时间后快速指针和慢速指针会聚
VarunPandey

如果a大于循环长度,则快速将产生多个循环,公式distance (fast) = a + b + b + c将更改为a + (b+c) * k + b引入额外的参数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;
}

复杂

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

空间复杂度O(2n)如何?
Programmer345

@ user3543449您说对了,应该是n固定的
Khaled.K 2015年

1
这实际上是时间〜O(n ^ 2),因为每个包含对ArrayList的检查都需要O(n),并且其中有O(n)个。使用HashSet代替线性时间。
Dave L.

3
这不会测试周期,而是使用元素equals和来测试重复值hashCode。不一样 并且它取消引用null最后一个元素。而且问题没有说出有关将节点存储在.NET中的任何信息LinkedList
Lii 2015年

2
@Lii是伪代码,不是Java代码,这就是为什么我用Algorithm
Khaled.K,2016年

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

您如何知道要执行for()的列表中有多少个元素?
杰思罗·拉森 Jethro Larson)

@JethroLarson:链表中的最后一个节点指向一个已知地址(在许多实现中,这是NULL)。当到达该已知地址时,终止for循环。
闪亮

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次查找。


2
是的,使用某种Set的解决方案可以很好地工作,但需要的空间与列表的大小成比例。
jjujuma 2010年

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

}

1

您甚至可以在恒定的O(1)时间内执行此操作(尽管它不是非常快或高效):您的计算机内存可以容纳的节点数量有限,例如N条记录。如果遍历多于N条记录,则有一个循环。


这不是O(1),该算法在big-O表示法中没有有意义的时间复杂度。当输入大小达到无穷大时,big-O标记仅告诉您极限性能。所以,如果你的算法是建立在假设存在比N个元素对于一些大型的N多没有列出,运行时为列表的大小限制,趋于无穷大是不明确的。因此,复杂度不是“ O(任何)”。
fgp

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

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中链表中的循环。


2
几乎与我上面的答案相同,但是有一个问题。它将为具有奇数长度列表的列表(无循环)抛出NullPointerException。例如,如果head.next为null,则sec.next.next将抛出NPE。
Dave L.

1

可以通过最简单的方法之一来检测链表中的循环,这会导致使用哈希图导致O(N)复杂性或使用基于排序的方法导致O(NlogN)复杂性。

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


此方法的复杂度为O(N log N)
fgp

0

我看不到有什么方法可以使它花费固定的时间或空间,两者都会随着列表的大小而增加。

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

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


3
花费固定的时间当然是不可能的,因为列表的末尾可能会有一个循环,因此必须访问整个列表。但是,快速/慢速算法演示了使用固定内存量的解决方案。
Dave L.

它不是指它的渐近行为,即它是线性O(n),其中n是列表的长度。固定为O(1)
Mark Robson

0

我可能来晚了又很新来处理这个线程。但是还是

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

如果我们可以这样制表

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)

因此,形成了一个循环。


您的解决方案未通过“恒定空间量”要求。
2015年

0

这是我的可运行代码。

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

这样做的有趣之处在于,它有助于检测链表中的循环,因为在前进过程中,您不希望返回起始点(根节点),并且其中一个临时节点不应为空,除非您有一个周期,这意味着它指向根节点。

该算法的时间复杂度为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));
    }
}

0

此代码经过了优化,比选择最佳答案的结果更快地产生结果。此代码避免了追逐前向和后向节点指针的漫长过程,如果遵循“最佳”操作,将在以下情况下发生回答''方法。仔细阅读下面的内容,您将了解我要说的内容。然后通过下面的给定方法查看问题并测量否。找到答案的步骤。

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
}

您确定在所有情况下都能产生正确的结果吗?如果您在列表1-> 2-> 3-> 4-> 5-> 6-> 7-> 3-> ...上运行此算法,我相信它将返回4作为头部,而您想要3.
Sunreef

问题是查找是否存在循环。在这种情况下,这个问题绝对可以正常工作,并获得所需的布尔结果。如果您希望从循环开始的确切节点开始,那么我们将需要向代码中添加更多内容。但是就产生结果而言,这将更快地得出结论。
Sarthak Mehra

您没有正确地阅读问题:boolean hasLoop(Node first)如果给定的Node是带有循环的列表的第一个,则返回true,否则返回false 的最佳书写方式是什么?
Sunreef

这是您的列表的空运行。第一个值表示后向指针,第二部分表示前向指针。(1,1)-(1,3)-(2,3)-(2,5)-(3,5) -(3,7)-(4,7)-(4,4)。
Sarthak Mehra

实际上,我现在意识到,有两种方法可以理解这个问题(或者至少我看到两种不同的解释)。如果您只是在搜索是否存在循环,则您的算法是正确的。但是我以为问题在问循环从哪里开始。
Sunreef

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

0

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

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

请随时查看我在链接列表数据结构上的博客文章,其中还包括一个代码片段,其中包含上述Java语言算法的实现。

问候,

安德里亚斯(@xnorcode)


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 
        }

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

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

这不符合问题中给出的恒定空间限制!
dedek 2014年

同意有空间开销;这是解决此问题的另一种方法。明显的方法是乌龟和Harse算法。
rai.skumar 2014年

@downvoter,如果您也可以解释原因,将很有帮助。
rai.skumar

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

}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.