ChatGPT解决这个技术问题 Extra ChatGPT

如何检测链表中的循环?

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

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

每个 Node 都指向下一个节点,除了最后一个 Node,它的 next 为 null。假设列表有可能包含一个循环 - 即最终节点,而不是空值,具有对列表中在它之前的节点之一的引用。

最好的写作方式是什么

boolean hasLoop(Node first)

如果给定节点是带有循环的列表的第一个,则返回 true,否则返回 false?你怎么能写出来,让它占用恒定的空间和合理的时间?

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

https://i.stack.imgur.com/irw1S.jpg

哇..我很想为这个雇主工作 finite amount of space and a reasonable amount of time? :)
@SLaks - 循环不需要循环回第一个节点。它可以循环回到中途。
下面的答案值得一读,但这样的面试问题很糟糕。您要么知道答案(即您已经看到了 Floyd 算法的变体),要么您不知道,而且它对测试您的推理或设计能力没有任何作用。
公平地说,大多数“了解算法”都是这样的——除非你在做研究级的事情!
@GaryF 然而,当他们不知道答案时,知道他们会做什么会很有启发性。例如,他们将采取哪些步骤,他们将与谁合作,他们将如何克服缺乏算法知识的问题?

D
Dave L.

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

这个想法是有两个对列表的引用并以不同的速度移动它们< /强>。一个向前移动 1 个节点,另一个向前移动 2 个节点。

如果链表有循环,它们肯定会相遇。

否则,两个引用中的任何一个(或它们的下一个)都将变为空。

实现算法的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;
    }
}

在再次调用 next 之前还需要对 fast.next 进行空检查:if(fast.next!=null)fast=fast.next.next;
您不仅应该检查 (slow==fast),还应该检查: (slow==fast || slow.next==fast) 以防止快速跳过慢速
我错了:快速不能跳过慢速,因为在下一步快速跳过慢速应该与慢速相同:)
除非列表只有一个节点,否则对 slow == null 的检查是多余的。您还可以摆脱对 Node.next 的调用。下面是一个更简单、更快速的循环版本:pastie.org/927591
你真的应该引用你的参考资料。这个算法是罗伯特·弗洛伊德在 60 年代发明的,它被称为弗洛伊德的寻环算法,又名。龟兔算法。
D
Dave L.

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

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
}

简洁明了。可以通过检查是否慢 == 快 || 来优化此代码(fast.next != null && slow = fast.next); :)
@arachnode.net 这不是优化。如果 slow == fast.nextslow 将在下一次迭代中等于 fast;它最多只保存一次迭代,但每次迭代都要进行额外的测试。
@ana01 slow 不能在 fast 之前变为 null,因为它遵循相同的引用路径(除非您同时修改了列表,在这种情况下所有赌注都关闭)。
出于好奇,这对奇数有何作用?野兔不能在奇数链表上通过海龟吗?
@theGreenCabbage 循环的每次迭代,兔子都会比乌龟领先一步。因此,如果野兔落后 3 步,那么下一次迭代需要两跳,乌龟需要一跳,现在野兔落后 2 步。在下一次迭代之后,野兔落后 1 跳,然后它正好赶上。如果野兔跳了 3 跳,而乌龟跳了 1,那么它可以跳过,因为它每次都会增加 2,但由于它每次迭代只增加 1,它不能跳过。
L
Laurel

优于弗洛伊德的算法

Richard Brent 描述了一个 alternative cycle detection algorithm,它很像兔子和乌龟 [弗洛伊德循环],除了这里的慢节点不移动,而是稍后以固定的时间间隔“传送”到快节点的位置.

说明可在 Brent's Cycle Detection Algorithm (The Teleporting Turtle) 获得。布伦特声称他的算法比弗洛伊德循环算法快 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;
}

这个答案太棒了!
真的很喜欢您的回答,将其包含在我的博客中 - k2code.blogspot.in/2010/04/…
为什么需要检查 slow.next != null?据我所知,slow 始终落后于或等于 fast
很久以前,当我开始学习算法时,我就这样做了。编辑了代码。谢谢 :)
m
meriton

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

当 loop 指向除 first 以外的任何节点时,反向是否正常工作?如果初始链表是这样的 1->2->3->4->5->2(循环从 5 到 2),那么反向链表看起来像 1->2<-3<-4 <-5 ?如果反过来,最终重建的名单会被搞砸吗?
@Zenil:这就是我写最后一个测试用例的原因,其中最后一个节点指向列表的中间。如果重建不起作用,则该测试将失败。关于您的示例:1->2->3->5->2 的反转将是 1->2->5->4->3->2,因为循环仅在列表末尾停止已经到达,而不是在到达循环结束时(我们无法轻易检测到)。
M
Mikesname

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 是否为空。此外,由于海龟总是在后面,您不必检查它是否为空 - 野兔已经这样做了。)


N
Neil

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

当快与慢在 p 点相遇时,

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

慢速行驶的距离 = a+b

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

所以当另一个慢指针再次从head运行到q时,同时快指针也会从p运行到q,所以它们在q点相遇。

https://i.stack.imgur.com/rbtDK.png

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

一张图片胜过千言万语。感谢您简洁明了的解释!
网上最好的解释。只是补充一点,这证明了快速和慢速指针在线性时间后收敛
如果 a 大于循环长度,则 fast 将进行多个循环,公式 distance (fast) = a + b + b + c 将更改为 a + (b+c) * k + b 引入额外的参数 k,该参数计算由 fast one 生成的 lopps 数。
C
Community

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

K
Khaled.K

算法

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) 是多少?
@user3543449 你是对的,它应该只是 n,已修复
这实际上是时间 ~ O(n^2),因为每个包含检查 ArrayList 需要 O(n) 并且其中有 O(n)。使用 HashSet 代替线性时间。
这不测试循环,而是测试使用元素 equalshashCode 的重复值。这不是一回事。它在最后一个元素上取消引用 null。并且问题没有说明将节点存储在 LinkedList 中。
@Lii 这是一个伪代码,不是 Java 代码,这就是为什么我用算法命名它
S
Sparky

以下可能不是最好的方法——它是 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()?
@JethroLarson:链表中的最后一个节点指向一个已知地址(在许多实现中,这是 NULL)。到达该已知地址时终止 for 循环。
s
smessing
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 次查找。


是的,具有某种 Set 的解决方案可以正常工作,但需要与列表大小成比例的空间。
s
smessing

如果我们被允许嵌入类 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;
        ....
    }

}

E
Eduardo

您甚至可以在恒定的 O(1) 时间内完成此操作(尽管它不会非常快速或高效):您的计算机内存可以容纳的节点数量有限,例如 N 条记录。如果您遍历 N 条以上的记录,那么您就有了一个循环。


这不是 O(1),这个算法在大 O 表示法中没有有意义的时间复杂度。大 O 表示法仅告诉您当输入大小变为无穷大时的极限性能。因此,如果您的算法建立在这样一个假设之上:对于某个大的 N,没有超过 N 个元素的列表,则当列表大小接近无穷大时,运行时的限制是未定义的。因此,复杂性不是“O(anything)”。
H
Habib Karbasian

这是我的可运行代码。

我所做的是通过使用三个跟踪链接的临时节点(空间复杂度 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));
    }
}

R
Richa
 // 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;
        }
    }
}

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


与我上面的答案几乎相同,但有一个问题。对于具有奇数长度列表(没有循环)的列表,它将引发 NullPointerException。例如,如果 head.next 为 null,则 sec.next.next 将抛出 NPE。
A
Abhinav

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

当您从头开始遍历列表时,创建一个排序的地址列表。当您插入一个新地址时,检查该地址是否已经在排序列表中,这需要 O(logN) 复杂度。


这种方法的复杂度是 O(N log N)
创建一个带有插入的排序列表将花费 O(log2n) 来使用二进制搜索识别插入点,对于 n 次插入,将花费 O(nlog2(n)) 最坏的情况,但插入操作本身会导致最大 n-1 个移位,即 O (n)。
因此,对于“n”个插入中的每个插入点,“n”的插入移位将导致 O(n^2) 二次时间的时间复杂度,而与搜索插入点 O(log2(n)) 无关。
在插入时创建排序数组将具有 O(n*n) 或 O(n^2) 时间复杂度。除了使用二分查找 O(log2(n)) 来获取插入点之外,还可以简单地使用线性查找 O(n) 因为在使用二分查找时 big-O 仍然是 O(n^2),因为 max n可以发生转变。
@mahee96,O(NlogN) 用于排序,O(N logN) 用于二进制搜索 N 次将导致 O(NlogN) 复杂度
T
TofuBeer

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

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

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


它肯定不可能花费固定的时间,因为列表的最后可能有一个循环,因此必须访问整个列表。但是,Fast/Slow 算法演示了使用固定内存量的解决方案。
它不是指它的渐近行为吗,即它是线性的 O(n),其中 n 是列表的长度。固定为 O(1)
A
Adit Ya

我可能非常迟到和新来处理这个线程。不过还是..

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

如果我们可以这样制表

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)

因此形成了一个循环。


您的解决方案没有通过“恒定空间量”的要求。
r
rai.skumar

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

Loop 可以通过在 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;  
  }  

这不符合问题中给出的恒定空间限制!
同意它有空间开销;这是解决这个问题的另一种方法。显而易见的方法是乌龟和缰绳算法。
@downvoter,如果您也能解释原因会很有帮助。
S
Sarthak Mehra

此代码经过优化,将比选择作为最佳答案的代码更快地产生结果。此代码可以避免进入一个非常漫长的追逐前向和后向节点指针的过程,如果我们遵循“最佳”,该过程将在以下情况下发生答案'方法。通过下面的试运行,你会明白我想说什么。然后通过下面给出的方法看问题并测量没有。为找到答案所采取的步骤。

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.
问题只是查找是否存在循环。在这种情况下,是的,这个问题绝对可以正常工作并获得所需的布尔结果。如果您想要循环开始的确切节点,那么我们将需要在代码中添加更多内容。但就产生结果而言,这将产生更快的结论。
您没有正确阅读问题:如果给定的 Node 是带有循环的列表的第一个,则编写 boolean hasLoop(Node first) 的最佳方法是什么,否则返回 false?
这是您列表的试运行。第一个值表示后向指针,第二部分表示前向指针。(1,1)-(1,3)-(2,3)-(2,5)-(3,5) -(3,7)-(4,7)-(4,4)。
实际上,我现在意识到有两种方法可以理解这个问题(或者至少我看到了两种不同的解释)。如果您只是在搜索是否存在循环,那么您的算法是正确的。但我认为问题是问循环从哪里开始。
I
Irshad ck

这是我在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;
}

x
xnorcode

您也可以使用上述答案中建议的弗洛伊德乌龟算法。

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

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

问候,

安德烈亚斯(@xnorcode)


v
vishwaraj

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

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 
        }

S
Sonu Mishra

// 链表查找循环函数

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

C
CausingUnderflowsEverywhere

如果链表结构实现了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;
}

R
RowanStone

我不确定这个答案是否适用于 Java,但我仍然认为它属于这里:

每当我们使用现代架构上的指针时,我们都可以期望它们是 CPU word aligned。对于 64 位架构,这意味着指针中的前 3 位始终为零。这让我们可以使用这个内存来标记我们已经看到的指针,通过将 1 写入它们的第一位。

如果我们遇到一个指针的第一位已经写入了 1,那么我们就成功地找到了一个循环,之后我们需要再次遍历该结构并将这些位屏蔽掉。完毕!

这种方法称为 pointer tagging,它在低级编程中被过度使用,例如 Haskell 将它用于某些 optimizations


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

}