面试:删除链表中的循环 - Java

Posted

技术标签:

【中文标题】面试:删除链表中的循环 - Java【英文标题】:Interview: Remove Loop in linked list - Java 【发布时间】:2011-08-02 05:06:37 【问题描述】:

我在面试中被问到这个问题:“如何检测链表中的循环?”,我解决了这个问题,但面试官立即问我如何删除链表中的循环。我摸不着头脑。

那么关于如何解决这个问题的任何指针,可能是伪代码或方法定义?

我对 Java 很熟悉,所以我在 java 下标记了这个问题。

例如这个链表有一个循环

 0--->1---->2---->3---->4---->5---->6
                  ▲                 |
                  |                 ▼
                 11<—-22<—-12<—-9<—-8

【问题讨论】:

你能定义什么是循环吗? @Enrique - 可能 OP 的意思是一个循环列表。 @Enrique : 编辑我的问题了解更多详情,请给我时间 与nomachetejuggling.com/2014/06/24/…密切相关 【参考方案1】:

这个问题有两个部分:

    检测列表中是否存在循环 确定循环的开始

一旦您知道循环从哪里开始,就很容易识别列表中的最后一个元素,因为它是列表中循环开始之后的元素,最终指向循环的开始。然后将此元素的下一个指针/引用设置为null 以更正循环链接列表(不是循环链接列表,这是最后一个元素指向第一个元素的位置 - 这将是循环列表的特定实例)是微不足道的)。

    Floyd's cycle detect algorithm, also called the tortoise and hare algorithm 因为它涉及使用以不同速度移动的两个指针/参考,是检测循环的一种方法。如果存在循环,则两个指针(比如p1p2)将在有限步数之后最终指向同一个元素。有趣的是,可以证明他们相遇的元素将与 loop 起点的距离相同(继续以相同的正向遍历列表)作为起点循环的 head 是列表的。也就是说,如果列表的线性部分具有k 元素,则两个指针将在长度为m 的循环内从循环开始处m-kk 元素到'end ' 的循环(当然,它是一个循环,所以它没有“结束” - 它只是再次“开始”)。这为我们提供了一种找到循环起点的方法:

    一旦检测到循环,让p2 继续指向上述步骤的循环终止的元素,但重置p1,使其重新指向列表的头部。现在,一次将每个指针移动一个元素。由于p2 在循环内部开始,它将继续循环。在k 步骤之后(等于循环开始到列表头部的距离),p1p2 将再次相遇。这将为您提供循环开始的参考。

    现在很容易将p1(或p2)设置为指向开始循环的元素并遍历循环,直到p1 最终指向起始元素。此时p1 正在引用“最后一个”元素列表,它的下一个指针可以设置为null


这里有一些快速而肮脏的 Java 代码,假设有一个 Nodes 的链表,其中 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。 ( 当我们在循环中行走时,我们当前的 位置是 (walk_length) mod M, 对吧?)假设 t1/t2 在 位置 p,那么他们的旅行时间是 同样,(L+k1*M+p)/v = (L+k2*M+p)/2v 对于一些k1

因此得出结论,如果 t1 从 p, t2 从头开始​​移动到 相同的速度,然后将受助人见面 在位置 0,第一个节点 循环。 QED。

更多参考资料:

http://www.quora.com/How-does-Floyds-cycle-finding-algorithm-work Explain how finding cycle start node in cycle linked list work? Proof of detecting the start of cycle in linked list Hristo's answer to this question on this page还引用了一本采访书的解释

【讨论】:

我真的很喜欢从您的回答中学习,感谢您的详尽以及链接。 我没有在“直到两个引用都短...”下得到这部分,因为它们现在以相同的速度移动,看起来fast.next 可能永远不会 i> 成为slow.next(他们永远在循环中互相追逐)。 @no.good.at.coding 但是如果他们没有在循环开始的地方相遇,那么他们将永远不会相遇。我不明白如何保证他们确实在那里见面。 我不确定您的k 是否保证正确,因为您无法确定兔子与乌龟在循环中的何处 @no.good.at.coding 是的,那是我缺少的部分。先生,为您 +1。【参考方案2】:

如果允许使用身份哈希映射(例如IdentityHashMap),这非常容易解决。不过,它确实会占用更多空间。

遍历节点列表。对于遇到的每个节点,将其添加到身份映射中。如果该节点已经存在于身份映射中,则该列表具有循环链接,并且在此冲突之前的节点是已知的(在移动之前检查或保留“最后一个节点”) - 只需根据需要设置“下一个”打破循环。

遵循这个简单的方法应该是一个有趣的练习:代码留给读者作为练习。

编码愉快。

【讨论】:

也许最终这将成为唯一的方法。但我只是不想太早放弃。 XD @Dante Jiang 这不是唯一的方法。 no.good.at.coding 是在某些事情上,他的方法可以适应。一旦检测到循环,继续运行兔子/头发,但这一次,如果小心的话,建立与兔子/头发第 2 次(每次)相遇的位置的 inverse 的列表为了确保兔子/头发不会在同一个地方相遇,这个新列表将更小并将包括循环节点,直到列表的循环长度为一(或二)。如果两个,从头走到这个循环被拦截(给出确切的节点),然后继续走到那个节点之前的节点...... 好吧,我错了。兔子/毛发周期检测的答案都有效。这是 where 的一个奇怪属性,如果在两者都从头开始时找到一个循环(尝试找出像我这样的反例;-),他们肯定会遇到。无论如何,上述解决方案仍然有效,即使不理想。 我们不能使用普通的 HashMap 吗?【参考方案3】:

解决方案 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 米 一个 n 步圈。他们下次什么时候 遇到?他们会在之前遇到公里 下一圈的开始。 (为什么?快 跑步者会做出 k + 2(n - k) 步骤,包括领先一步,以及 慢跑者会让 n - k 步数都将在前 k 步 循环开始)。

现在,回到问题,当 Fast Runner (n2) 和 慢跑者 (n1) 在我们周围移动 循环链表,n2 会有一个 当 n1 开始循环时 进入。具体来说,它将有一个 k 的开头,其中 k 是数字 循环前的节点数。由于 n2 有 k 个节点 n1 和 n2 的领先优势 将在开始之前遇到 k 个节点 循环。

所以,我们现在知道以下内容:

    Head 是 LoopStart 中的 k 个节点(根据定义) n1 和 n2 的MeetingPoint 是 LoopStart 的 k 个节点(如上所示)

因此,如果我们将 n1 移回 Head 并将 n2 保持在 MeetingPoint,并以相同的速度移动它们,它们将在 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;

我希望这会有所帮助。 赫里斯托

【讨论】:

我看到与 no.good.at.coding 相同的问题——“虽然 n1 不同于 n2”可能不会终止,因为不能保证 n1 和 n2 永远相等因为“n1 从头部开始”,但 n2“从兔子和头发在循环中相遇的地方开始”。如果他们没有在循环本身相遇,那么他们都会陷入循环中,以相同的速度互相追逐。由于周期的前导不同且周期长度不同,因此不确定是否可以保证距离 head -> cycleNode = distance meetingNode -> cycleNode。 但是,我未能提出反案,请帮忙! :p (请参阅 no.good.at.coding 的答案和链接,这解释了为什么我找不到反案;-)。这个答案也是+1。同样的道理。 我只是引用我阅读的采访书并将他们的解释粘贴到上面标记的解决方案1 您的解决方案 (2) 似乎仅在链表具有唯一项时才有效。 @Hristo - 您的方法依赖于列表项的唯一性,因此它无法真正解决循环或循环的存在。非唯一项 VALUES 的唯一真正唯一性是表示这些项的对象的内存地址,或者可能是包含该值的非原始对象的 ID。由于 Java 不允许您查看机器地址(我认为),因此您必须使用后者。这也是因为(我认为)Java 的 CONTAINS 方法使用类的 EQUALS 方法,该方法比较对象的哈希码而不是内存地址。【参考方案4】:

这个回复不是为了争夺答案,而是为了多解释一下龟兔算法中两个节点的相遇。

    两个节点最终都会进入循环。因为一个 (F) 比另一个 (S) 移动得更快,所以 (F) 最终会圈 (S)。

    如果循环的起点位于列表的头部,则 (F) 必须在列表头部与 (S) 相遇。这只是因为 (F) 的速度是 (S) 的 2X;如果它是 3X,那么这将是不正确的。这是真的,因为 (F) 在 (S) 跑完半圈时跑完一圈,所以当 (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);该方程用左边的时间 t 表示 (F) 所经过的距离,以右边的 (S) 所经过的距离表示:

    d_F(t) = 2 * d_S(t) + k

    因此,当 (S) 进入循环并在循环中行进 0 距离时,(F) 仅行进了 (k) 距离。到时间 d_S = M - k,d_F = 2M - k。因为我们还必须使用模块化数学,考虑到 M 表示循环中单圈的总距离,所以 (F) 和 (S) 在任何整个 M(无余数)处的 POSITION 为 0。那么就POSITION(或微分),这留下k(或者更确切地说,-k)。

    最后,(S) 和 (F) 将在 (0 - k) 或 (k) 个节点处相交。

    鉴于上面的 [3],因为 (k) 表示领先 (F) 已经开始,并且 (F) 已经行进了 2X 距离 (S) 从列表的头部进入循环, (k) 也代表距离列表起点的距离,然后代表循环的起点。

这里有点晚了,所以我希望我已经有效地表达了。否则请告诉我,我会尝试更新我的回复。

【讨论】:

Bitxwise.. 整洁,但要注意添加代码,例如方法定义? @SuperMan - no.good.at.coding 的答案包括一个代码示例,但他很难解释为什么该算法实际有效(即为什么 2 个节点保证在特定点相遇,这表明循环的开始)。我只是在龟/兔算法为什么/如何工作的原因上加上我的 2 美分。 no.good.at.coding 的代码示例肯定会更干净,也许我以后可以添加一个更干净的编码示例 - 但对于 no.good.at.coding 来说,他本人承认代码示例既快速又脏。跨度> 【参考方案5】:
 0--->1---->2---->3---->4---->5---->6
                  ▲                 |
                  |                 ▼
                 11<—-22<—-12<—-9<—-8  

在链表的每个节点之后插入虚拟节点,在插入之前检查下一个节点是否为虚拟节点。如果 next to next 是虚拟的,则在该节点的 next 中插入 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

【讨论】:

【参考方案6】:
//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

【讨论】:

【参考方案7】:

最简单独特的方式

为了解决这个问题,我们只计算节点数(就是这样)。 我敢打赌,到目前为止,您还没有在任何有竞争力的网站上看到过这个解决方案,因为我今天自己做到了......

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)

它只计算元素的数量。我们将在 c++ 中采用 unordered_set。 如果元素不存在于容器中,它会插入该元素并增加其大小。 现在,当节点指向已经添加的节点时,悬念就开始了,因此在这种情况下,大小不会增加,我们将在旁边设置为 NULL。

如果您认为它是独一无二的,请支持它。

【讨论】:

以上是关于面试:删除链表中的循环 - Java的主要内容,如果未能解决你的问题,请参考以下文章

图解精选 TOP 面试题 001 | LeetCode 237. 删除链表中的节点

链表经典面试题(含图解)

剑指Offer-代码的完整性面试题18.2:删除链表中的重复节点

#yyds干货盘点# 面试必刷TOP101:删除有序链表中重复的元素-I

Java面试官:java字符的asc码

剑指offer——面试题18.1:删除链表中重复的节点