我在采访中被问到这个问题:“如何检测链表中的循环?”,我解决了这个问题,但是面试官立刻问我如何删除链表中的循环。我摸索着。
所以关于如何解决这个问题的指针可能是伪代码还是方法定义?
我对Java很满意,所以我在java下标记了这个问题。
对于实例,此链接列表具有循环
0--->1---->2---->3---->4---->5---->6
▲ |
| ▼
11<—-22<—-12<—-9<—-8
答案 0 :(得分:61)
这个问题分为两部分:
一旦你知道循环的开始位置,很容易识别列表中的最后一个元素,因为它是循环开始后列表中的元素,最后指向循环的开始。然后将此元素的下一个指针/引用设置为null
以更正循环链接列表(不是循环链接列表,这是最后一个元素指向第一个元素的位置 - 这将是一个特定的实例)是微不足道的。循环列表)。
Floyd's cycle detect algorithm, also called the tortoise and hare algorithm因为它涉及使用以不同速度移动的两个指针/引用,是检测周期的一种方法。如果有一个循环,那么两个指针(比如p1
和p2
)将在有限步数之后指向同一个元素。有趣的是,可以证明它们遇到的元素与循环的开始相同的距离(继续以相同的向前方向遍历列表)作为开始循环是指列表的 head 。也就是说,如果列表的线性部分具有k
个元素,则两个指针将在循环开始点m
处的m-k
长度k
内循环或{{ 1}}元素到循环的'end'(当然,它是一个循环所以它没有'end' - 它只是'start'再一次)。这为我们提供了一种找到循环开始的方法:
一旦检测到一个周期,让p2
保持指向上面步骤的循环终止的元素,但重置p1
,使其指向列表的头部。现在,一次将每个指针移动一个元素。由于p2
在循环内部开始,它将继续循环。在k
步骤(等于循环开始距离列表头部的距离)后,p1
和p2
将再次相遇。这将为您提供循环开始的参考。
现在可以很容易地将p1
(或p2
)设置为指向开始循环的元素并遍历循环,直到p1
结束指向开始元件。此时p1
引用'last'元素列表,它的下一个指针可以设置为null
。
这里有一些快速而脏的Java代码,假设Node
的链接列表中Node
有一个next
引用。这可以优化,但它应该给你基本的想法:
Node slow, fast, start;
fast = slow = head;
//PART I - Detect if a loop exists
while (true)
{
// fast will always fall off the end of the list if it is linear
if (fast == null || fast.next == null)
{
// no loop
return;
}
else if (fast == slow || fast.next == slow)
{
// detected a loop
break;
}
else
{
fast = fast.next.next; // move 2 nodes at at time
slow = slow.next; // move 1 node at a time
}
}
//PART II - Identify the node that is the start of the loop
fast = head; //reset one of the references to head of list
//until both the references are one short of the common element which is the start of the loop
while(fast.next != slow.next)
{
fast = fast.next;
slow = slow.next;
}
start = fast.next;
//PART III - Eliminate the loop by setting the 'next' pointer
//of the last element to null
fast = start;
while(fast.next != start)
{
fast = fast.next;
}
fast.next = null; //break the loop
This explanation可能有助于第二部分背后的原因:
假设周期长度为M, 和其余的长度 链表是L.让我们搞清楚 什么是周期中的位置 t1 / t2第一次见面?
定义循环中的第一个节点 位置0,跟随我们的链接 有位置1,2,......,高达M-1。 ( 当我们走进循环,我们当前 position是(walk_length)mod M, 对吗?)假设t1 / t2第一次见面 位置p,那么他们的旅行时间是 同样,(L + k1 * M + p)/ v =(L + k2 * M + p)/ 2v 对于一些k1
所以它得出结论,如果t1从 p,t2从头开始并向前移动 同样的速度,然后才会受到满足 在位置0,第一个节点 周期。 QED。
更多参考资料:
答案 1 :(得分:15)
解决方案1 - 由Career Cup and "Cracking the Coding Interview" book提供:
public static LinkedListNode findStartOfLoop(LinkedListNode head) {
LinkedListNode n1 = head;
LinkedListNode n2 = head;
// find meeting point using Tortoise and Hare algorithm
// this is just Floyd's cycle detection algorithm
while (n2.next != null) {
n1 = n1.next;
n2 = n2.next.next;
if (n1 == n2) {
break;
}
}
// Error check - there is no meeting point, and therefore no loop
if (n2.next == null) {
return null;
}
/* Move n1 to Head. Keep n2 at Meeting Point. Each are k steps
/* from the Loop Start. If they move at the same pace, they must
* meet at Loop Start. */
n1 = head;
while (n1 != n2) {
n1 = n1.next;
n2 = n2.next;
}
// Now n2 points to the start of the loop.
return n2;
}
这个解决方案的解释直接来自本书:
如果我们移动两个指针,一个用 速度1和速度2的另一个,他们 如果链接,将结束会议 列表有一个循环。为什么?想想两个 汽车在赛道上行驶;更快的车 将永远通过较慢的一个!
这里棘手的部分是找到开始 循环。想象一下,作为一个类比, 两个人在赛道上比赛, 一个跑得快两倍 其他。如果他们从同一个开始 地方,他们什么时候下次见面?他们 接下来会在开始时见面 下一圈。
现在,让我们假设Fast Runner的开头为k米 一步之遥。他们什么时候下一个 遇到?他们之前会遇到k米 下一圈的开始。 (为什么?快 亚军会取得k + 2(n - k) 步骤,包括它的先声,和 慢跑者会成为n - k 步骤两者将是之前的k步 开始循环)。
现在,回到问题,当Fast Runner(n2)和 慢跑者(n1)正在我们身边移动 循环链表,n2将有一个 当n1时,开始循环 进入。具体来说,它将有一个 k的头部开始,其中k是数字 循环之前的节点。因为n2有 k个节点的头部起点,n1和n2 将在开始之前满足k个节点 循环。
所以,我们现在知道以下内容:
- Head是来自LoopStart的k个节点(根据定义)
- MeetingPoint for n1和n2是来自LoopStart的k个节点(如上所示)
醇>因此,如果我们将n1移回Head并在MeetingPoint保持n2,并以相同的速度移动它们,它们将在LoopStart会面
解决方案2 - 礼貌我:)。
public static LinkedListNode findHeadOfLoop(LinkedListNode head) {
int indexer = 0;
Map<LinkedListNode, Integer> map = new IdentityHashMap<LinkedListNode, Integer>();
map.put(head, indexer);
indexer++;
// start walking along the list while putting each node in the HashMap
// if we come to a node that is already in the list,
// then that node is the start of the cycle
LinkedListNode curr = head;
while (curr != null) {
if (map.containsKey(curr.next)) {
curr = curr.next;
break;
}
curr = curr.next;
map.put(curr, indexer);
indexer++;
}
return curr;
}
我希望这会有所帮助 赫里斯托斯
答案 2 :(得分:6)
这种反应并不是为了争夺答案,而是为了解释乌龟和兔子算法中两个节点的会议。
两个节点最终都会进入循环。因为一个比另一个(S)移动得更快(F),所以(F)最终会绕圈(S)。
如果循环的开始位于列表的头部,则(F)必须在列表的头部回到(S)。这只是因为(F)的速度是2X(S);如果它是3倍,那么就不会是真的。这是正确的,因为当(S)完成半圈时(F)完成一圈,所以当(S)完成第一圈时,(F)完成两圈 - 并且在(S)回到循环开始时
如果循环的开始不在列表的头部,则在时间(S)进入循环时,(F)在循环中具有(k)节点的开头。因为(S)的速度一次只有一个节点,它将在循环开始时在(k)节点处满足(F) - 如,(k)在到达开始之前的更多步骤,而不是(k)步骤之后开始。如果(S)的速度不是1并且速度比在(F)和(S)之间不是2:1,则不是这样。
3.1。这是解释一下有点棘手的地方。我们可以同意(F)将继续研磨(S)直到它们最终相遇(见上面的1),但是为什么在循环开始的(k)节点处呢?考虑以下等式,其中M是节点的数量或循环的距离,k是起始点(F);方程表示(F)左边给定时间t的距离,以右边(S)行进的距离表示:
d_F(t)= 2 * d_S(t)+ k
因此,当(S)进入循环并且在循环中行进了0距离时,(F)仅行进了(k)距离。到时间d_S = M-k,d_F = 2M-k。因为考虑到M代表循环中单圈的总距离,我们还必须使用模数运算,任何整个M(无余数)的(F)和(S)的位置都是0.那么就... POSITION(或差异),这留下k(或更确切地说,-k)。
最后,(S)和(F)将在远离循环开始的位置(0-k)或(k)节点处相遇。
鉴于上面的[3],因为(k)表示头部开始(F),并且因为(F)已经行进了2倍距离(S)从列表头部进入循环, (k)也表示从列表开始的距离,然后表示循环的开始。
这里有点晚了,所以我希望我能有效地表达出来。让我知道其他情况,我会尝试更新我的回复。
答案 3 :(得分:5)
如果允许使用身份哈希映射(例如IdentityHashMap),这非常容易解决。但它确实使用了更多的空间。
遍历节点列表。对于遇到的每个节点,将其添加到身份映射。如果节点已经存在于身份映射中,则列表具有循环链接,并且已知此冲突之前的节点(在移动之前检查或保留“最后一个节点”) - 只需根据需要设置“下一个”打破这个循环。
遵循这个简单的方法应该是一个有趣的练习:代码留给读者练习。
快乐的编码。
答案 4 :(得分:3)
0--->1---->2---->3---->4---->5---->6
▲ |
| ▼
11<—-22<—-12<—-9<—-8
在链接列表的每个节点之后插入虚节点,并在插入之前检查下一个节点是否为哑。如果next旁边是dummy,则在该节点的下一个中插入null。
0-->D->1-->D->2-->D->3->D-->4->D-->5->D-->6
▲ |
/ ▼
11<—D<-22<—D<-12<—D<-9<—D<--8
Node(11)->next->next == D
Node(11)->next =null
答案 5 :(得分:0)
//Find a Loop in Linked List and remove link between node
public void findLoopInList() {
Node fastNode = head;
Node slowNode = head;
boolean isLoopExist = false;
while (slowNode != null && fastNode != null && fastNode.next != null) {
fastNode = fastNode.next.next;
slowNode = slowNode.next;
if (slowNode == fastNode) {
System.out.print("\n Loop Found");
isLoopExist = true;
break;
}
}
if (isLoopExist) {
slowNode = head;
Node prevNode = null;
while (slowNode != fastNode) {
prevNode = fastNode;
fastNode = fastNode.next;
slowNode = slowNode.next;
}
System.out.print("Loop Found Node : " + slowNode.mData);
prevNode.next = null; //Remove the Loop
}
}
:) GlbMP
答案 6 :(得分:-1)
要解决此问题,我们只计算节点个数(就这样)。 我敢打赌,到目前为止,您还没有在任何竞争网站上看到此解决方案,因为我是今天亲自完成的...
void removeTheLoop(Node *root)
{
std :: unordered_set < struct Node * > s;
if(root == NULL)
return ;
s.insert(root);
int before_size = s.size();
while(1)
{
if(root -> next == NULL)
return;
s.insert(root -> next);
if(before_size == s.size())
{
root -> next = NULL;
return;
}
before_size = s.size();
root = root -> next;
}
}
工作方式:
时间复杂度:O(n)...空间复杂度:O(n)