【发布时间】:2010-09-07 05:57:58
【问题描述】:
确定链表中是否有循环的最佳(停止)算法是什么?
[编辑] 分析时间和空间的渐近复杂度会很不错,因此可以更好地比较答案。
[编辑] 原始问题不是解决 outdegree > 1 的节点,但有一些讨论。这个问题更像是“在有向图中检测循环的最佳算法”。
【问题讨论】:
标签: algorithm data-structures linked-list
确定链表中是否有循环的最佳(停止)算法是什么?
[编辑] 分析时间和空间的渐近复杂度会很不错,因此可以更好地比较答案。
[编辑] 原始问题不是解决 outdegree > 1 的节点,但有一些讨论。这个问题更像是“在有向图中检测循环的最佳算法”。
【问题讨论】:
标签: algorithm data-structures linked-list
def has_cycle(head): 计数器 = 设置()
while head is not None:
if head in counter:
return True
else:
counter.add(head)
head = head.next
【讨论】:
这是一种使用哈希表(实际上只是一个列表)来保存指针地址的解决方案。
def hash_cycle(node):
hashl=[]
while(node):
if node in hashl:
return True
else:
hashl.append(node)
node=node.next
return False
【讨论】:
有两个指针遍历列表;让一个以两倍于另一个的速度迭代,并在每一步比较它们的位置。在我的脑海中,类似于:
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),尽你所能。
【讨论】:
取2个指针 *p 和 *q ,使用两个指针开始遍历链表“LL”:
1) 指针p每次都会删除前一个节点,指向下一个节点。
2) 指针 q 每次只会向前移动。
条件:
1) 指针 p 指向 null 而 q 指向某个节点:存在循环
2) 两个指针都指向null:没有循环
【讨论】:
“Hack”解决方案(应该在 C/C++ 中工作):
next指针的最后一位设为1。时间复杂度为 2n。看起来它没有使用任何时间变量。
【讨论】:
在链表的头部初始化了两个指针。一个指针每一步前进一次,另一个指针每一步前进两次。如果较快的指针再次遇到较慢的指针,则列表中存在循环。否则,如果更快的到达列表末尾,则没有循环。
下面的示例代码就是按照这个方案来实现的。较快的指针为 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 上找到。博客中讨论了一个比较多的问题:当列表中有循环/循环时,入口节点是什么?
【讨论】:
前提条件:跟踪列表大小(添加或删除节点时更新大小)。
循环检测:
在遍历列表大小时保留一个计数器。
如果计数器超出列表大小,则可能存在循环。
复杂度:O(n)
注意:计数器和列表大小的比较,以及列表大小的更新操作,必须是线程安全的。
【讨论】:
您必须访问每个节点才能确定这一点。这可以递归地完成。为了阻止您访问已经访问过的节点,您需要一个标志来表示“已经访问过”。这当然不会给你循环。因此,不要使用位标志,而是使用数字。从 1 开始。检查连接的节点,然后将它们标记为 2 并递归直到网络被覆盖。如果在检查节点时遇到比当前节点少一个以上的节点,那么您就有了一个循环。循环长度由差值给出。
【讨论】:
在这种情况下,OysterD 的代码将是最快的解决方案(顶点着色)
这真的让我感到惊讶。我的解决方案最多通过列表两次(如果最后一个节点链接到倒数第二个矿脉),并且在常见情况下(无循环)只会通过一次。没有散列,没有内存分配等。
是的。我注意到这个公式并不完美,并改写了它。我仍然相信一个聪明的散列可能会执行得更快——只差一点点。不过,我相信您的算法是最好的解决方案。
只是为了强调我的观点:vertec 着色用于检测现代垃圾收集器的依赖循环,因此它有一个非常真实的用例。他们大多使用位标志来执行着色。
【讨论】:
在这种情况下,OysterD 的代码将是最快的解决方案(顶点着色)
这真的让我感到惊讶。我的解决方案最多通过列表两次(如果最后一个节点链接到倒数第二个矿脉),并且在常见情况下(无循环)只会通过一次。没有散列,没有内存分配等。
【讨论】:
如果循环不指向起点,康拉德鲁道夫的算法将无法工作。以下列表将使其成为无限循环:1->2->3->2.
DrPizza 的算法绝对是必经之路。
【讨论】:
我想知道除了迭代之外是否还有其他方法 - 在您前进时填充一个数组,并检查当前节点是否已经存在于数组中......
【讨论】:
如何使用哈希表来存储已经看到的节点(您从列表的开头按顺序查看它们)?在实践中,您可以获得接近 O(N) 的结果。
否则,使用排序堆而不是哈希表将实现 O(N log(N))。
【讨论】: