Informatik Q12: Lernbereich 2

Geordnete Binärbäume & Entwurfsmuster Kompositum
⬇ Binärbaum.json (Online-IDE ) herunterladen
← Zurück zur Übersicht

1. Konzept: Grenzen der Liste

In Lernbereich 2 haben wir Listen implementiert. Sie sind dynamisch, haben aber eine Schwäche.

Gedankenexperiment

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".

Merke: Während bei der Liste jeder neue Eintrag die Suche linear verlängert, steigt der Aufwand beim Baum nur extrem langsam an (logarithmisch).
← Zurück zur Übersicht

2. Die Architektur (Kompositum)

Wir verwenden das Entwurfsmuster Kompositum. Es hilft uns, die Rekursion sauber zu programmieren, indem wir "leere" Enden als eigene Objekte behandeln.

Aufgabe 1: Wer ist zuständig?

Ordne die folgenden Aufgaben den Klassen Knoten oder Abschluss zu.

Aussagen:
  1. "Ich speichere das eigentliche Datenobjekt (z.B. ein Wort)."
  2. "Ich repräsentiere das 'Nichts' am Ende eines Astes."
  3. "Ich habe zwei Zeiger auf weitere Baumelemente."
  4. "Wenn man bei mir etwas sucht, ist die Antwort immer 'Falsch/Nicht gefunden'."
Lösung prüfen
  • 1. Knoten (enthält Datenelement)
  • 2. Abschluss (ist das Ende)
  • 3. Knoten (leNext und riNext)
  • 4. Abschluss (Abbruchbedingung der Rekursion)

Aufgabe 2: Polymorphie verstehen

Warum leiten wir beide Klassen von der abstrakten Klasse Baumelement ab?

Tipp 1

Überlege, welchen Datentyp die Zeiger leNext und riNext im Knoten haben müssen.

Lösung

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.

← Zurück zur Übersicht

3. PRIMM: Einfügen (Aufbau)

Bevor wir suchen können, müssen wir Daten einfügen. Hier verändert sich die Struktur des Baums.

Predict (Vorhersage)

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.

Auflösung
  1. Start: wurzel ist ein Abschluss.
  2. "Maus": Der Abschluss ersetzt sich selbst durch einen Knoten("Maus"). Dieser hat zwei neue Abschlüsse.
  3. "Elefant": Die Wurzel ("Maus") vergleicht. Elefant < Maus. Der Befehl geht an den linken Abschluss. Dieser ersetzt sich durch einen Knoten("Elefant").

Modify & Make (Implementierung)

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;
}
    
Hilfe Stufe 1 (Logik)

Wenn das neue Element größer ist als der Inhalt, muss es in den rechten Teilbaum. Das Prinzip ist spiegelsymmetrisch zur linken Seite.

Hilfe Stufe 2 (Syntax)

Du musst den rechten Nachfolger (riNext) aufrufen und dessen Rückgabewert wieder in riNext speichern.

Hilfe Stufe 3 (Code-Lösung)
this.riNext = this.riNext.einfuegen(d);

Die Abschluss-Klasse

Was macht der Abschluss, wenn einfuegen aufgerufen wird?

Lösung
public BaumElement einfuegen(DatenElement d) {
    // Aus Abschluss wird Knoten!
    return new Knoten(d);
}
            
← Zurück zur Übersicht

4. PRIMM: Suchen

Der Baum ist aufgebaut. Jetzt wollen wir prüfen, ob ein Wort enthalten ist.

Predict (Analyse)

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);
    }
}
    
Lösung Predict
  • 1. Vergleich: "Maus" ist ungleich "Zebra".
  • 2. Richtung: "Maus" ist nicht größer als "Zebra" (Z kommt nach M).
  • 3. Aktion: Der else-Zweig wird gewählt. Abstieg nach rechts (riNext).

Investigate (Untersuchen)

Wir haben in Modul 1 über Effizienz gesprochen. Wo genau im Code passiert die "Magie", die den Baum so schnell macht?

Hilfe Stufe 1

Schau dir die if / else if / else Struktur an. Wie viele Wege gehen wir?

Lösung

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.

← Zurück zur Übersicht

5. Traversierung

Manchmal wollen wir nicht suchen, sondern alles ausgeben (z.B. Wörterbuch drucken).

Strategien

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. ???
}
    
Hilfe Stufe 1

Um A (links) vor B (Wurzel) zu drucken, müssen wir zuerst absteigen.

Hilfe Stufe 2

Reihenfolge: Erst Links, dann Ich, dann Rechts.

Lösung (Inorder)
leNext.ausgeben();
System.out.println(this.inhalt);
riNext.ausgeben();
            

Dies nennt man Inorder-Traversierung.

← Zurück zur Übersicht

6. Projekt & Abschluss

Zum Abschluss eine Aufgabe für Profis. Wir wollen wissen, wie tief der Baum ist (wie lang der längste Ast ist).

Aufgabe: Die Methode tiefe()

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.

Hilfe Stufe 1

Du brauchst zwei Variablen für die Tiefe links und rechts.

Hilfe Stufe 2

Nutze Math.max(a, b) um den größeren Wert zu finden.

Lösung
public int tiefe() {
    int tL = leNext.tiefe();
    int tR = riNext.tiefe();
    return 1 + Math.max(tL, tR);
}