访谈:删除链表中的循环 - Java

时间:2011-04-09 19:29:01

标签: java data-structures linked-list

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

所以关于如何解决这个问题的指针可能是伪代码还是方法定义?

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

对于实例,此链接列表具有循环

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

7 个答案:

答案 0 :(得分:61)

这个问题分为两部分:

  1. 检测列表中是否有循环
  2. 确定循环的开始
  3. 一旦你知道循环的开始位置,很容易识别列表中的最后一个元素,因为它是循环开始后列表中的元素,最后指向循环的开始。然后将此元素的下一个指针/引用设置为null以更正循环链接列表(不是循环链接列表,这是最后一个元素指向第一个元素的位置 - 这将是一个特定的实例)是微不足道的。循环列表)。

    1. Floyd's cycle detect algorithm, also called the tortoise and hare algorithm因为它涉及使用以不同速度移动的两个指针/引用,是检测周期的一种方法。如果有一个循环,那么两个指针(比如p1p2)将在有限步数之后指向同一个元素。有趣的是,可以证明它们遇到的元素循环的开始相同的距离(继续以相同的向前方向遍历列表)作为开始循环是指列表的 head 。也就是说,如果列表的线性部分具有k个元素,则两个指针将在循环开始点m处的m-k长度k内循环或{{ 1}}元素到循环的'end'(当然,它是一个循环所以它没有'end' - 它只是'start'再一次)。这为我们提供了一种找到循环开始的方法:

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

    3. 现在可以很容易地将p1(或p2)设置为指向开始循环的元素并遍历循环,直到p1结束指向开始元件。此时p1引用'last'元素列表,它的下一个指针可以设置为null


    4. 这里有一些快速而脏的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个节点   循环。

     

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

     
      
  1. Head是来自LoopStart的k个节点(根据定义)
  2.   
  3. MeetingPoint for n1和n2是来自LoopStart的k个节点(如上所示)
  4.         

    因此,如果我们将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)

这种反应并不是为了争夺答案,而是为了解释乌龟和兔子算法中两个节点的会议。

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

  2. 如果循环的开始位于列表的头部,则(F)必须在列表的头部回到(S)。这只是因为(F)的速度是2X(S);如果它是3倍,那么就不会是真的。这是正确的,因为当(S)完成半圈时(F)完成一圈,所以当(S)完成第一圈时,(F)完成两圈 - 并且在(S)回到循环开始时

  3. 如果循环的开始不在列表的头部,则在时间(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)节点处相遇。

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

  5. 这里有点晚了,所以我希望我能有效地表达出来。让我知道其他情况,我会尝试更新我的回复。

答案 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)

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

如果您认为独特,请予以支持。