In Lernbereich 2 haben wir Listen implementiert. Sie sind dynamisch, haben aber eine Schwäche.
Stell dir vor, eine Datenbank speichert 1.000.000 Einträge in einer Liste.
Die Auswirkung:
Um unter 1.000.000 Einträgen zu suchen, braucht der Baum (im Idealfall) nur ca. 20 Vergleiche. Das ist der Unterschied zwischen "sofort da" und "Wartezeit".
Wir verwenden das Entwurfsmuster Kompositum. Es hilft uns, die Rekursion sauber zu programmieren, indem wir "leere" Enden als eigene Objekte behandeln.
Ordne die folgenden Aufgaben den Klassen Knoten oder Abschluss zu.
Datenelement)leNext und riNext)Warum leiten wir beide Klassen von der abstrakten Klasse Baumelement ab?
Überlege, welchen Datentyp die Zeiger leNext und riNext im Knoten haben müssen.
Ein Knoten weiß nicht, ob sein Nachfolger wieder ein Knoten oder ein Abschluss ist. Er kennt nur den Oberbegriff Baumelement. Dank der Vererbung kann er auf beiden Objekten dieselben Methoden aufrufen (z.B. suchen()), ohne mit if(nachfolger == null) prüfen zu müssen.
Bevor wir suchen können, müssen wir Daten einfügen. Hier verändert sich die Struktur des Baums.
Wir haben einen leeren Baum (nur ein Abschluss). Wir fügen "Maus" ein. Danach fügen wir "Elefant" ein.
Beschreibe, wie sich die Objekte verändern.
wurzel ist ein Abschluss.Implementiere die Methode einfuegen in der Klasse Knoten.
Achtung: Da wir die Struktur ändern, geben Methoden im Kompositum oft das (neue) Objekt zurück!
public BaumElement einfuegen(DatenElement d) { if (this.inhalt.istGleich(d)) { // Element schon da -> keine Änderung return this; } else if (this.inhalt.istGroesserAls(d)) { // Nach links absteigen und Ergebnis speichern this.leNext = this.leNext.einfuegen(d); } else { // AUFGABE: Was passiert rechts? // ??? } return this; }
Wenn das neue Element größer ist als der Inhalt, muss es in den rechten Teilbaum. Das Prinzip ist spiegelsymmetrisch zur linken Seite.
Du musst den rechten Nachfolger (riNext) aufrufen und dessen Rückgabewert wieder in riNext speichern.
this.riNext = this.riNext.einfuegen(d);
Was macht der Abschluss, wenn einfuegen aufgerufen wird?
public BaumElement einfuegen(DatenElement d) { // Aus Abschluss wird Knoten! return new Knoten(d); }
Der Baum ist aufgebaut. Jetzt wollen wir prüfen, ob ein Wort enthalten ist.
Analysiere folgenden Code der Klasse Knoten. Was passiert Schritt für Schritt bei der Suche nach "Zebra", wenn die Wurzel "Maus" ist?
public boolean suchen(DatenElement d) { if (this.inhalt.istGleich(d)) { return true; } else if (this.inhalt.istGroesserAls(d)) { return leNext.suchen(d); } else { return riNext.suchen(d); } }
else-Zweig wird gewählt. Abstieg nach rechts (riNext).Wir haben in Modul 1 über Effizienz gesprochen. Wo genau im Code passiert die "Magie", die den Baum so schnell macht?
Schau dir die if / else if / else Struktur an. Wie viele Wege gehen wir?
Indem wir uns für einen Weg (links ODER rechts) entscheiden, ignorieren wir den kompletten anderen Teilbaum. Wir müssen ihn gar nicht betrachten. Das spart die Rechenzeit.
Manchmal wollen wir nicht suchen, sondern alles ausgeben (z.B. Wörterbuch drucken).
Ein Baum ist nicht linear. Wir müssen eine Reihenfolge festlegen. Gegeben ist ein Baum: Wurzel(B), Links(A), Rechts(C).
Aufgabe: Implementiere ausgeben() so, dass die Buchstaben alphabetisch (A, B, C) erscheinen.
public void ausgeben() { // 1. ??? System.out.println(this.inhalt); // 2. ??? }
Um A (links) vor B (Wurzel) zu drucken, müssen wir zuerst absteigen.
Reihenfolge: Erst Links, dann Ich, dann Rechts.
leNext.ausgeben(); System.out.println(this.inhalt); riNext.ausgeben();
Dies nennt man Inorder-Traversierung.
Zum Abschluss eine Aufgabe für Profis. Wir wollen wissen, wie tief der Baum ist (wie lang der längste Ast ist).
Die Tiefe eines Knotens ist die länge des Pfades eines Knotens bis zur Wurzel. Passend dazu ist die Höhe eines Baums ist die Tiefe eines Knotens der am weitesten von der Wurzel entfernt ist (also die maximale Tiefe). Die Wurzel hat die Tiefe 0, ein leerer Baum hat die Tiefe -1.
Versuche, die Methode für die Klasse Knoten zu schreiben.
Du brauchst zwei Variablen für die Tiefe links und rechts.
Nutze Math.max(a, b) um den größeren Wert zu finden.
public int tiefe() { int tL = leNext.tiefe(); int tR = riNext.tiefe(); return 1 + Math.max(tL, tR); }