web-dev-qa-db-fra.com

Entretien: Supprimer la boucle dans la liste chaînée - Java

On m'a posé cette question lors de l'entretien: "Comment détecter la boucle dans la liste chaînée?", J'ai résolu le problème, mais l'intervieweur m'a immédiatement demandé comment supprimer la boucle dans une liste chaînée. J'ai tâté.

Donc, tout indicateur sur la façon de résoudre ce problème peut être un pseudo-code ou une définition de méthode?

Je suis à l'aise avec Java alors j'ai tagué cette question sous Java.

Par exemple, cette liste liée a une boucle

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

Il y a deux parties à ce problème:

  1. Détecter s'il y a une boucle dans la liste
  2. Identifier le début de la boucle

Une fois que vous savez où la boucle commence, il est facile d’identifier le dernier élément de la liste car c’est l’élément de la liste suivant le début de la boucle qui renvoie au début de la boucle. Il est alors trivial de définir le pointeur/la référence suivante de cet élément sur null pour corriger la liste de liens cycliques (liste non circulaire reliée où les derniers éléments renvoient au premier - il s'agirait d'une instance spécifique de listes cycliques).

  1. L’algorithme de détection de cycle de Floyd, également appelé algorithme tortue et lièvre car il implique l’utilisation de deux pointeurs/références se déplaçant à des vitesses différentes, est un moyen de détecter le cycle. S'il y a un cycle, les deux pointeurs (disons p1 et p2) finiront par pointer vers le même élément après un nombre fini d'étapes. Fait intéressant, il peut être prouvé que l’élément auquel ils se rencontrent sera à la même distance du début de la boucle ( en continuant à parcourir la liste dans le même sens), le début de la boucle se dirigeant vers la tête de la liste . En d’autres termes, si la partie linéaire de la liste comporte k éléments, les deux pointeurs se rencontreront à l’intérieur de la boucle de longueur m en un point m-k à partir du début de la boucle ou k éléments jusqu’à la fin de la boucle (bien sûr, c’est une boucle, donc elle n’a pas de "fin" - c’est juste le "début" une fois de plus). Et cela nous permet de trouver le début de la boucle:

  2. Une fois qu'un cycle a été détecté, laissez p2 en continuant à pointer sur l'élément où la boucle de l'étape ci-dessus s'est terminée, mais réinitialisez p1 pour qu'il pointe en arrière de la liste. Maintenant, déplacez chaque pointeur un élément à la fois. Puisque p2 a commencé à l'intérieur de la boucle, la boucle continue. Après k pas (égaux à la distance du début de la boucle par rapport au début de la liste), p1 et p2 se reverront. Cela vous donnera une référence au début de la boucle.

  3. Il est maintenant facile de définir p1 (ou p2) pour qu'il pointe vers l'élément qui commence la boucle et la parcourt jusqu'à ce que p1 renvoie à l'élément de départ. À ce stade, p1 fait référence à la liste des "derniers" éléments et son pointeur suivant peut être défini sur null.


Voici un code Java rapide et compliqué, supposant une liste chaînée de Nodes où un Node a une référence next. Cela pourrait être optimisé mais cela devrait vous donner l’idée de base:

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

Cette explication pourrait aider le pourquoi derrière la partie II:

Supposons que la longueur du cycle est M et la longueur du reste de la liste liée est L. Voyons quelle est la position du cycle lorsque t1/t2 se rencontrent pour la première fois?

Définir le premier nœud du cycle est la position 0, en suivant les liens que nous avons les positions 1, 2, ..., jusqu’à M-1. (Lorsque nous marchons dans le cycle, notre position actuelle est (longueur_de_route) mod M, n'est-ce pas?) Supposons que t1/t2 se rencontrent d'abord à la position p, puis que leur temps de trajet est identique, (L + k1 * M + p)/v = (L + k2 * M + p)/2v pour certains k1

Il conclut donc que si t1 part de p, t2 part de tête et se déplace à la même vitesse, il sera alors autorisé à se réunir à la position 0, le premier nœud du cycle. QED.

Plus de références:

61

Solution 1 - avec l'aimable autorisation de Carrière et livre "Cracking the Coding Interview" :

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;
}

L'explication de cette solution provient directement du livre:

Si nous déplaçons deux pointeurs, un avec vitesse 1 et autre vitesse 2, ils finira par rencontrer si le lié la liste a une boucle. Pourquoi? Pensez à deux les voitures conduisant sur une piste; la voiture la plus rapide passera toujours le plus lent! 

La partie délicate ici consiste à trouver le début de la boucle. Imaginez, par analogie, deux personnes courant autour d'une piste, un qui court deux fois plus vite que le autre. S'ils commencent au même lieu, quand vont-ils se rencontrer la prochaine fois? Ils se réunira ensuite au début du prochain tour.

Supposons maintenant que Fast Runner ait une longueur d’avance de k mètres un tour en n-step. Quand seront-ils ensuite rencontrer? Ils rencontreront k mètres avant le début du tour suivant. (Pourquoi? Rapide Le coureur aurait fait k + 2 (n - k) Étapes, y compris son avance, et Le coureur lent aurait fait n - k Étapes. Les deux seront k étapes. avant le début de la boucle .__).

Revenons maintenant au problème, lorsque Fast Runner (n2) et Slow Runner (n1) se déplace autour de notre liste circulaire, n2 aura un tête sur la boucle quand n1 entre dans. Plus précisément, il aura un début de tête de k, où k est le nombre des nœuds avant la boucle. Depuis n2 a une longueur d'avance de k noeuds, n1 et n2 rencontrera k nœuds avant le début de la boucle.

Donc, nous savons maintenant ce qui suit: 

  1. Head est k nœuds de LoopStart (par définition) 
  2. MeetingPoint pour n1 et n2 est k nœuds de LoopStart (comme indiqué ci-dessus)

Ainsi, si nous retournons n1 à Head et maintenons n2 chez MeetingPoint et si nous les déplaçons au même rythme, ils se rencontreront à LoopStart.

Solution 2 - gracieuseté de moi :)

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;
}

J'espère que ça aide.
Hristo

14
Hristo

Cette réponse n'est pas destinée à entrer en compétition pour la réponse, mais plutôt à expliquer un peu plus sur la réunion des deux nœuds dans l'algorithme tortoise et l'algorithme du lièvre.

  1. Les deux nœuds finiront par entrer dans la boucle. Parce que l’un se déplace plus vite (F) que l’autre (S), (F) finira par tourner (S).

  2. Si le début de la boucle se trouve en tête de la liste, le point (F) doit se retrouver au dos de la tête (S). Ceci est UNIQUEMENT parce que la vitesse de (F) est de 2X (S); si c'était 3X cela ne serait alors pas vrai. Cela est vrai car (F) termine un tour lorsque (S) termine un demi-tour, donc lorsque (S) termine son premier tour, (F) a terminé deux tours - et revient au début de la boucle avec (S). .

  3. Si le début de la boucle N'EST PAS en tête de la liste, au moment où (S) entre dans la boucle, (F) a eu une longueur d'avance de (k) nœuds dans la boucle. Parce que la vitesse de (S) n’est qu’un nœud à la fois, il rencontrera (F) en (k) nœuds dès le début de la boucle - comme dans (k), plusieurs étapes avant d’atteindre le début, PAS (k) étapes APRÈS le début. Cela ne serait PAS vrai si la vitesse de (S) n'était pas une et si le rapport de vitesse n'était pas de 2: 1 entre (F) et (S).

    3.1. C'est là que ça devient un peu difficile à expliquer. Nous pouvons convenir que (F) continuera à roder (S) jusqu'à ce qu'ils se rencontrent (voir 1 ci-dessus), mais pourquoi à (k) nœuds à partir du début de la boucle? Considérons l'équation suivante où M est le nombre de nœuds ou la distance de la boucle et k est le début de la tête (F); l'équation représente la distance parcourue par (F) à partir du temps t à gauche en termes de distance parcourue par (S) à droite:

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

    Ainsi, lorsque (S) entre dans la boucle et a parcouru 0 km dans la boucle, (F) n’a parcouru que la distance (k). Au moment d_S = M - k, d_F = 2M - k. Étant donné que nous devons également utiliser les mathématiques modulaires en considérant que M représente la distance totale d’un tour dans la boucle, la position de (F) et de (S) pour tout M (aucun reste) est 0. Donc, en termes de POSITION (ou le différentiel), cela laisse k (ou plutôt, -k).

    Et donc enfin, (S) et (F) se rencontreront à la position (0 - k), ou (k) nœuds éloignés du début de la boucle.

  4. Étant donné [3] ci-dessus, étant donné que (k) représente la longueur d'avance (F) et que (F) a parcouru 2 fois la distance (S) parcourue pour entrer dans la boucle à partir de l'en-tête de la liste, distance du début de la liste, qui représente alors le début de la boucle.

Il est un peu tard, j'espère avoir bien articulé. Faites-moi savoir autrement et je vais essayer de mettre à jour ma réponse.

6
bitxwise

Si l’utilisation d’une carte de hachage d’identité (telle que IdentityHashMap ) est autorisée, elle est terriblement facile à résoudre. Cependant, il utilise plus d'espace.

Parcourez la liste des nœuds. Pour chaque nœud rencontré, ajoutez-le à la carte d'identité. Si le nœud existait déjà dans la carte d'identité, la liste possède un lien circulaire et le nœud qui existait avant ce conflit est connu (vérifiez avant de déplacer ou conservez le "dernier nœud") - il vous suffit de définir "suivant" de manière appropriée. rompre le cycle.

Suivre cette approche simple devrait être un exercice amusant: le code est laissé comme un exercice pour le lecteur.

Bonne codage.

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

Insérez un nœud factice après chaque nœud de la liste de liens et avant de l'insérer, vérifiez que le nœud suivant est factice ou non. Si next to next est dummy, insérez null dans next de ce noeud. 

 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
3
Parag Bafna
//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

0
Manoj Kumar Pandit