【问题标题】: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 循环的想法。顺便说一句,列表不应该是一个循环链表,如果它循环,它可能会指向中间的某个地方。

标签: algorithm loops linked-list cycle


【解决方案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@stackoverflow.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找到。

    【讨论】:

      猜你喜欢
      • 2015-06-18
      • 2016-09-05
      • 2015-09-14
      • 2017-04-08
      • 2020-11-23
      • 1970-01-01
      • 2020-10-17
      • 2020-07-22
      • 1970-01-01
      相关资源
      最近更新 更多