如何确定一个链表是不是有一个仅使用两个内存位置的循环

Posted

技术标签:

【中文标题】如何确定一个链表是不是有一个仅使用两个内存位置的循环【英文标题】:How to determine if a linked list has a cycle using only two memory locations如何确定一个链表是否有一个仅使用两个内存位置的循环 【发布时间】:2010-10-04 10:27:21 【问题描述】:

有没有人知道一种算法来查找链表是否在自身上循环,只使用两个变量来遍历链表。假设您有一个对象的链接列表,那么什么类型的对象都没有关系。我在一个变量中有一个指向链表头部的指针,并且只给了另一个变量来遍历列表。

所以我的计划是比较指针值,看看是否有任何指针相同。该列表的大小有限,但可能很大。我可以将两个变量都设置为头部,然后用另一个变量遍历列表,总是检查它是否等于另一个变量,但是,如果我确实遇到了一个循环,我将永远无法摆脱它。我认为这与遍历列表和比较指针值的不同速率有关。有什么想法吗?

【问题讨论】:

谢谢,Turtle and Rabbit 确实提供了一个很好的解决方案。从概念上讲,如果列表循环回到自身,我也喜欢兔子绕着 Turtle 循环的想法。顺便说一句,列表不应该是一个循环链表,如果它循环,它可能会指向中间的某个地方。 【参考方案1】:

我建议使用Floyd's Cycle-Finding Algorithm aka Tortoise and the Hare Algorithm。它具有 O(n) 复杂度,我认为它符合您的要求。

示例代码:

function boolean hasLoop(Node startNode)
  Node slowNode = Node fastNode1 = Node fastNode2 = startNode;
  while (slowNode && fastNode1 = fastNode2.next() && fastNode2 = fastNode1.next())
    if (slowNode == fastNode1 || slowNode == fastNode2) return true;
    slowNode = slowNode.next();
  
  return false;

有关***的更多信息:Floyd's cycle-finding algorithm。

【讨论】:

谢谢,这个使用了额外的节点变量。 是的,您可以轻松修改上面的代码,将 fastNode1 设置为 slowNode.next().next() :) 如果我们一次将fastNode 提前三个而不是两个会发生什么?难道我们不能检测到fastNode 已经交叉 slowNode。显然,相等性检查(我们目前用于检测这一点)不一定需要与三个的进步一起工作。你怎么看?这(一次跳更多步)会是一个更好的算法吗? @Lazer - 两个指针都像这样包裹的小循环存在风险 为什么复杂度是 o(n) ?找圈子和遍历到最后一个元素一样吗?【参考方案2】:

您可以使用Turtle and Rabbit 算法。

***也有解释,他们称之为“Floyd's cycle-finding algorithm”或“龟兔赛跑”

【讨论】:

我已将错误报告发送至 team@***.com ***终于解决了我多年来对这个算法的私人愚蠢怀疑。感谢您发布此链接。【参考方案3】:

当然。一种解决方案确实可以使用两个指针遍历列表,其中一个指针的移动速度是另一个指针的两倍。

从指向列表中任何位置的“慢”和“快”指针开始。运行遍历循环。如果“快”指针在任何时候都与慢指针重合,那么您就有了一个循环链表。

int *head = list.GetHead();
if (head != null) 
    int *fastPtr = head;
    int *slowPtr = head;

    bool isCircular = true;

    do 
    
        if (fastPtr->Next == null || fastPtr->Next->Next == null) //List end found
        
            isCircular = false;
            break;
        

        fastPtr = fastPtr->Next->Next;
        slowPtr = slowPtr->Next;
     while (fastPtr != slowPtr);

    //Do whatever you want with the 'isCircular' flag here

【讨论】:

如果 fastPtr 恰好位于循环顶部列表中的最后一个元素上,这会不会因指针错误而失败? 或者如果列表为空或1个元素长,则在fastPtr的初始分配上? 当列表没有循环且长度为奇数时,这不起作用,next->next 会给你一个空指针异常(或类似的东西)【参考方案4】:

我尝试自己解决这个问题,并找到了一个不同的(效率较低但仍然是最佳的)解决方案。

这个想法是基于在线性时间内反转一个单链表。这可以通过在迭代列表的每个步骤中进行两次交换来完成。如果 q 是前一个元素(最初为 null)而 p 是当前元素,则 swap(q,p->next) swap(p,q) 将反转链接并同时推进两个指针。可以使用 XOR 来完成交换,以防止不得不使用第三个内存位置。

如果列表有一个循环,那么在迭代期间的某一时刻,您将到达一个指针已更改的节点。你不知道是哪个节点,但是通过继续迭代,交换一些元素两次,你再次到达列表的头部。

通过两次反转列表,列表的结果保持不变,您可以根据您是否到达列表的原始头部来判断它是否有一个循环。

【讨论】:

由于这需要修改列表,我认为这是一个更糟糕的解决方案。有两个问题的例子:如果列表可能驻留在常量内存中(例如static const 结构或内存映射的只读文件),或者如果列表被多个线程使用(只要访问是只读,不需要锁定;锁定会变得非常慢和/或停止其他线程)。【参考方案5】:
int isListCircular(ListNode* head)
    if(head==NULL)
        return 0;
    ListNode *fast=head, *slow=head;
    while(fast && fast->next)
        if(fast->next->next==slow)
            return 1;
        fast=fast->next->next;
        slow=slow->next;
    
    return 0;

【讨论】:

【参考方案6】:
boolean findCircular(Node *head)

    Node *slower, * faster;
    slower = head;
    faster = head->next;
    while(true) 
        if ( !faster || !faster->next)
            return false;
        else if (faster == slower || faster->next == slower)
            return true;
        else
            faster = faster->next->next;
    

【讨论】:

不推荐仅使用代码的答案,尝试至少简要说明您所做的事情。【参考方案7】:

将此问题带到下一步将是识别循环(即,不仅循环存在,而且它在列表中的确切位置)。 Tortoise 和 Hare 算法可以用于相同的目的,但是,我们需要始终跟踪列表的头部。这个算法的说明可以在here找到。

【讨论】:

以上是关于如何确定一个链表是不是有一个仅使用两个内存位置的循环的主要内容,如果未能解决你的问题,请参考以下文章

检查两个链表是不是合并。如果有,在哪里?

加入内存时,LINQ 查询中的“位置”是不是重要?

无锁的原子函数改变两个独立的内存位置

LeetCode 剑指Offer II 029.排序的循环链表[链表 双指针] HERODING的LeetCode之路

Java每日一题——>剑指 Offer II 029. 排序的循环链表

Java每日一题——>剑指 Offer II 029. 排序的循环链表