【问题标题】:Cyclical Linked List Algorithm循环链表算法
【发布时间】:2011-03-10 05:45:15
【问题描述】:

我最近在一次工作面试中被要求开发一种算法,可以确定链表是否是循环的。由于它是一个链表,我们不知道它的大小。这是一个双向链表,每个节点都有“下一个”和“上一个”指针。一个节点可以连接到任何其他节点,也可以连接到自身。

我当时想出的唯一解决方案就是选择一个节点,并与链表的所有节点进行检查。面试官显然不喜欢这个想法,因为它不是最佳解决方案。有什么更好的方法?

【问题讨论】:

  • 你原来的解决方案不是一个解决方案,因为如果你在圆外拾取一个节点,你的解决方案将无限循环。它远非“最佳”。通常在面试中,一个糟糕的解决方案也是可以接受的,如果它是一个解决方案。
  • 您是要检查整个列表是否是循环的,还是列表中是否存在某个循环?
  • 我讨厌这样的面试问题。如果有人知道答案,那么它告诉你的只是他们知道答案。如果他们不知道“正确”的答案,他们应该如何找出答案?盲目的运气?呸。我听说过这个问题被问到“如何找到链表的中间节点?”,它与面试问题具有相同的解决方案和相同的问题。我认为这些被称为“啊哈!”关于 SO 播客过去几集的问题。
  • 我会说这是 SO 3001695 的副本,除了它可能与其他三个副本一样被关闭:SO 2338683SO 494830SO 34249。由于 SO 34249 是最早的,它“获胜”...

标签: c++ c linked-list data-structures


【解决方案1】:

一般的解决方案是让 2 个指针以不同的速率移动。如果列表的某些部分是循环的,它们最终将相等。类似这样的东西:

 function boolean hasLoop(Node startNode){
   Node slowNode = startNode;
   Node fastNode1 = startNode;
   Node fastNode2 = startNode;

   while (slowNode && fastNode1 = fastNode2.next() && fastNode2 = fastNode1.next()){
     if (slowNode == fastNode1 || slowNode == fastNode2) 
        return true;

     slowNode = slowNode.next();
   }
   return false;
 }

从这里公然盗取:http://ostermiller.org/find_loop_singly_linked_list.html

【讨论】:

    【解决方案2】:

    您可以像这样处理一般的完整循环列表:通过第一个元素循环遍历链表,直到到达列表末尾或返回到第一个元素。

    但是,如果您想处理列表的一部分是循环的情况,那么您还需要定期向前移动您的第一个指针。

    【讨论】:

    • 如果列表包含一个不是从第一个元素开始的循环,您将不会回到第一个元素。
    • @Mike:是的,看第二部分,也许你在我还在写的时候写了这个。
    • 在一维链表中,要么整个链表是环,要么链表不包含环。
    • @JohnMcG:同意,但我认为这不是海报的意图。我认为他们想检测 A->B->C->D->C->D->C->D->C->D...
    • @Brian R. Bondy:一致的双向链表不能(明智地)包含这样的循环,因为 C 不能有两个先前的指针,一个指向 B,一个指向 D。
    【解决方案3】:

    从两个指向同一个元素的指针开始。跟随next 指针在列表中移动一个指针。另一个在previous 指针之后遍历列表。如果两个指针相遇,则列表是循环的。如果您发现一个元素的 previousnext 指针设置为 NULL,那么您知道该列表不是循环的。

    【讨论】:

    • 这不起作用。列表中的某处可能有一个循环,该算法无法找到该循环。
    • 如果您在循环中执行此操作,请不要在每次迭代时移动两个指针。如果你这样做了并且列表中的项目数是奇数,那么在你检查它们是否相遇之前,这些指针可能会相互传递。
    • @Mike Daniels- OP 试图检测列表是否是循环的,而不是列表中是否存在循环(或者至少这就是我阅读问题的方式)。 @Scott Langham-谢谢,我应该在回答中澄清这一点。每次迭代只移动一个指针。两个相反方向移动的原因是为了更快地找到列表的末尾(如果它不是圆形的)。
    • @Scott,试图改进这一点没有用。如果我有一个链表 A->B->C->D->B,并且我将两个指针都从 A 开始,那么我已经搞砸了。
    • 在一维链表中,要么整个链表是环,要么链表不包含环。没有循环的子列表或循环之外的列表中的节点之类的东西。如果是图表,那就另当别论了。
    【解决方案4】:

    您正在寻找的是循环查找算法。 Joel 所指的算法称为“龟兔赛跑”算法或 Floyd 的循环查找算法。我更喜欢第二种,因为它听起来像是一个很好的 D&D 咒语。

    Wikpedia overview of cycle finding algorithms,附示例代码

    【讨论】:

    • Stepanov 在他的编程元素中给出了一个非常易读的工作。
    【解决方案5】:

    [编辑问题和主题已被改写,以澄清我们正在检查双向链表中的循环,而不是检查双向链表是否仅仅是循环,因此本文的部分内容可能无关紧要。]

    它是一个包含每个节点的双向链表 具有“下一个”和“上一个”指针。

    双向链表通常实现为链表的头部和尾部指向 NULL 以指示它们的结束位置。

    [编辑] 正如所指出的,这只检查列表是否作为一个整体是循环的,而不是它是否有循环,但这是原始问题的措辞。 如果列表是循环的,tail->next == head 和/或 head->prev == tail。如果您不能同时访问尾节点和头节点,并且只有其中一个节点但不能同时访问,那么只需检查 head->prev != NULL 或 tail->next != NULL 就足够了。

    如果这不是一个足够的答案,因为我们只得到了一些随机节点[并在列表中的任何地方寻找循环],那么您所要做的就是获取这个随机节点并继续遍历列表直到您到达匹配的节点(在这种情况下是循环的)或空指针(在这种情况下不是)。

    但是,这与您已经提供的面试官不喜欢的答案基本相同。我很确定,如果没有一些神奇的技巧,如果没有线性复杂度算法,就无法检测链表中的循环。

    [Edit] 现在我的思路已经转换,专注于检测列表中的循环,而不是确定链表是否是循环的。

    如果我们遇到这样的情况: 123[2]

    我认为我们可以检测循环的唯一方法是跟踪到目前为止我们遍历的所有元素并在此过程中寻找任何匹配项。

    当然,这可能很便宜。如果我们被允许修改列表节点,我们可以为我们设置的每个节点保留一个简单的遍历标志。如果我们遇到一个已经设置了这个标志的节点,那么我们就找到了一个循环。但是,这不适用于并行性。

    这里提出了一个解决方案 [我从另一个答案中偷来的],称为“弗洛伊德的循环查找算法”。让我们看一下(经过修改以使我更容易阅读)。

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

    它基本上涉及使用 3 个迭代器而不是 1 个。我们可以看一下这样的情况:1->2->3->4->5->6->[2] 情况:

    首先,我们从 [1] 开始,使用一个快速迭代器到 [2],另一个在 [3] 或 [1, 2, 3]。当第一个迭代器与两个第二个迭代器中的任何一个匹配时,我们就会停止。

    我们继续[2, 4, 5](第一个快速迭代器遍历第二个快速迭代器的下一个节点,然后第二个快速迭代器遍历第一个快速迭代器的下一个节点)。然后是 [3, 6, 2],最后是 [4, 3, 4]。

    是的,我们找到了匹配项,因此确定列表包含 4 次迭代中的循环。

    【讨论】:

    • 我几乎肯定面试问题实际上是关于检测列表中是否有循环,不一定从第一个元素开始。
    • 这只捕获整个列表是循环的情况。通常面试官更喜欢完整的解决方案
    • @Joel 是的,但首先,我修改了帖子以处理列表仅部分循环并且其中包含循环的情况。其次,我们必须注意,OP 已经提供了我认为正确的答案,即继续线性遍历,直到我们找到相同的节点或列表的末尾。
    • @Joel 除非有比线性复杂度更快的解决方案,否则面试官拒绝这个答案的事实是否意味着他有一些关于我们可以使用的列表的额外上下文信息?
    • @stinky472 OP 的解决方案是将每个节点与每个其他节点进行比较——这是二次的,而不是线性的。如果他只比较单个节点,他会错过列表中更靠后的循环。
    【解决方案6】:

    保留指针值的散列。每次访问一个节点时,对它的指针进行哈希处理并存储它。如果您曾经访问过一个已经存储的列表,您就会知道您的列表是循环的。

    如果您的哈希表是常数,这是一个 O(n) 算法。

    【讨论】:

    • 我认为它是 O(n) 即使你的哈希表自动扩展。
    【解决方案7】:

    另一个选择是,由于列表是双向链接的,您可以遍历列表并检查下一个指针是否始终是当前节点或 null 而不是头。这里的想法是循环必须包含整个列表或看起来像这样:

     - -*- \
         \  \
          \---
    

    在节点 * 有 2 个传入链接,其中只有一个可以是前一个。

    类似:

     bool hasCycle(Node head){
        if( head->next == head ) return true;
    
        Node current = head -> next;
    
        while( current != null && current->next != null ) {
             if( current == head || current->next->prev != current )
                return true;
    
             current = current->next;
        }
        return false; // since I've reached the end there can't be a cycle.
     }
    

    【讨论】:

    • 这个想法+1,但是如果前两个节点不为空,则编写的代码总是返回true。
    • @Dave L.:很好。我认为这可以解决它。
    【解决方案8】:

    假设有人说“这里是指向列表成员的指针。它是循环列表的成员吗?”然后您可以检查列表的一个方向上的所有可访问成员,以查找指向您在其指针中应指向远离您的指针中的一个节点的指针。如果您决定朝 next 方向前进,那么您将寻找与您第一次获得的指针相等的 next 指针。如果您选择朝 prev 方向前进,那么您会寻找与您第一次获得的指针相等的 prev 指针。如果您在任一方向到达NULL 指针,那么您已经找到了终点并且知道它不是圆形的。

    您可以通过同时向两个方向走并查看是否撞到自己来扩展此功能,但它会变得更加复杂,并且实际上并不能为您节省任何东西。即使您在多核机器上使用 2 个线程来实现这一点,您也会处理共享易失性内存比较,这会降低性能。

    或者,如果您可以标记列表中的每个节点,您可以尝试通过在搜索结尾时查找标记来确定是否存在循环。如果你在一个节点中找到你的标记,你就会知道你以前去过那里。如果你在找到一个标记之前找到了一个终点,你就会知道它不是圆形的。但是,如果另一个线程同时尝试执行此操作,这将不起作用,因为您会混淆标记,但是如果其他线程在测试的同时重新排序列表,则其他实现将不起作用.

    【讨论】:

      【解决方案9】:

      您需要的是Floyd's cycle-finding algorithm。你也可以把寻找循环的交点当作作业。

      【讨论】:

        【解决方案10】:

        令人难以置信的是,复杂的解决方案可以传播到如此广泛的范围内。

        这是判断链表是否为循环的绝对最小值:

        bool is_circular(node* head)
        {
            node* p = head;
        
            while (p != nullptr) {
                p = p->next;
                if (p == head)
                    return true;
            }
        
            return false;
        }
        

        【讨论】:

        • 如果循环开始于列表头部以外的某个位置,则这是一个无限循环。
        【解决方案11】:

        这是一种基于 Floyd 算法的干净的方法来测试链表中是否有循环(如果它是循环的):

        int HasCycle(Node* head)
        {
            Node *p1 = head;
            Node *p2 = head;
        
          while (p1 && p2) { 
                p1 = p1->next;
                p2 = p2->next->next;
        
                if (p1 == p2)
                    return 1;
            }
            return 0;
        }
        

        这个想法是使用两个指针,都从head 开始,以不同的速度前进。如果它们相遇,这就是我们的列表中存在循环的线索,如果没有,则列表是无循环的。

        【讨论】:

          猜你喜欢
          • 2012-06-18
          • 2011-11-15
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2016-09-07
          • 2010-09-07
          • 2016-01-10
          相关资源
          最近更新 更多