Basis–Hausübungen 2-B und 4-B zu Programmierung II (C++) Thomas Letschert Wintersemester 2002/2003 FH Giessen–Friedberg 27. Oktober 2002 Vorbemerkungen Die Hausübungen 2–B und 4–B sind vereinfachte Versionen der Hausübungen 2 und 4. Für sie gelten die gleichen Regeln wie für die Hausübungen 2 und 4, außer: 1. Die bei der Abgabe eventuell erzielten Bonuspunkte können nicht dazu verwendet werden, die Klausur–Note auf einen Wert kleiner als 2,0 zu verbessern. 2. Die Abgabe berechtigt nicht zur Teilnahme an einer mündlichen Prüfung. Aufgabe 2–B (Basis–Hausübung) Bäume Baum Das Konzept Baum (engl.: Tree) ist eine Verallgemeinerung der Liste. In einer Liste hat ein Element einen Nachfolger. In einem Baum dagegen kann es mehrere Nachfolger haben. (Bei dem Wort “Baum” sollte man darum auch eher an einen Stammbaum, als einen wirklichen Baum denken.) Die Elemente eines Baums nennt man allgemein Knoten und die Nachfolgerbeziehung Kanten. Der erste Knoten wird Wurzel genannt, und Knoten ohne Nachfolger nennt man Blätter. Der Baum, der den Nachfolger eines Knotens als Wurzel hat, wird Unterbaum des Knotens genannt. Varianten von Bäumen Bäume gibt es vielen Varianten. Die Zahl der Nachfolger kann für jedes Element fest oder beliebig sein. Die Nachfolger können sortiert (erster, zweiter, etc. Nachfolger) oder unsortiert sein. Binäre Bäume sind sortierte Bäume bei denen jeder Knoten höchstens zwei Nachfolger hat: den linken und den rechten Nachfolger. Implementierung von Bäumen Bäume können auf vielfältige Art implementiert werden. Die naheliegenste Form einen Baum zu speichern ist, jeden Knoten durch ein Objekt mit der Nachfolgerelation als Feld von Zeigern auf die Nachfolger darzustellen. Eventuell kann man auch einen Zeiger zum Vorgänger (dem “Elternknoten”) speichern. Die optimale Implementierung hängt natürlich von der Anwendung ab – also von den Operationen die auf dem Baum ausgeführt werden sollen. Binäre Suchbäume Eine spezielle Art von Bäumen sind die binären Suchbäume. Dabei handelt es sich um binäre Bäume, die besonders gut zur Speicherung von Daten geeignet sind. Speziell auf Suchoperationen hin sind sie optimiert: In einem 1 binären Suchbaum werden Daten so abgelegt, dass das Wiederauffinden eines Datums innerhalb des Baums effizient durchgeführt werden kann. Ein binärer Suchbaum (auch geordneter Binärbaum) ist ein binärer Baum mit folgenden Eigenschaften: – Der Wert der Wurzel ist größer als der Wert der Wurzel des linken Unterbaums. – Der Wert der Wurzel ist kleiner als der Wert der Wurzel des rechten Unterbaums. – Der linke und der rechte Unterbaum sind binäre Suchbäume. Abbildung 1 zeigt ein Beispiel. 3 1 10 9 2 13 8 12 19 Abbildung 1: binärer Suchbaum In binären Suchbäumen kann sehr schnell gesucht werden, ohne dass Einfüg– und Löschoperationen besonders komplex sind. Klassendefinition eines binären Suchbaums Das Beispiel eines binären Suchbaums für Zeichenketten ist: class Tree { public: Tree (); ˜Tree (); ... ... void insert (const string &); void erase (const string &); bool lookup (const string &) const; ... ... private: class Node { public: Node (); Node (const string &, Node *c1, Node *c2); ˜Node (); string Node val; *succ[2]; }; Node * root; ... weitere interne Definitionen ... }; 2 insert fügt einen Wert als neuen Knoten ein. erase löscht den Knoten mit dem übergebenen Wert. Die Suchoperation Die Suchoperation lookup sieht nach, ob ein vorgegebener Wert im Baum vorhanden ist. Die Einfügoperation Die Einfügoperation speichert einen Wert im Baum und erhält dabei dessen Eigenschaft ein Binärbaum zu sein. Sie kann leicht rekursiv definiert werden: void Tree::insert (const string &v) { insert_r (v, root); } void Tree::insert_r (const string &v, Node * &r) { if (r == 0) { r = new Node (v, 0, 0); } else { if (v < r->val) insert_r (v, r->succ[0]); if (v > r->val) insert_r (v, r->succ[1]); //if (v == r->val) schon drin: keine Aktion! } } Wenn der einzufügende Wert bereits im Baum vorhanden ist, dann wird einfach nichts getan. In diesem Fall könnte man eventuell auch eine Fehlermeldung ausgeben. Der Parameter r (wie root) in der rekursiven Funktion insert r zeigt auf den Anfang des zu untersuchenden Baums. Man beachte, dass es sich um einen Referenzparameter handelt. Die Löschoperation Einen Knoten kann man nicht so ganz einfach aus einem Baum herauslöschen. Beim Entfernen eines Knotens muss der Baum ja die Eigenschaft behalten, ein binärer Suchbaum zu sein. Falls der zu löschende Knoten außen “am Baumrand” liegt (keinen linken oder rechten Nachfolger hat), dann kann er einfach weggelöscht werden. Hat der zu löschende Knoten nur einen Nachfolger, dann kann der Verweis, der auf ihn selbst zeigt, auf diesen Nachfolger gerichtet werden. Der Fall, dass beide Nachfolger nicht existieren, ist ein Sonderfall vom Fall eines Nachfolgers. Der Zeiger auf ihn im Vorgänger wird auf 0 gesetzt. Also insgesamt: Zeigt r auf den zu löschenden Knoten mit maximal einem Nachfolger, dann ist die Löschoperation (ohne Speicherfreigabe, r ist der Verweis auf den zu löschenden Knoten): if (r->succ[0] == 0) //linker oder beide Nachfolger sind 0 r = r->succ[1]; //r wird durch seinen rechten Nachfolger ersetzt else if (r->succ[1] == 0) r = r->succ[0]; Im komplizierten Fall hat der zu löschende Knoten zwei Nachfolger. Er wird dadurch gelöscht, dass er durch einen Knoten vom Rand ersetzt und dieser entfernt wird: Im rechten Unterbaum wird der Knoten mit dem kleinsten Wert gesucht und dieser dann an die Stelle des zu löschenden Knotens geschoben. D.h. der Unterknoten “nach rechts unten” mit dem kleinsten Wert wird an die Stelle des zu löschenden gesetzt. Diese Aktion erhält die Eigenschaft binärer Suchbaum zu sein: Rechts unten sind nur größere Knoten; deren kleinster ist kleiner als alle anderen rechts unten, aber immer noch größer als alle links unten. Er kann also die neue Wurzel sein und die Stelle der zu eliminierenden alten einnehmen: if (r->succ[0] == 0) ... else if (r->succ[1] == 0) 3 ... else { // beide Unterbaeume existieren: string smallest_val; erase_min (r->succ[1], smallest_val); r->val = smallest_val; } Die Hilfsfunktion erase min löscht den kleinsten Unterknoten im übergebenen (rechten) Unterbaum und liefert dessen Wert in den Referenzparametern smallest val. Mit diesen Werten wird dann der aktuelle Knoten belegt. Damit wird dieser durch den gelöschten ersetzt. (Siehe Abbildung 2): 3 1 10 durch 12 ersetzen 10 9 8 13 12 19 Knoten löschen Wert in Knoten mit einem Kind löschen Wert (10) in Knoten mit zwei Kindern löschen Abbildung 2: Knoten im Suchbaum löschen Den kleinsten Unterknoten eines Baums zu löschen ist unproblematisch, da er sich “ganz links unten” befinden muss. Er kann keinen linken Unterknoten haben, da dieser ja kleiner sein müsste: void Tree::erase_min(Node * & r, string & s_val) { if (r->succ[0] == 0) { s_val = r->val; r = r->succ[1]; } else erase_min (r->succ[0], s_val); } Die Löschoperation insgesamt: void Tree::erase (const string &k) { erase_r (k, root); } void Tree::erase_r (const string &v, Node * &r) { if (r != 0) { if (v < r->val) erase_r (v, r->succ[0]); else if (v > r->val) erase_r (v, r->succ[1]); else { // dieser Knoten muss geloescht werden if (r->succ[0] == 0) { r = r->succ[1]; 4 } else if (r->succ[1] == 0) { r = r->succ[0]; } else { // Beide Unterbaeume vorhanden! string smallest_val; erase_min (r->succ[1], smallest_val); r->val = smallest_val; } } } } Aufgabenstellung 1. Lösen Sie alle Übungsaufgaben aus Kapitel 3. 2. Beweisen Sie an Hand einer spontan vorgegebenen einfachen Problemstellung, dass Sie den Stoff von Kapitel 3 beherrschen. 3. Trainieren Sie die – für Informatiker wichtige – Fähigkeit, abstrakte Definitionen zu verstehen, indem Sie die obigen Ausführungen über Bäume lesen und verstehen. (Es ist auch erlaubt bei Bedarf Bücher zu Rate zu ziehen.) Testen Sie Ihr Verständnis indem Sie angeben, wie die Werte als Binärbaum gespeichert werden können. 4. Erklären Sie den Weg von Gebäude F zur Bibliothek. Erläutern Sie in welchem Teilgebiet der Informatik Dinge wie binäre Bäume behandelt werden und wo in der Bibliothek Bücher zu diesem Thema zu finden sind. 5. Definieren Sie informal einen Algorithmus, mit dem ein Wert in einen binären Suchbaum eingefügt werden kann. Fügen Sie mit Hilfe dieses Algorithmus’ (auf Papier und mit Bleistift) alle Werte von oben in einen zunächst leeren Baum ein. 6. Definieren Sie informal einen Algorithmus, mit dem ein Wert in einem binären Suchbaum gesucht werden kann. Suchen Sie mit Hilfe dieses Algorithmus’ alle Werte von oben in dem eben erzeugten Baum. 7. Wie können alle Werte in einem binären Suchbaum in aufsteigender Reihenfolge und wie in absteigender Reihenfolge ausgegeben werden? Wie muss dazu der Baum durchlaufen werden? Geben Sie informal jeweils einen entsprechenden (rekursiven) Algorithmus an. 8. Machen Sie sich mit der Problematik des Löschens in einem binären Baum vertraut. Warum kann man nicht einfach einen Knoten streichen? 9. Definieren Sie einen konkreten Datentyp (Kopierkonstruktor, Zuweisungsoperator jeweils mit tiefer Kopie!) Tree für binäre Suchbäume mit Strings als Werten. 10. Schreiben Sie ein Programm zum Test Ihres Suchbaums. Ihr Programm liest von der Eingabe oder aus einer Datei Kommandos der Form INSERT LOOKUP LOOKUP DELETE DELETE Hugo Charlotte Hugo Hugo Detlef führt sie aus und gibt das Ergebnis der Ausführung etwa wie folgt aus: 5 OK: FALSE: TRUE: OK: ERROR: Hugo INSERTED Charlotte NOT FOUND Hugo FOUND Hugo DELETED Detlef NOT FOUND Ein Programmargument entscheidet, ob Ihr Programm interaktiv oder auf einer Datei arbeitet. Aufgabe 4–B (Basis–Hausübung) 1. Definieren Sie zu den Kommandos aus Aufgabe 2-B eine geeignete Klassenhierarchie mit Umschlagklasse (Cmd), einer Basisklasse und Ableitungen für die Varianten von Kommandos. Geben Sie ein entsprechendes Klassendiagramm in UML an. 2. Definieren Sie einen Eingabeoperator für Kommandos, der einen Kommando–Text einliest und eine Kommando– Variable mit einem entsprechenden Kommando–Objekt belegt. 3. Modifizieren Sie Ihr Programm derart, dass Kommandos mit Hilfe ihres Eingabeoperators eingelesen und dann verarbeitet werden. Erweitern Sie dazu Cmd um eine Methode string Cmd::evaluate(Tree &) zur Auswertung eines Kommandos und der Rückgabe der entsprechenden Erfolgs– bzw. der Fehlermeldung. 4. Modifizieren Sie Ihr Programm aus 2-B derart, dass es jetzt mit der Klasse Cmd, ihren Methoden und ihrem Eingabeoperator arbeitet. 5. Erläutern Sie den Begriff “Polymorphismus” und inwieweit sie in Ihrem Programm Polymorphismus zu welchem Zweck einsetzen. 6. Finden Sie heraus, was der Begriff “lexikographische Ordnung” bedeutet. 7. Finden Sie heraus, was die englischen Vokabeln to ascend und to descend bedeuten. 8. Erweitern Sie Ihre Lösung um ein Kommando zum Einfügen aller Strings, die in einer Textdatei zu finden sind, sowie um Kommandos zur Ausgabe aller gespeicherten Werte in lexikographisch auf– und absteigender Reihenfolge: INSERT-FILE datei.txt PRINT-ASCENDING PRINT-DESCENDING 9. Teilen Sie Ihr Programm in Übersetzungseinheiten auf: Übersetzungseinheit 1 für die Implementierung der Kommando–Klassen Übersetzungseinheit 2 für die Implementierung des Baums Übersetzungseinheit 3 für das Hauptprogramm. und schreiben Sie eine Make–Datei zur Produktion des Gesamtprogramms. 6