Ich suche nach dem effizientesten Algorithmus, um einen Baum zu erstellen (entweder als Liste von Kanten oder als Liste von Zuordnungen vom übergeordneten Knoten zu einer Liste von untergeordneten Knoten gespeichert). und für JEDEN Knoten eine Liste aller von ihm abstammenden Knoten (Blattebene und Nicht-Blattebene) erstellen.
Die Implementierung muss aufgrund der Skalierung über Schleifen statt über Reclusion erfolgen. und sollte idealerweise O (N) sein.
Diese SO-Frage behandelt eine einigermaßen offensichtliche Standardlösung, um die Antwort für EINEN Knoten in einem Baum zu finden. Aber offensichtlich ist es sehr ineffizient, diesen Algorithmus auf jedem Baumknoten zu wiederholen (von oben (O (NlogN) bis O (N ^ 2)).
Die Baumwurzel ist bekannt. Der Baum hat eine absolut willkürliche Form (z. B. nicht N-när, in keiner Weise ausgeglichen, Form oder Gestalt, keine einheitliche Tiefe) - einige Knoten haben 1-2 Kinder, andere 30K Kinder.
Auf praktischer Ebene (obwohl dies den Algorithmus nicht beeinflussen sollte) hat der Baum ~ 100K-200K-Knoten.
quelle
Antworten:
Wenn Sie tatsächlich jede Liste als unterschiedliche Kopien PRODUZIEREN möchten, können Sie nicht hoffen, im schlimmsten Fall einen besseren Platz als n ^ 2 zu erreichen. Wenn Sie nur Zugriff auf jede Liste benötigen:
Ich würde den Baum ausgehend von der Wurzel in der richtigen Reihenfolge durchlaufen:
http://en.wikipedia.org/wiki/Tree_traversal
Speichern Sie dann für jeden Knoten im Baum die minimale In-Order-Nummer und die maximale In-Order-Nummer in seinem Teilbaum (dies kann leicht durch Rekursion beibehalten werden - und Sie können dies mit einem Stapel simulieren, wenn Sie dies wünschen).
Nun setzen Sie alle Knoten in ein Array A der Länge n, wobei sich der Knoten mit der Ordnungsnummer i an Position i befindet. Wenn Sie dann die Liste für einen Knoten X suchen müssen, schauen Sie in A [X.min, X.max] nach - beachten Sie, dass dieses Intervall den Knoten X enthält, der auch leicht repariert werden kann.
All dies wird in O (n) Zeit erreicht und nimmt O (n) Raum ein.
Ich hoffe das hilft.
quelle
Der ineffiziente Teil durchläuft nicht den Baum, sondern erstellt die Knotenlisten. Es erscheint sinnvoll, die Liste folgendermaßen zu erstellen:
Da jeder untergeordnete Knoten in die Liste jedes übergeordneten Knotens kopiert wird, ergibt sich für ausgeglichene Bäume im Durchschnitt eine Komplexität von O (n log n) und für entartete Bäume, bei denen es sich wirklich um verknüpfte Listen handelt, der Worst-Case von O (n²).
Wir können auf O (n) oder O (1) fallen, je nachdem, ob Sie ein Setup durchführen müssen, wenn wir den Trick der trägen Berechnung der Listen verwenden. Angenommen, wir haben eine
child_iterator(node)
, die uns die Kinder dieses Knotens gibt. Wir können dann trivial so etwas definierendescendant_iterator(node)
:Eine nicht rekursive Lösung ist viel komplizierter, da der Ablauf der Iteratorsteuerung schwierig ist (Coroutinen!). Ich werde diese Antwort später heute aktualisieren.
Da das Durchlaufen eines Baums O (n) ist und die Iteration über eine Liste ebenfalls linear ist, werden die Kosten durch diesen Trick vollständig verschoben, bis sie trotzdem bezahlt werden. Zum Beispiel hat das Ausdrucken der Liste der Nachkommen für jeden Knoten eine O (n²) -Komplexität im ungünstigsten Fall: Das Iterieren über alle Knoten ist O (n), und das Iterieren über die Nachkommen jedes Knotens, unabhängig davon, ob sie in einer Liste gespeichert oder ad hoc berechnet sind .
Dies funktioniert natürlich nicht, wenn Sie eine tatsächliche Sammlung benötigen, an der Sie arbeiten können.
quelle
Dieser kurze Algorithmus sollte es tun. Schauen Sie sich den Code an
public void TestTreeNodeChildrenListing()
Der Algorithmus geht tatsächlich nacheinander durch die Knoten des Baums und behält die Liste der Eltern des aktuellen Knotens bei. Gemäß Ihrer Anforderung ist der aktuelle Knoten ein untergeordnetes Element jedes übergeordneten Knotens und wird jedem untergeordneten Knoten als untergeordnetes Element hinzugefügt.
Das Endergebnis wird im Wörterbuch gespeichert.
quelle
Normalerweise würden Sie nur einen rekursiven Ansatz verwenden, da Sie damit Ihre Ausführungsreihenfolge so ändern können, dass Sie die Anzahl der Blätter ab den Blättern aufwärts berechnen können. Da Sie das Ergebnis Ihres rekursiven Aufrufs verwenden müssten, um den aktuellen Knoten zu aktualisieren, wäre es besonders aufwändig, eine rekursive Endversion zu erhalten. Wenn Sie sich nicht darum bemühen, würde dieser Ansatz Ihren Stapel natürlich einfach für einen großen Baum explodieren lassen.
Angesichts der Tatsache, dass wir erkannt haben, dass die Hauptidee darin besteht, eine Schleifenreihenfolge von den Blättern bis zur Wurzel zu erhalten, ist die natürliche Idee, eine topologische Sortierung des Baums durchzuführen . Die resultierende Folge von Knoten kann linear durchlaufen werden, um die Anzahl der Blätter zu summieren (vorausgesetzt, Sie können überprüfen, ob ein Knoten ein Blatt ist
O(1)
). Die Gesamtzeitkomplexität der topologischen Sortierung beträgtO(|V|+|E|)
.Ich gehe davon aus, dass dies
N
die Anzahl der Knoten ist, die|V|
normalerweise (aus der DAG-Nomenklatur) stammen. Die Größe vonE
hängt andererseits stark von der Arität Ihres Baumes ab. Beispielsweise hat ein BinärbaumO(|E|) = O(2*|V|) = O(|V|)
in diesem Fall höchstens 2 Kanten pro Knoten , was zu einemO(|V|)
Gesamtalgorithmus führen würde. Beachten Sie, dass Sie aufgrund der Gesamtstruktur eines Baums so etwas nicht haben könnenO(|E|) = O(|V|^2)
. Da jeder Knoten ein eindeutiges übergeordnetes Element hat, können Sie höchstens eine Kante pro Knoten zählen, wenn Sie nur übergeordnete Beziehungen berücksichtigen. Für Bäume haben wir also eine Garantie dafürO(|E|) = O(|V|)
. Daher ist der obige Algorithmus in der Größe des Baums immer linear.quelle