我最近在求职面试中被问到开发一种算法,可以确定链表是否是周期性的。由于它是一个链表,我们不知道它的大小。这是一个双向链表,每个节点都有“下一个”和“前一个”指针。节点可以连接到任何其他节点,也可以连接到自身。
我当时提出的唯一解决方案是选择一个节点并与链表的所有节点一起检查。面试官显然不喜欢这个想法,因为它不是最佳解决方案。什么是更好的方法?
答案 0 :(得分:9)
您正在寻找的是一种循环寻找算法。 Joel所指的算法被称为“乌龟和野兔”算法或Floyd的循环发现算法。我更喜欢第二种,因为它听起来会成为一个很好的D& D咒语。
答案 1 :(得分:8)
一般的解决方案是让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 :(得分:2)
保持指针值的哈希值。每次访问节点时,都会散列其指针并存储它。如果你曾经访问过一个已经存储过的,你知道你的列表是循环的。
如果您的哈希表是常量,这是一个O(n)算法。
答案 3 :(得分:1)
另一种选择是,由于列表是双向链接的,因此您可以遍历列表并检查前一个指针是否始终是当前节点或者是否为空而不是头部。这里的想法是循环必须包含整个列表或看起来像这样:
- -*- \
\ \
\---
在Node *上有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.
}
答案 4 :(得分:0)
你可以像这样处理一般的完整循环列表:通过第一个元素遍历链表,直到你到达列表的末尾或直到你回到第一个元素。
但是如果你想处理列表的一部分是循环的情况,那么你还需要定期向前移动你的第一个指针。
答案 5 :(得分:0)
从指向同一元素的两个指针开始。按照next
指针在列表中走一个指针。另一个按照previous
指针遍历列表。如果两个指针相遇,那么列表是循环的。如果您找到previous
或next
指针设置为NULL
的元素,那么您知道列表不是循环。
答案 6 :(得分:0)
[编辑问题和主题已经改写,以澄清我们正在检查双向链表中的周期,而不是检查双向链表是否只是循环,因此这篇文章的部分内容可能无关紧要。]
它是每个节点的双重链接列表 有'下一个'和'前一个'指针。
双链表通常是在列表的头部和尾部指向NULL的情况下实现的,以指示它们的结束位置。
[编辑]正如所指出的,这仅检查列表是否为整体循环,而不是它是否有循环,但这是原始问题的措辞。 如果列表是循环的,则tail-> next == head和/或head-> prev == tail。如果您无法访问尾部和头部节点并且只能访问其中一个而不是两者,则只需检查head-> prev!= NULL或tail-> next!= NULL就足够了。
如果这不是一个足够的答案,因为我们只给了一些随机节点[并在列表中的任何地方查找周期],那么你所要做的就是采用这个随机节点并继续遍历列表直到你到达匹配的节点(在这种情况下它是圆形的)或空指针(在这种情况下它不是)。
然而,这与你已经提供的面试官不喜欢的答案基本相同。我很确定如果没有一些神奇的黑客,就没有办法在链表中检测一个循环,提供一个随机节点,没有线性复杂度算法。
[编辑]我的思绪现在已经转变,重点是检测列表中的周期,而不是确定链表是否为圆形。
如果我们有这样的案例: 1· - →; 2'; - > 3'; - > [2]
我能看到我们可以检测周期的唯一方法是跟踪我们到目前为止所经历的所有元素,并在此过程中寻找任何匹配。
当然这可能很便宜。如果我们允许修改列表节点,我们可以在我们设置的每个节点上保留一个简单遍历的标志。如果我们遇到已设置此标志的节点,那么我们找到了一个循环。但是,这对并行性来说效果不佳。
这里提出了一个解决方案[我从另一个答案中偷走了]称为“Floyd的循环寻找算法”。让我们来看一下(修改后让我更容易阅读)。
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次迭代的循环。
答案 7 :(得分:0)
假设有人说“这里是指向列表成员的指针。它是循环列表的成员吗?”那么你可以在列表的一个方向检查所有可到达的成员,以指向你的指针指向一个节点的一个节点的指针,指针应指向你。如果您决定进入 next 方向,那么您将查找与您首次给出的指针相等的next
指针。如果您选择进入 prev 方向,那么您将查找与您首次给出的指针相等的prev
指针。如果你到达任一方向的NULL
指针,那么你已经找到了结束并且知道它不是圆形的。
你可以通过同时向两个方向进行扩展,看看你是否碰到了自己,但它变得更加复杂,它确实无法为你节省任何东西。即使您在多核计算机上使用2个线程实现了这一点,您也会处理共享的易失性内存比较,这会导致性能下降。
或者,如果您可以标记列表中的每个节点,则可以在搜索结束时通过查找标记来尝试确定是否存在循环。如果你在一个节点中找到了你的标记,你就会知道你曾经去过那里。如果你在找到你的一个标记之前找到了结局,你会发现它不是圆形的。但是,另一个线程试图同时执行此操作不起作用,因为您会将您的标记混淆,但如果其他线程在测试的同时重新排序列表,则其他实现将无法工作
答案 8 :(得分:0)
您需要的是Floyd's cycle-finding algorithm。你也可以考虑找到循环的交叉点作为家庭作业。
答案 9 :(得分:0)
这是一个干净的方法来测试链接列表是否有基于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
开始,以不同的速度前进。如果他们彼此见面,我们的线索是我们的列表中有一个循环,如果没有,那么该列表是无循环的。
答案 10 :(得分:-1)
令人难以置信的是,复杂的解决方案有多广泛。
这是查找链表是否为循环所需的绝对最小值:
bool is_circular(node* head)
{
node* p = head;
while (p != nullptr) {
p = p->next;
if (p == head)
return true;
}
return false;
}