Eine der am häufigsten verwendeten und diskutierten Datenstrukturen in der Informatik ist der binäre Suchbaum. Dies ist häufig die erste Datenstruktur, die mit einem nichtlinearen Einfügungsalgorithmus eingeführt wird. Ein binärer Suchbaum ähnelt einer doppelt verketteten Liste, wobei jeder Knoten Daten und zwei Zeiger auf andere Knoten enthält. Der Unterschied besteht in der Art und Weise, wie diese Knoten miteinander in Beziehung stehen. Die Zeiger eines Knotens eines binären Suchbaums werden normalerweise „links“ und „rechts“ genannt, um den mit dem aktuellen Wert verknüpften Teilbaum anzuzeigen. Eine einfache JavaScript-Implementierung dieses Knotens sieht wie folgt aus: var Knoten = { Wert: 125, links: null, rechts: null }; Wie der Name schon sagt, ist ein binärer Suchbaum in einer hierarchischen Baumstruktur organisiert. Das erste Element wird zum Stammknoten und jeder weitere Wert wird dem Baum als Vorfahr dieser Wurzel hinzugefügt. Allerdings sind die Werte an den Knoten eines binären Suchbaums eindeutig und nach den darin enthaltenen Werten geordnet: Der Wert im linken Teilbaum eines Knotens ist immer kleiner als der Wert des Knotens, und der Wert im rechten Teilbaum ist immer größer als der Wert des Knotens. Auf diese Weise wird das Finden eines Wertes in einem binären Suchbaum sehr einfach. Wenn der gesuchte Wert kleiner ist als der Knoten, den Sie verarbeiten, gehen Sie nach links. Wenn der Wert größer ist, gehen Sie nach rechts. In einem binären Suchbaum dürfen keine Duplikate vorhanden sein, da eine Duplizierung die Beziehung zerstören würde. Das folgende Diagramm stellt einen einfachen binären Suchbaum dar. Die obige Abbildung stellt einen binären Suchbaum dar, dessen Wurzelwert 8 ist. Wenn der Wert 3 hinzugefügt wird, wird er zum linken Kind der Wurzel, da 3 kleiner als 8 ist. Wenn der Wert 1 hinzugefügt wird, wird er zum linken Kind von 3, da 1 kleiner als 8 ist (also links) und dann 1 kleiner als 3 ist (wieder links). Wenn der Wert 10 hinzugefügt wird, wird er zum rechten Kind der Wurzel, da 10 größer als 8 ist. Dieser Vorgang wird mit den Werten 6, 4, 7, 14 und 13 fortgesetzt. Die Tiefe dieses binären Suchbaums beträgt 3, was bedeutet, dass der am weitesten von der Wurzel entfernte Knoten drei Knoten beträgt. Binäre Suchbäume weisen am Ende eine natürlich sortierte Reihenfolge auf und können daher zum schnellen Auffinden von Daten verwendet werden, da Sie bei jedem Schritt Möglichkeiten sofort ausschließen können. Durch die Begrenzung der Anzahl der Knoten, die gesucht werden müssen, können Suchvorgänge schneller durchgeführt werden. Angenommen, Sie möchten im obigen Baum den Wert 6 finden. Beginnen Sie an der Wurzel und stellen Sie fest, dass 6 kleiner als 8 ist. Gehen Sie daher zum linken Kind der Wurzel. Da 6 größer als 3 ist, gelangen Sie zum richtigen Knoten. Sie finden den richtigen Wert. Sie müssen also nur drei statt neun Knoten besuchen, um den Wert zu finden. Um einen binären Suchbaum in JavaScript zu implementieren, besteht der erste Schritt darin, die grundlegende Schnittstelle zu definieren: Funktion BinärerSuchbaum() { diese._root = null; } BinarySearchTree.prototype = { //Konstruktor wiederherstellen Konstruktor: BinarySearchTree, hinzufügen: Funktion (Wert) { }, enthält: Funktion(Wert){ }, entfernen: Funktion(Wert){ }, Größe: Funktion(){ }, zuArray: Funktion(){ }, toString: Funktion(){ } }; Grundlegende Verbindungen ähneln anderen Datenstrukturen und verfügen über Methoden zum Hinzufügen und Entfernen von Werten. Ich habe auch einige praktische Methoden hinzugefügt, size(), toArray() und toString(), die für JavaScript nützlich sind. Um mit der Verwendung eines binären Suchbaums zu beginnen, ist die Methode contains() ein guter Ausgangspunkt. Die Methode contains() akzeptiert einen Wert als Argument und gibt „true“ zurück, wenn der Wert im Baum vorhanden ist, andernfalls gibt sie „false“ zurück. Diese Methode folgt einem grundlegenden binären Suchalgorithmus, um zu bestimmen, ob der Wert vorhanden ist: BinarySearchTree.prototype = { //mehr Code enthält: Funktion(Wert){ var gefunden = false, aktuell = diese._root //Stellen Sie sicher, dass ein Knoten zum Durchsuchen vorhanden ist während(!gefunden && aktuell){ //wenn der Wert kleiner ist als der des aktuellen Knotens, gehe nach links wenn (Wert < aktueller.Wert){ aktuell = aktuell.links; //wenn der Wert größer ist als der des aktuellen Knotens, gehe nach rechts } sonst wenn (Wert > aktueller.Wert){ aktuell = aktuell.rechts; //Werte sind gleich, gefunden! } anders { gefunden = wahr; } } //nur fortfahren, wenn der Knoten gefunden wurde Rückgabe gefunden; }, //mehr Code }; Die Suche beginnt an der Wurzel des Baums. Wenn keine Daten hinzugefügt werden, verfügen Sie wahrscheinlich nicht über Root-Zugriff und müssen dies überprüfen. Das Durchlaufen des Baums folgt dem zuvor besprochenen einfachen Algorithmus: Bewegen Sie sich nach links, wenn der gesuchte Wert kleiner als der aktuelle Knoten ist, und bewegen Sie sich nach rechts, wenn der Wert größer ist. Der aktuelle Zeiger wird jedes Mal überschrieben, bis der Wert gefunden wird (in diesem Fall wird „found“ auf „true“ gesetzt) oder bis in diese Richtung keine weiteren Knoten vorhanden sind (in diesem Fall ist der Wert nicht im Baum enthalten). Die in contains() verwendete Methode kann auch verwendet werden, um neue Werte in den Baum einzufügen. Der Hauptunterschied besteht darin, dass Sie nicht in einem Baum nach einem Wert suchen, sondern nach einem Speicherort, an dem Sie den neuen Wert einfügen können: BinarySearchTree.prototype = { //mehr Code hinzufügen: Funktion(Wert){ //Erstelle ein neues Item-Objekt, platziere die Daten in var Knoten = { Wert: Wert, links: null, rechts: null }, //wird zum Durchlaufen der Struktur verwendet aktuell; //Sonderfall: noch keine Elemente im Baum wenn (this._root === null){ this._root = Knoten; } anders { aktuell = diese._root; während(wahr){ //wenn der neue Wert kleiner ist als der Wert dieses Knotens, gehe nach links wenn (Wert < aktueller.Wert){ //wenn keiner mehr da ist, dann gehört der neue Knoten dorthin wenn (aktuell.links === null){ aktuell.links = Knoten; brechen; } anders { aktuell = aktuell.links; } //wenn der neue Wert größer ist als der Wert dieses Knotens, gehe nach rechts } sonst wenn (Wert > aktueller.Wert){ //wenn es kein Recht gibt, dann gehört der neue Knoten dorthin wenn (aktuell.rechts === null){ aktuell.rechts = Knoten; brechen; } anders { aktuell = aktuell.rechts; } //wenn der neue Wert dem aktuellen entspricht, einfach ignorieren } anders { brechen; } } } }, //mehr Code }; Beim Hinzufügen von Werten zu einem binären Suchbaum liegt ein Sonderfall vor, wenn keine Wurzel vorhanden ist. In diesem Fall genügt es, die Wurzel einfach auf den neuen Wert einzustellen. In den anderen Fällen ist der grundlegende Algorithmus genau derselbe wie der in contains() verwendete: Der neue Knoten geht nach links, wenn er kleiner als der aktuelle Wert ist, und nach rechts, wenn er größer ist. Der Hauptunterschied besteht darin, dass der neue Wert hierher gelangt, wenn es nicht mehr weitergeht. Wenn Sie also nach links gehen müssen, aber kein linker Knoten vorhanden ist, ist der neue Wert der linke Knoten (dasselbe wie der rechte Knoten). Da keine Duplikate vorhanden sind, wird der Vorgang abgebrochen, wenn ein Knoten mit demselben Wert gefunden wird. Bevor ich mit der size()-Methode fortfahre, möchte ich die Baumdurchquerung ausführlich besprechen. Um die Größe eines binären Suchbaums zu berechnen, ist es notwendig, jeden Knoten im Baum zu besuchen. Binäre Suchbäume verfügen üblicherweise über unterschiedliche Arten von Durchlaufmethoden, von denen der In-Order-Durchlauf die gebräuchlichste ist. Führen Sie an jedem Knoten eine In-Order-Traversierung durch, indem Sie zuerst den linken Teilbaum, dann den Knoten selbst und dann den rechten Teilbaum verarbeiten. Da binäre Suchbäume auf diese Weise von links nach rechts sortiert werden, ist das Ergebnis, dass die Knoten in der richtigen Sortierreihenfolge verarbeitet werden. Für die Methode size() ist die Reihenfolge der Knotendurchquerung nicht wirklich wichtig, für die Methode toArray() jedoch schon. Da beide Methoden eine Traversierung durchführen müssen, habe ich beschlossen, eine universell einsetzbare traverse()-Methode hinzuzufügen: BinarySearchTree.prototype = { //mehr Code durchqueren: Funktion(Prozess){ //Hilfsfunktion Funktion inOrder(Knoten){ wenn (Knoten){ //den linken Teilbaum durchlaufen wenn (node.left !== null){ inOrder(Knoten.links); } //rufen Sie die Prozessmethode auf diesem Knoten auf Prozess.Aufruf(dieser, Knoten); //den rechten Teilbaum durchlaufen wenn (node.right !== null){ inOrder(node.right); } } } //beginne mit der Wurzel inOrder(diese._root); }, //mehr Code }; Diese Methode akzeptiert ein Argument, „Prozess“, eine Funktion, die auf jedem Knoten im Baum ausgeführt werden soll. Diese Methode definiert eine Hilfsfunktion namens inOrder() zum rekursiven Durchlaufen des Baums. Beachten Sie, dass die Rekursion nur nach links und rechts verläuft, wenn der aktuelle Knoten vorhanden ist (um die mehrfache Verarbeitung von Null zu vermeiden). Die Methode traverse() durchläuft dann der Reihe nach beginnend beim Stammknoten, und die Funktion process() verarbeitet jeden Knoten. Sie können dann diese Methode verwenden, um size(), toArray(), toString() zu implementieren: BinarySearchTree.prototype = { //mehr Code Größe: Funktion(){ Varlänge = 0; dies.traverse(Funktion(Knoten){ Länge++; }); Rücklauflänge; }, zuArray: Funktion(){ var Ergebnis = []; dies.traverse(Funktion(Knoten){ Ergebnis.Push(Knoten.Wert); }); Ergebnis zurückgeben; }, toString: Funktion(){ gib dies zurück.toArray().toString(); }, //mehr Code }; Sowohl size() als auch toArray() rufen die Methode traverse() auf und übergeben eine Funktion, die auf jedem Knoten ausgeführt werden soll. Im Fall von size() erhöht die Funktion einfach die Größenvariable, während toArray() eine Funktion verwendet, um den Wert des Knotens zum Array hinzuzufügen. Die Methode toString() konvertiert das zurückgegebene Array vor dem Aufruf von toArray() in einen String und gibt es zurück. Wenn Sie einen Knoten löschen, müssen Sie feststellen, ob es sich um einen Stammknoten handelt. Der Stammknoten wird ähnlich wie andere Knoten behandelt, mit der bemerkenswerten Ausnahme, dass der Stammknoten am Ende auf einen anderen Wert gesetzt werden muss. Der Einfachheit halber wird dies im JavaScript-Code als Sonderfall behandelt. Der erste Schritt zum Löschen eines Knotens besteht darin, festzustellen, ob der Knoten vorhanden ist: BinarySearchTree.prototype = { //mehr Code hier entfernen: Funktion(Wert){ var gefunden = false, Elternteil = null, aktuell = this._root, Kinderanzahl, Ersatz, ErsatzElternteil; //Stellen Sie sicher, dass ein Knoten zum Durchsuchen vorhanden ist während(!gefunden && aktuell){ //wenn der Wert kleiner ist als der des aktuellen Knotens, gehe nach links wenn (Wert < aktueller.Wert){ Elternteil = aktuell; aktuell = aktuell.links; //wenn der Wert größer ist als der des aktuellen Knotens, gehe nach rechts } sonst wenn (Wert > aktueller.Wert){ Elternteil = aktuell; aktuell = aktuell.rechts; //Werte sind gleich, gefunden! } anders { gefunden = wahr; } } //nur fortfahren, wenn der Knoten gefunden wurde wenn (gefunden){ //weitermachen } }, //mehr Code hier }; Der erste Teil der Methode remove() besteht darin, den zu entfernenden Knoten mithilfe einer binären Suche zu lokalisieren und ihn nach links zu verschieben, wenn der Wert kleiner als der aktuelle Knoten ist, oder nach rechts, wenn der Wert größer als der aktuelle Knoten ist. Sie behalten beim Durchlaufen auch den übergeordneten Knoten im Auge, da Sie den Knoten irgendwann von seinem übergeordneten Knoten entfernen müssen. Wenn „found“ gleich „true“ ist, ist der aktuelle Wert der zu löschende Knoten. Beim Löschen eines Knotens müssen drei Bedingungen beachtet werden:
Das Löschen von etwas anderem als einem Blattknoten aus einem binären Suchbaum bedeutet, dass Werte verschoben werden müssen, um die richtige Sortierung des Baums beizubehalten. Die ersten beiden sind relativ einfach zu implementieren und erfordern lediglich das Entfernen eines Blattknotens und eines Knotens mit einem untergeordneten Knoten und das Ersetzen durch dessen untergeordnetes Knoten. Im letzten Fall ist der spätere Zugriff etwas komplizierter. Bevor Sie verstehen, wie ein Knoten gelöscht wird, müssen Sie wissen, wie viele untergeordnete Knoten auf dem Knoten vorhanden sind. Wenn dies bekannt ist, müssen Sie bestimmen, ob der Knoten ein Stammknoten ist. Dadurch erhalten Sie einen ziemlich einfachen Entscheidungsbaum: BinarySearchTree.prototype = { //mehr Code hier entfernen: Funktion(Wert){ var gefunden = false, Elternteil = null, aktuell = this._root, Kinderanzahl, Ersatz, ErsatzElternteil; //Knoten suchen (aus Platzgründen entfernt) //nur fortfahren, wenn der Knoten gefunden wurde wenn (gefunden){ //Finde heraus, wie viele Kinder Kinderanzahl = (aktuell.links !== null ? 1 : 0) + (aktuell.rechts !== null ? 1 : 0); //Sonderfall: der Wert steht an der Wurzel wenn (aktuell === diese._root){ Schalter(Anzahl der Kinder){ //keine Kinder, lösche einfach die Wurzel Fall 0: diese._root = null; brechen; //ein Kind, benutze eins als Wurzel Fall 1: this._root = (current.right === null ? aktuell.links : aktuell.rechts); brechen; //zwei Kinder, wenig Arbeit Fall 2: //ZU TUN //kein Standard } //Nicht-Root-Werte } anders { Schalter (Anzahl der Kinder) { //keine Kinder, einfach vom Elternteil entfernen Fall 0: //wenn der aktuelle Wert kleiner ist als sein //übergeordnet, setze den linken Zeiger auf Null wenn (aktueller.Wert < übergeordneter.Wert){ übergeordnetes Element.links = null; //wenn der aktuelle Wert größer ist als sein //übergeordnet, setze den rechten Zeiger auf Null } anders { übergeordnetes Element = null; } brechen; //ein Kind, einfach dem Elternteil neu zuweisen Fall 1: //wenn der aktuelle Wert kleiner ist als sein //Elternteil, setze den linken Zeiger zurück wenn (aktueller.Wert < übergeordneter.Wert){ parent.left = (aktuell.left === null? aktuell.rechts : aktuell.links); //wenn der aktuelle Wert größer ist als sein //Eltern, setze den rechten Zeiger zurück } anders { übergeordnetes Element = (aktuelles Element === null? aktuell.rechts : aktuell.links); } brechen; //zwei Kinder, etwas komplizierter Fall 2: //ZU TUN //kein Standard } } } }, //mehr Code hier }; Beim Arbeiten mit dem Stammknoten handelt es sich lediglich um den Überschreibungsprozess. Bei Nicht-Root-Knoten müssen die entsprechenden Zeiger auf dem übergeordneten Knoten entsprechend dem Wert des zu löschenden Knotens gesetzt werden: Wenn der gelöschte Wert kleiner als der übergeordnete Knoten ist, muss der linke Zeiger auf Null (für einen Knoten ohne untergeordnete Knoten) oder den linken Zeiger des gelöschten Knotens zurückgesetzt werden. Wenn der gelöschte Wert größer als der übergeordnete Knoten ist, muss der rechte Zeiger auf Null oder den rechten Zeiger des gelöschten Knotens zurückgesetzt werden. Wie bereits erwähnt, ist das Löschen eines Knotens mit zwei untergeordneten Knoten der komplexeste Vorgang. Die folgende Abbildung ist eine Darstellung des Catechu-Suchbaums. Die Wurzel ist 8 und das linke Kind ist 3. Was passiert, wenn 3 gelöscht wird? Es gibt zwei Möglichkeiten: 1 (das linke Kind von 3, der sogenannte Vorgänger in der Reihenfolge) oder 4 (das äußerste linke Kind des rechten Teilbaums, der sogenannte Nachfolger in der Reihenfolge) können beide 3 ersetzen. Jede dieser beiden Optionen ist angemessen. Um den Vorgänger in der Reihenfolge zu finden, also den Wert vor dem Wert, den Sie löschen, prüfen Sie den linken Teilbaum des Knotens, den Sie löschen möchten, und wählen Sie das am weitesten rechts stehende untergeordnete Element aus. Um den Nachfolger in der Reihenfolge zu finden, also den Wert, der unmittelbar nach dem Wert kommt, den Sie löschen, kehren Sie den Vorgang um und prüfen Sie den am weitesten links stehenden rechten Teilbaum. Für jeden dieser Schritte ist ein weiterer Durchlauf durch den Baum erforderlich, um den Vorgang abzuschließen: BinarySearchTree.prototype = { //mehr Code hier entfernen: Funktion(Wert){ var gefunden = false, Elternteil = null, aktuell = this._root, Kinderanzahl, Ersatz, ErsatzElternteil; //Knoten suchen (aus Platzgründen entfernt) //nur fortfahren, wenn der Knoten gefunden wurde wenn (gefunden){ //Finde heraus, wie viele Kinder Kinderanzahl = (aktuell.links !== null ? 1 : 0) + (aktuell.rechts !== null ? 1 : 0); //Sonderfall: der Wert steht an der Wurzel wenn (aktuell === diese._root){ Schalter(Anzahl der Kinder){ //andere Fälle wurden aus Platzgründen entfernt //zwei Kinder, wenig Arbeit Fall 2: //neue Wurzel wird das linke Kind der alten Wurzel sein //... Vielleicht Ersatz = this._root.left; //finde den am weitesten rechts gelegenen Blattknoten //die wirklich neue Wurzel während (Ersatz.rechts !== null){ replacementParent = Ersatz; Ersatz = Ersatz.rechts; } //es ist nicht der erste Knoten auf der linken Seite wenn (Ersatz-Parent !== null){ //entferne die neue Wurzel von ihrer //vorherige Position ErsatzParent.rechts = Ersatz.links; //gib der neuen Wurzel alle alten //Root's Kinder Ersatz.rechts = dies._root.rechts; Ersatz.links = dies._root.links; } anders { // Weisen Sie einfach die Kinder zu Ersatz.rechts = dies._root.rechts; } //offiziell neue Wurzel zuweisen this._root = Ersatz; //kein Standard } //Nicht-Root-Werte } anders { Schalter (Anzahl der Kinder) { //andere Fälle wurden aus Platzgründen entfernt //zwei Kinder, etwas komplizierter Fall 2: //Zeiger für neue Durchquerung zurücksetzen Ersatz = aktuell.links; Ersatz-Elternteil = aktuell; //den Knoten ganz rechts finden während(Ersatz.rechts !== null){ replacementParent = Ersatz; Ersatz = Ersatz.rechts; } ErsatzParent.rechts = Ersatz.links; // dem Ersatz untergeordnete Elemente zuweisen Ersatz.rechts = aktuell.rechts; Ersatz.links = aktuell.links; //Platziere den Ersatz an der richtigen Stelle wenn (aktueller.Wert < übergeordneter.Wert){ parent.left = Ersatz; } anders { übergeordnetes Recht = Ersatz; } //kein Standard } } } }, //mehr Code hier }; Der Code für einen Stammknoten und einen Nicht-Stammknoten mit zwei untergeordneten Knoten ist nahezu identisch. Diese Implementierung findet den Vorgänger in der richtigen Reihenfolge immer, indem sie im linken Teilbaum nach dem am weitesten rechts stehenden Kind sucht. Die Durchquerung erfolgt mithilfe der Variablen „replacement“ und „replacementParent“ in einer While-Schleife. Der ersetzte Knoten ist letztendlich der Knoten, der den aktuellen ersetzt. Daher wird er von seiner aktuellen Position entfernt, indem der rechte Zeiger seines übergeordneten Knotens auf den linken Zeiger des Ersatzknotens gesetzt wird. Wenn der Ersatz für den Stammknoten ein direktes untergeordnetes Element des Stammknotens ist, ist „replacementParent“ null. Der rechte Zeiger des Ersatzes wird daher einfach auf den rechten Zeiger der Wurzel gesetzt. Der letzte Schritt besteht darin, die Ersatzknoten den richtigen Standorten zuzuweisen. Bei Stammknoten wird der Ersatz auf die neue Wurzel gesetzt, bei Nicht-Stammknoten wird der Ersatz der entsprechenden Position auf dem ursprünglichen übergeordneten Knoten zugewiesen. Hinweis: Das ständige Ersetzen von Knoten durch ihre Vorgänger in der richtigen Reihenfolge kann zu einem unausgeglichenen Baum führen, bei dem sich die meisten Werte auf einer Seite des Baums befinden. Ein unausgeglichener Baum bedeutet eine weniger effiziente Suche und sollte daher in realen Szenarien Anlass zur Sorge geben. Bei der Implementierung eines binären Suchbaums muss entschieden werden, ob In-Order-Vorgänger oder In-Order-Nachfolger verwendet werden, damit der Baum richtig ausgewogen bleibt (oft als selbstausgleichender binärer Suchbaum bezeichnet). Zusammenfassen Dies ist das Ende dieses Artikels zur Verwendung von JavaScript zur Implementierung eines binären Suchbaums. Weitere relevante Inhalte zum binären Suchbaum in JavaScript finden Sie in den vorherigen Artikeln von 123WORDPRESS.COM oder in den folgenden verwandten Artikeln. Ich hoffe, dass jeder 123WORDPRESS.COM in Zukunft unterstützen wird! Das könnte Sie auch interessieren:
|
>>: Analyse der problematischen „Aborted“-Warnung in MySQL anhand von Fallstudien
CentOS 8 ist schon seit längerem auf dem Markt. A...
Es ist sehr wichtig, den Betriebsstatus von Conta...
Manchmal müssen wir Server stapelweise bedienen, ...
Die bedingten Kommentare des Internet Explorers s...
Inhaltsverzeichnis Vorwort Rendern setTable-Kompo...
Vorwort Heute habe ich von einem Entwickler die R...
Der spezifische Code lautet wie folgt: <!DOCTY...
Viele Websites verfügen oben über eine feste Navi...
Erstellen Sie einen Benutzer: Erstellen Sie den B...
Ohne weitere Umschweife werde ich den Code direkt...
Vor Kurzem habe ich das Problem gelöst, dass Dock...
Wenn wir eine Webseite erstellen, möchten wir man...
Welche historische Version kann die aktuelle Tran...
Test: Chrome v80.0.3987.122 ist normal Es gibt zw...
Inhaltsverzeichnis Vorwort Installieren Sie vue-i...