最近在一次工作面试中,我被要求开发一个算法来确定一个链表是否是循环的。由于它是一个链表,我们不知道它的大小。它是一个双向链表,每个节点都有“next”和“previous”指针。一个节点可以连接到任何其他节点,也可以连接到自身。
那时我想到的唯一解决方案是选择一个节点并将其与链表中的所有节点进行检查。显然,面试官并不喜欢这个想法,因为它不是一个最优解。有什么更好的方法吗?
最近在一次工作面试中,我被要求开发一个算法来确定一个链表是否是循环的。由于它是一个链表,我们不知道它的大小。它是一个双向链表,每个节点都有“next”和“previous”指针。一个节点可以连接到任何其他节点,也可以连接到自身。
那时我想到的唯一解决方案是选择一个节点并将其与链表中的所有节点进行检查。显然,面试官并不喜欢这个想法,因为它不是一个最优解。有什么更好的方法吗?
一般的解决方案是使用两个移动速度不同的指针。如果链表中存在循环部分,它们最终会相等。代码大致如下:
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
保留指针值的哈希表。每次访问一个节点时,对其指针进行哈希并存储。如果你发现已经存储了这个节点,那么你就知道你的列表是循环的。
如果你的哈希表是常数级别的,那么这是一个O(n)算法。
另一个选项是,由于该列表是双向链接的,您可以遍历该列表并检查下一个指针的前一个是否始终为当前节点或null而不是头部。这里的想法是循环必须要么包含整个列表,要么看起来像这样:
- -*- \
\ \
\---
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.
}
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
开始,以不同的速度前进。如果它们相遇,那就是我们的线索,说明列表中存在一个循环;否则,该列表没有循环。
这是一个双向链表,每个节点都有“next”和“previous”指针。
双向链表通常使用指向NULL的头部和尾部来实现,以表示它们的结束位置。
[编辑]正如指出的那样,这只检查整个列表是否为循环,而不是其中是否存在循环,但这是原始问题的措辞。如果列表是循环的,则tail->next == head和/或head->prev == tail。如果您没有访问尾部和头部节点,而只有其中一个,则仅检查head->prev!= NULL或tail->next!= NULL即可。
如果这不是一个充分的答案,因为我们只给了一些随机节点[并且正在寻找列表中的任何地方的循环],那么您所要做的就是取这个随机节点并继续遍历列表,直到达到匹配的节点(在这种情况下它是循环的)或空指针(在这种情况下它不是)。
然而,这本质上与您已经提供的答案相同,但面试官不喜欢。我非常确定,除非有一些神奇的黑客,否则没有办法在提供随机节点的情况下检测链表中的循环,而不使用线性复杂度算法。
[编辑] 我现在已经转换思路,专注于检测列表中的循环,而不是确定链表是否为循环。
如果我们有这样一个情况:
1<->2<->3<->[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次迭代中包含一个循环。
next
指针的方向前进。另一个指针则按照previous
指针的方向遍历链表。如果两个指针相遇,则说明链表是循环的。如果你发现一个元素的previous
或next
指针设置为NULL
,那么你就知道该链表不是循环的。next指针。如果您选择向prev方向前进,则寻找与您首先给出的指针相等的prev指针。如果您在任一方向上到达了NULL指针,则已经找到了末尾,并知道它不是循环的。
你可以通过同时向两个方向走并看看是否会撞到自己来扩展这一点,但它变得更加复杂,而且真的没有节省任何东西。即使您在多核机器上使用2个线程实现此操作,您也将处理共享易失性内存比较,这将影响性能。
或者,如果您可以标记列表中的每个节点,您可以尝试在搜索结束时查找标记以确定是否存在循环。如果您在节点中找到了自己的标记,则知道您之前已经到过那里。如果您在找到其中一个标记之前找到了结束节点,则知道它不是循环的。但是,如果另一个线程同时尝试执行此操作,则此方法将无法正常工作,因为您的标记会混淆。而另一种实现方式如果在测试期间有其他线程正在重新排序列表,则也无法正常工作。