Basis–Haus¨ubungen 2-B und 4-B zu Programmierung II (C++)

Werbung
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
Herunterladen