Goethe-Center for Scientific Computing (G-CSC) Goethe-Universität Frankfurt am Main Einführung in die Programmierung (EPR) (Übung, Wintersemester 2014/2015) Dr. S. Reiter, M. Rupp, Dr. A. Vogel, Dr. K. Xylouris, Prof. Dr. G. Wittum Aufgabenblatt 7 (Abgabe: Fr., 19.12., 9:29h online) Eine häufig verwendete und sehr nützliche Datenstruktur ist der Binärbaum. Ein Beispiel für einen Binärbaum ist wie folgt: gerichtete Verbindung (edge) Wurzel (root) Innerer Knoten (inner) Blatt (leaf) Die Elemente eines Binärbaums werden als Knoten bezeichnet. Die Knoten sind durch gerichtete Kanten verbunden. Dabei gilt in einem Binärbaum, dass von jedem Knoten nur maximal zwei Verbindungen ausgehen dürfen. Einer der Knoten ist dadurch ausgezeichnet, dass er keine eingehende Verbindung besitzt und wird als Wurzel (engl. root) bezeichnet. Alle anderen Knoten haben genau eine eingehende Verbindung. Geht eine gerichtete Verbindung von einem Knoten A zu Knoten B, so wird A als Elternteil und B als Kind bezeichnet. Hat ein Knoten keine Kinder, so wird er als Blatt (engl. leaf) des Baumes bezeichnet. Nimmt man zu einem Knoten seine Kinder und rekursiv deren Kinder, so erhält man wiederum einen Binärbaum, der als Teilbaum (engl. subtree) bezeichnet wird. Mit jedem der Knoten kann zusätzlich ein Wert (sogenannter Schlüssel) assoziiert werden. Auf diesem Aufgabenblatt soll dieser Schlüssel vom Datentyp int sein. Anschaulich ergeben sich dann z.b. folgende Bäume: 5 17 8 8 22 5 oder 11 oder 3 20 67 Um einen solchen Binärbaum zu realisieren, bietet es sich an eine Struktur für jeden Knoten einzuführen (vgl. verkettete Liste):
/// Node structure to represent a node in a binary tree struct Node { int value; ///< value associated with node Node* left; ///< left subtree ( == 0, if no subtree present) Node* right; ///< right subtree ( == 0, if no subtree present) ; Eine Instanz der Struktur Node repräsentiert dabei einen Eintrag in einem binären Baum. Die Membervariable value speichert den Wert, der mit dem Eintrag verbunden ist. Die Membervariablen left und right speichern einen Zeiger auf den linken bzw. rechten Teilbaum. Dies kann man sich anschaulich so vorstellen, dass die Knoten im Binärbaum folgendermaßen aussehen: 6 4 8 Ist eine der Verbindungen nach links oder rechts nicht vorhanden, so wird der Nullzeiger gesetzt. Ein Blatt ist somit eine Node, bei der beide Zeiger den Wert 0 besitzen. Ein Binärbaum heißt binärer Suchbaum, falls er zusätzlich die folgende Eigenschaft erfüllt: Die Schlüssel im linken Teilbaum eines Knotens sind kleiner oder gleich dem Schlüssel im Knoten selbst und die Schlüssel im rechten Teilbaum sind alle echt größer. Die oben dargestellten Bäume erfüllen beispielsweise diese Eigenschaft. Aufgabe 1 (4 + 6 + 4 + 4 + 6 + 6 + 1 Punkte) Schreiben Sie die folgenden Funktionen: a) Node* CreateNode(int value): Diese Methode soll eine Instanz der Struktur Node über den Befehl new erstellen, diese wie folgt initialisieren und als Zeiger zurückgeben: Die Membervariable value der neuen Struktur soll auf den übergebenen Wert value gesetzt werden, die beiden Membervariablen left und right sollen auf 0 gesetzt werden. b) void InsertNode(Node* root, Node* node): Fügt den übergebenen Knoten node in den übergebenen Baum root ein. Dabei wird angenommen, dass sowohl root als auch node bei der Übergabe die Eigenschaft eines binären Suchbaums erfüllen. Nach dem Einfügen von node soll diese Eigenschaft für root weiterhin gelten.
c) void InsertValue(Node* root, int value): Erstellt einen neuen Knoten mit dem Wert value und fügt diesen in den übergebenen Baum root ein. Nutzen Sie hierbei die Funktionen CreateNode und InsertNode. Achten Sie darauf, dass nach Terminierung auch hier root wieder die Eigenschaft eines binären Suchbaums erfüllt. d) Node* CreateTree(int* values, int size): Erstellt einen Baum und fügt die übergebenen Werte ein. values ist dabei ein Zeiger auf ein Feld von int-werten, size gibt die Zahl an Werten in diesem Feld an. Die Methode gibt einen Zeiger auf die Wurzel des erstellten Baums zurück. Nutzen Sie hierbei die Methoden CreateNode und InsertValue. Achten Sie darauf, dass der zurückgegebene Baum die Eigenschaft eines binären Suchbaums erfüllt. e) void PrintTreeSorted(Node* root): Gibt die Einträge des Baums root sortiert auf dem Bildschirm aus. Die Reihenfolge soll dabei in aufsteigender Ordung ausgegeben werden, d.h. niedrige Schlüssel zuerst. Man betrachte dazu das Beispiel aus Aufgabenteil (g). f) void DeleteTree(Node* root): Löscht den Baum mit Wurzel root, indem mittels des delete Befehls der Speicher aller zugehörigen Knoten frei gegeben wird. Neben der Wurzel root sind auch alle Kind-Knoten, also alle Knoten in den Teilbäumen root->left und root->right, zu löschen. g) Testen Sie Ihre Implementierung mit dem folgenden Programm: #include <iostream> using namespace std; //... Ihr Quelltext... int values[] = {7, 1, 3, 8, 2, 9, 5, 4, 6, 0; Node* tree = CreateTree(values, 10); PrintTreeSorted(tree); cout << endl; // Erwartete Ausgabe: 0 1 2 3 4 5 6 7 8 9 Hinweis: Für die Implementierung der Methoden PrintTreeSorted und DeleteTree ist ein rekursiver Ansatz empfehlenswert. Auch die Methode InsertNode eignet sich gut für eine rekursive Implementierung. Aufgabe 2 (6 + 6 + 6 + 1 Punkte) Erweitern Sie den Quelltext aus Aufgabe 1 um die folgenden Funktionen:
a) int NumTreeNodes(Node* root): Berechnet die Anzahl an Knoten des Baums mit Wurzel root (inklusive root) und gibt diese zurück. b) int TreeDepth(Node* root): Berechnet die Tiefe des Baums mit Wurzel root und gibt diese zurück. Die Tiefe eines Binären Baums mit Wurzel Node* t wird dabei formal wie folgt definiert: 0, falls t == 0 depth(t) = 1 + max(depth(t->left), depth(t->right)), sonst. c) void PrintTree(Node* root): Im Gegensatz zu PrintTreeSorted soll diese Funktion die Struktur des Baums auf dem Bildschirm ausgeben. Dazu wird jeweils der Wert eines Knotens ausgegeben. Hat der Knoten einen linken oder rechten Teilbaum, so wird anschließend das Symbol ( ausgegeben. In der Klammer wird nun zunächst der linke Teilbaum mittels PrintTree ausgegeben, gefolgt vom Symbol,, anschließend der rechte Teilbaum, wieder mittels PrintTree, gefolgt vom abschließenden Symbol ). Für das Beispiel aus Aufgabe 1(g) sollte die Funktion dementsprechend folgendes ausgeben: 7(1(0,3(2,5(4,6))),8(,9)) d) Testen Sie Ihre Implementierung mit folgendem Programm: #include <iostream> using namespace std; //... Ihr Quelltext... int values[] = {8, 12, 5, 24, 2, 6, 98, 67, 1, 4, 312, 380, 410, 612, 780; Node* tree = CreateTree(values, 15); cout << endl << "NumTreeNodes: " << NumTreeNodes(tree) << endl; cout << "TreeDepth: " << TreeDepth(tree) << endl << endl; Aufgabe 3 (Bonuspunkte: 4 + 5 + 1) Erweitern Sie den Quelltext aus Aufgaben 1 und 2 um die folgenden Funktionen: a) Node* FindNode(Node* root, int value): Gibt den Knoten in dem durch root gegebenen Baum zurück, der den Wert value enthält. Ist kein Knoten mit diesem Wert im Baum, so wird der Nullzeiger 0 zurückgegeben.
Nutzen Sie bei der Implementierung die spezielle Eigenschaft binärer Suchbäume, um bei der Suche jeweils möglichst wenige Knoten zu besuchen. b) Node* RemoveNode(Node* root, Node* node): Entfernt den Knoten node aus dem durch root gegebenen Baum. Die von node ausgehenden Teilbäume (node->left und node->right) sollen dabei nicht gelöscht werden und müssen dementsprechend wieder in den Baum root eingefügt werden. Die Funktion gibt einen Zeiger auf die Wurzel des so entstandenen Baums zurück. Es ist dabei darauf zu achten, dass der entstandene Baum wieder die Eigenschaft eines binären Suchbaums erfüllt und dass der Speicher des entfernten Knotens node frei gegeben wird. Nutzen Sie auch hier bei der Implementierung die spezielle Eigenschaft binärer Suchbäume, um beim Löschen jeweils möglichst wenige Knoten zu besuchen. c) Testen Sie Ihre Implementierung mit folgendem Programm: int values[] = {8, 12, 5, 2, 6, 67, 24, 98, 1, 4; Node* tree = CreateTree(values, 10); // remove an entry from the tree int delval = 67; cout << endl << "Removing " << delval << " from tree:" << endl; Node* delnode = FindNode(tree, delval); if(delnode) tree = RemoveNode(tree, delnode); cout << endl; Information zum Zeitrahmen: Zur Bearbeitung dieses Aufgabenblatts haben Sie zwei Wochen Zeit. Es empfiehlt sich frühzeitig mit der Bearbeitung zu beginnen und auftauchende Fragen gegebenenfalls in der Übungsgruppe zu stellen. Information zur Abgabe: Bitte schicken Sie Ihre Lösung als *.cpp Datei an Ihren Übungsgruppenleiter per Mail. Wichtig ist, dass Sie Ihre Lösung freitags vor 9:30h abgeschickt haben. Bitte schicken Sie nur den Quellcode, nicht jedoch ihr gesamtes Eclipse- Projekt oder ausführbare Dateien.