63 Stimmen

Interview: Schleife in verknüpfter Liste entfernen - Java

Diese Frage wurde mir im Vorstellungsgespräch gestellt: "Wie erkennt man eine Schleife in einer verknüpften Liste?", ich löste diese Frage, aber sofort fragte mich der Interviewer, wie ich die Schleife in einer verknüpften Liste entfernen kann. Ich fummelte.

So alle Hinweise auf, wie dies zu lösen, kann Pseudo-Code oder Methode Definition sein?

Ich kenne mich mit Java gut aus, daher habe ich diese Frage unter Java getaggt.

Diese verknüpfte Liste hat zum Beispiel eine Schleife

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

69voto

no.good.at.coding Punkte 19983

Dieses Problem besteht aus zwei Teilen:

  1. Erkennen, ob es eine Schleife in der Liste gibt
  2. Identifizieren Sie den Beginn der Schleife

Sobald Sie wissen, wo die Schleife beginnt, ist es einfach, das letzte Element in der Liste zu identifizieren, da es das Element in der Liste ist, das auf den Beginn der Schleife folgt und schließlich auf den Beginn der Schleife zurückzeigt. Es ist dann trivial, den nächsten Zeiger/Verweis auf dieses Element zu setzen null um die zyklische Verknüpfungsliste zu korrigieren (keine zirkulär verknüpfte Liste, bei der das letzte Element auf das erste zurückverweist - dies wäre ein spezieller Fall von zyklischen Listen).

  1. Floyds Zykluserkennungsalgorithmus, auch Schildkröten- und Hasenalgorithmus genannt ist eine Möglichkeit, den Zyklus zu erkennen, da es sich um zwei Zeiger/Referenzen handelt, die sich mit unterschiedlicher Geschwindigkeit bewegen. Wenn es einen Zyklus gibt, werden die beiden Zeiger (z. B. p1 y p2 ) verweisen nach einer endlichen Anzahl von Schritten auf dasselbe Element. Interessanterweise kann bewiesen werden, dass das Element, an dem sie sich treffen, folgendes ist die gleiche Entfernung zum Beginn der Schleife (die Liste wird weiterhin in der gleichen Richtung durchlaufen), da der Beginn der Schleife an der Kopf der Liste . Das heißt, wenn der lineare Teil der Liste k Elemente, treffen sich die beiden Zeiger innerhalb der Schleife der Länge m an einem Punkt m-k vom Beginn der Schleife an oder k Elemente an das "Ende" der Schleife (natürlich ist es eine Schleife, also hat sie kein "Ende" - sie ist nur wieder der "Anfang"). Auf diese Weise können wir den Anfang der Schleife finden:

  2. Sobald ein Zyklus erkannt wurde, lassen Sie p2 weiterhin auf das Element zeigen, an dem die Schleife für den obigen Schritt beendet wurde, aber zurückgesetzt p1 so dass er wieder auf den Anfang der Liste verweist. Bewegen Sie nun jeden Zeiger ein Element nach dem anderen. Da p2 innerhalb der Schleife beginnt, wird die Schleife fortgesetzt. Nach k Schritte (gleich dem Abstand des Schleifenbeginns vom Kopf der Liste), p1 y p2 werden sich wieder treffen. Dadurch erhalten Sie einen Verweis auf den Beginn der Schleife.

  3. Jetzt ist es einfach, die p1 (oder p2 ), um auf das Element zu zeigen, mit dem die Schleife beginnt, und die Schleife zu durchlaufen, bis p1 zeigt am Ende wieder auf das Startelement. An diesem Punkt p1 referenziert die 'letzte' Elementliste und ihr nächster Zeiger kann auf null .


Hier ist ein schneller und schmutziger Java-Code, der von einer verknüpften Liste von Node s wo ein Node hat eine next Referenz. Dies könnte noch optimiert werden, aber es sollte Ihnen die Grundidee vermitteln:

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

Diese Erklärung könnte das Warum von Teil II erklären:

Angenommen, die Länge des Zyklus ist M, und die Länge des Rests der verknüpften Liste ist L. Wir wollen herausfinden was ist die Position im Zyklus, wenn t1/t2 sich zum ersten Mal treffen?

Definieren Sie den ersten Knoten im Zyklus als Position 0, nach den Links haben wir haben wir Position 1, 2,..., bis zu M-1. ( Wenn wir im Zyklus gehen, ist unsere aktuelle Position ist (Länge des Weges) mod M, richtig?) Angenommen, t1/t2 treffen sich zuerst an Position p, dann ist ihre Reisezeit die gleiche, (L+k1*M+p)/v = (L+k2*M+p)/2v für einige k1

Daraus ergibt sich, dass, wenn t1 von p, t2 vom Kopf aus starten und sich mit dem mit derselben Geschwindigkeit bewegen, dann wird Grantee an der Position 0, dem ersten Knoten des Zyklus. QED.

Weitere Referenzen:

16voto

Hristo Punkte 43811

Lösung 1 - mit freundlicher Genehmigung von Career Cup und Buch "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;
}

Die Erklärung für diese Lösung ist direkt aus dem Buch:

Wenn wir zwei Zeiger verschieben, einen mit Geschwindigkeit 1 und einem anderen mit Geschwindigkeit 2, werden sie sich am Ende treffen, wenn die verknüpfte Liste eine Schleife hat. Und warum? Denken Sie an zwei Autos, die auf einer Strecke fahren; das schnellere Auto wird immer das langsamere überholen!

Der knifflige Teil besteht darin, den Anfang zu finden der Schleife zu finden. Stellen Sie sich, als Analogie, vor zwei Personen rennen um eine Bahn, einer läuft doppelt so schnell wie der andere. Wenn sie an der gleichen Stelle starten wann werden sie sich das nächste Mal treffen? Sie treffen sich beim Start der nächsten nächsten Runde.

Nehmen wir nun an, Fast Runner hätte einen Vorsprung von k Metern auf einer Runde mit n Schritten. Wann werden sie sich das nächste Mal treffen? Sie treffen sich k Meter vor dem Start der nächsten Runde. (Warum? Fast Der Läufer hätte k + 2(n - k) Schritte gemacht, einschließlich seines Vorsprungs, und der langsame Läufer hätte n - k Schritte gemacht Beide werden k Schritte vor dem Start der Schleife).

Nun zurück zum Problem: Wenn Fast Runner (n2) und Slow Runner (n1) sich um unseren kreisförmig verknüpften Liste bewegen, hat n2 einen Vorsprung in der Schleife, wenn n1 eintritt. Genauer gesagt, hat er einen Vorsprung von k, wobei k die Anzahl der der Knoten vor der Schleife ist. Da n2 einen einen Vorsprung von k Knoten hat, werden n1 und n2 k Knoten vor dem Beginn der Schleife treffen der Schleife.

Wir wissen jetzt also Folgendes:

  1. Kopf ist k Knoten vom LoopStart entfernt (per Definition)
  2. MeetingPoint für n1 und n2 ist k Knoten von LoopStart (wie oben gezeigt)

Wenn wir also n1 zurück nach Head bewegen und n2 am MeetingPoint belassen und beide im gleichen Tempo bewegen, werden sie sich am LoopStart treffen

Lösung 2 - mit freundlicher Genehmigung von mir :)

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

Ich hoffe, das hilft.
Hristo

6voto

bitxwise Punkte 3464

Diese Antwort soll nicht mit der Antwort konkurrieren, sondern vielmehr das Zusammentreffen der beiden Knoten im Algorithmus von Schildkröte und Hase etwas näher erläutern.

  1. Beide Knoten werden schließlich die Schleife betreten. Da sich einer schneller bewegt (F) als der andere (S), wird (F) schließlich (S) überholen.

  2. Wenn die Schleife am Kopf der Liste beginnt, muss (F) am Kopf der Liste wieder auf (S) treffen. Dies ist NUR deshalb der Fall, weil die Geschwindigkeit von (F) das 2fache der Geschwindigkeit von (S) beträgt; wäre sie das 3fache, würde dies nicht zutreffen. Dies ist wahr, weil (F) eine Runde vollendet, wenn (S) eine halbe Runde vollendet, so dass, wenn (S) seine erste Runde vollendet, (F) zwei Runden vollendet hat - und wieder am Anfang der Schleife mit (S) ist.

  3. Wenn der Start der Schleife NICHT am Kopf der Liste liegt, hat (F) zu dem Zeitpunkt, zu dem (S) in die Schleife eintritt, einen Kopfstart von (k) Knoten in der Schleife gehabt. Da sich (S) immer nur um einen Knoten weiterbewegt, trifft er (F) bei (k) Knoten vom Schleifenanfang an - also (k) weitere Schritte vor dem Erreichen des Anfangs, NICHT (k) Schritte NACH dem Anfang. Dies wäre NICHT der Fall, wenn die Geschwindigkeit von (S) nicht eins wäre und das Geschwindigkeitsverhältnis zwischen (F) und (S) nicht 2:1 wäre.

    3.1. An dieser Stelle wird es ein wenig kompliziert zu erklären. Wir können uns darauf einigen, dass (F) weiterhin (S) überlappen wird, bis sie sich schließlich treffen (siehe 1 oben), aber warum bei (k) Knoten vom Beginn der Schleife? Betrachten wir die folgende Gleichung, in der M die Anzahl der Knoten oder die Entfernung der Schleife und k der Vorsprung ist, den (F) hatte; die Gleichung stellt die von (F) zurückgelegte Entfernung zur Zeit t auf der linken Seite in Form der von (S) zurückgelegten Entfernung auf der rechten Seite dar:

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

    Wenn also (S) in die Schleife eintritt und in der Schleife eine Strecke von 0 zurückgelegt hat, hat (F) nur die Strecke (k) zurückgelegt. Zu dem Zeitpunkt, an dem d_S = M - k ist, ist d_F = 2M - k. Da wir auch die modulare Mathematik anwenden müssen, um zu berücksichtigen, dass M die Gesamtstrecke einer einzigen Runde in der Schleife darstellt, ist die POSITION von (F) und (S) bei einem beliebigen ganzen M (ohne Rest) 0. In Bezug auf die POSITION (oder das Differential) bleibt also k (oder vielmehr -k) übrig.

    Und so treffen sich schließlich (S) und (F) an der Position (0 - k), oder (k) Knoten vom Beginn der Schleife entfernt.

  4. Ausgehend von [3] stellt (k) den Vorsprung von (F) dar, und da (F) die zweifache Strecke zurückgelegt hat, die (S) zurückgelegt hat, um vom Kopf der Liste in die Schleife zu gelangen, stellt (k) auch die Entfernung vom Anfang der Liste dar, die dann den Anfang der Schleife darstellt.

Es ist ein bisschen spät, deshalb hoffe ich, dass ich mich gut ausgedrückt habe. Lassen Sie es mich wissen, und ich werde versuchen, meine Antwort zu aktualisieren.

5voto

Bei Verwendung einer Identitäts-Hash-Map (z. B. IdentityHashMap ) zulässig ist, ist dies furchtbar einfach zu lösen. Allerdings wird dadurch mehr Platz benötigt.

Durchlaufen Sie die Knotenliste. Fügen Sie jeden gefundenen Knoten zur Identitätskarte hinzu. Wenn der Knoten bereits in der Identitätskarte vorhanden war, dann hat die Liste eine zirkuläre Verbindung und der Knoten, der vor diesem Konflikt lag, ist bekannt (entweder vor dem Verschieben prüfen oder den "letzten Knoten" beibehalten) -- einfach "next" entsprechend setzen, um den Zyklus zu unterbrechen.

Diesem einfachen Ansatz zu folgen, sollte eine unterhaltsame Übung sein: Der Code wird dem Leser als Übung überlassen.

Viel Spaß beim Kodieren.

3voto

Parag Bafna Punkte 22529
 0--->1---->2---->3---->4---->5---->6
                                   |
                  |                 
                 11<—-22<—-12<—-9<—-8  

Fügen Sie nach jedem Knoten der Verknüpfungsliste einen Dummy-Knoten ein und prüfen Sie vor dem Einfügen, ob der nächste Knoten ein Dummy-Knoten ist oder nicht. Wenn next to next ein Dummy ist, fügen Sie null in next dieses Knotens ein.

 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

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X