【问题标题】:Best algorithm to test if a linked list has a cycle测试链表是否有循环的最佳算法
【发布时间】:2010-09-07 05:57:58
【问题描述】:

确定链表中是否有循环的最佳(停止)算法是什么?

[编辑] 分析时间和空间的渐近复杂度会很不错,因此可以更好地比较答案。

[编辑] 原始问题不是解决 outdegree > 1 的节点,但有一些讨论。这个问题更像是“在有向图中检测循环的最佳算法”。

【问题讨论】:

标签: algorithm data-structures linked-list


【解决方案1】:

def has_cycle(head): 计数器 = 设置()

while head is not None:
    if head in counter:
        return True
    else:
        counter.add(head)
        head = head.next

【讨论】:

    【解决方案2】:

    这是一种使用哈希表(实际上只是一个列表)来保存指针地址的解决方案。

    def hash_cycle(node):
        hashl=[]
        while(node):
            if node in hashl:
                return True
            else:
                hashl.append(node)
                node=node.next
        return False
    

    【讨论】:

      【解决方案3】:

      有两个指针遍历列表;让一个以两倍于另一个的速度迭代,并在每一步比较它们的位置。在我的脑海中,类似于:

      node* tortoise(begin), * hare(begin);
      while(hare = hare->next)
      {
          if(hare == tortoise) { throw std::logic_error("There's a cycle"); }
          hare = hare->next;
          if(hare == tortoise) { throw std::logic_error("There's a cycle"); }
          tortoise = tortoise->next;
      }
      

      O(n),尽你所能。

      【讨论】:

      • 糟糕,您实际上有一个错误。 while 循环应该是 `while (hare && hare = hare->next) 否则你可以立即迭代。
      • 你不需要 while(hare && hare = hare->next)。唯一可以保护您的是列表为空(root 为空)。否则,定义的 while 循环将在 hare->next 返回 null 时立即终止。
      • @Herms,hare 在循环体中设置为 hare->next,因此此时 hare 可能为 null。
      • Stepanov & McJones 在 Elements of Programming 中对此主题进行了非常详细的处理。第 2 章,您可以在此处阅读,elementsofprogramming.com/book.html 涵盖了这一点。
      • 顺便说一句,“不经意间”可能暗示某人您发明了此算法,误导人们将其称为“DrPizza 算法”(!)。让我们在应得的地方给予信任。这是弗洛伊德早在 1967 年就发表的:en.wikipedia.org/wiki/Cycle_detection#Tortoise_and_hare
      【解决方案4】:

      取2个指针 *p 和 *q ,使用两个指针开始遍历链表“LL”:

      1) 指针p每次都会删除前一个节点,指向下一个节点。

      2) 指针 q 每次只会向前移动。

      条件:

      1) 指针 p 指向 null 而 q 指向某个节点:存在循环

      2) 两个指针都指向null:没有循环

      【讨论】:

        【解决方案5】:

        “Hack”解决方案(应该在 C/C++ 中工作):

        • 遍历链表,将next指针的最后一位设为1。
        • 如果找到带有标记指针的元素 -- 返回 true 和循环的第一个元素。
        • 在返回之前,将指针重置回来,尽管我相信即使使用标记的指针也可以解除引用。

        时间复杂度为 2n。看起来它没有使用任何时间变量。

        【讨论】:

          【解决方案6】:

          在链表的头部初始化了两个指针。一个指针每一步前进一次,另一个指针每一步前进两次。如果较快的指针再次遇到较慢的指针,则列表中存在循环。否则,如果更快的到达列表末尾,则没有循环。

          下面的示例代码就是按照这个方案来实现的。较快的指针为 pFast,较慢的为 pSlow。

          bool HasLoop(ListNode* pHead)
          {
              if(pHead == NULL)
                  return false;
          
          
              ListNode* pSlow = pHead->m_pNext;
              if(pSlow == NULL)
                  return false;
          
          
              ListNode* pFast = pSlow->m_pNext;
              while(pFast != NULL && pSlow != NULL)
              {
                  if(pFast == pSlow)
                      return true;
          
          
                  pSlow = pSlow->m_pNext;
          
          
                  pFast = pFast->m_pNext;
                  if(pFast != NULL)
                      pFast = pFast->m_pNext;
              }
          
          
              return false;
          }
          

          此解决方案可在my blog 上找到。博客中讨论了一个比较多的问题:当列表中有循环/循环时,入口节点是什么?

          【讨论】:

            【解决方案7】:

            前提条件:跟踪列表大小(添加或删除节点时更新大小)。

            循环检测:

            1. 在遍历列表大小时保留一个计数器。

            2. 如果计数器超出列表大小,则可能存在循环。

            复杂度:O(n)

            注意:计数器和列表大小的比较,以及列表大小的更新操作,必须是线程安全的。

            【讨论】:

              【解决方案8】:

              您必须访问每个节点才能确定这一点。这可以递归地完成。为了阻止您访问已经访问过的节点,您需要一个标志来表示“已经访问过”。这当然不会给你循环。因此,不要使用位标志,而是使用数字。从 1 开始。检查连接的节点,然后将它们标记为 2 并递归直到网络被覆盖。如果在检查节点时遇到比当前节点少一个以上的节点,那么您就有了一个循环。循环长度由差值给出。

              【讨论】:

                【解决方案9】:

                在这种情况下,OysterD 的代码将是最快的解决方案(顶点着色)

                这真的让我感到惊讶。我的解决方案最多通过列表两次(如果最后一个节点链接到倒数第二个矿脉),并且在常见情况下(无循环)只会通过一次。没有散​​列,没有内存分配等。

                是的。我注意到这个公式并不完美,并改写了它。我仍然相信一个聪明的散列可能会执行得更快——只差一点点。不过,我相信您的算法最好的解决方案。

                只是为了强调我的观点:vertec 着色用于检测现代垃圾收集器的依赖循环,因此它有一个非常真实的用例。他们大多使用位标志来执行着色。

                【讨论】:

                  【解决方案10】:

                  在这种情况下,OysterD 的代码将是最快的解决方案(顶点着色)

                  这真的让我感到惊讶。我的解决方案最多通过列表两次(如果最后一个节点链接到倒数第二个矿脉),并且在常见情况下(无循环)只会通过一次。没有散​​列,没有内存分配等。

                  【讨论】:

                    【解决方案11】:

                    如果循环不指向起点,康拉德鲁道夫的算法将无法工作。以下列表将使其成为无限循环:1->2->3->2.

                    DrPizza 的算法绝对是必经之路。

                    【讨论】:

                      【解决方案12】:

                      我想知道除了迭代之外是否还有其他方法 - 在您前进时填充一个数组,并检查当前节点是否已经存在于数组中......

                      【讨论】:

                        【解决方案13】:

                        如何使用哈希表来存储已经看到的节点(您从列表的开头按顺序查看它们)?在实践中,您可以获得接近 O(N) 的结果。

                        否则,使用排序堆而不是哈希表将实现 O(N log(N))。

                        【讨论】:

                        • 哈希表解决方案是 O(n) 空间。
                        猜你喜欢
                        • 1970-01-01
                        • 2012-06-18
                        • 2011-11-15
                        • 2010-11-09
                        • 2010-09-20
                        相关资源
                        最近更新 更多