Dr. G. Zachmann A. Greß Universität Bonn Institut für Informatik II 1. Dezember 2004 Wintersemester 2004/2005 Übungen zu Programmierung I - Blatt 8 Abgabe am Mittwoch, dem 15.12.2004, 15:00 Uhr per E-Mail Aufgabe 1 (Pointer auf Structs, 3 Punkte) Gegeben seien die folgenden Definitionen der Structs Person und Abteilung: s t r u c t Person s t r u c t A b t e i l u n g { { i n t p e r s o n a l N r ; i n t a b t e i l u n g s N r ; A b t e i l u n g a b t e i l u n g ; Person l e i t e r ; } ; } ; Schreiben Sie eine Funktion, welche eine Person (oder alternativ einen Pointer auf eine Person) als Parameter erhält und für diese Person folgende Daten ausgibt: ihre Personalnummer die Nummer der Abteilung, bei der die Person angestellt ist ob die Person Leiter dieser Abteilung ist (Hinweis: Pointer-Vergleich) falls nicht, die Personalnummer des zugehörigen Abteilungsleiters Schreiben Sie zum Testen dieser Funktion ein Hauptprogramm, welches die folgenden Variablen-Belegungen verwendet und die obige Funktion für alle fünf Personen aufruft: Person anton, berta, c a e s a r, dora, e m i l ; A b t e i l u n g l a g e r, v e r k a u f ; anton. p e r s o n a l N r = 105; anton. a b t e i l u n g = & l a g e r ; b e r t a. p e r s o n a l N r = 114; b e r t a. a b t e i l u n g = & l a g e r ; c a e s a r. p e r s o n a l N r = 1 2 3 ; c a e s a r. a b t e i l u n g = & l a g e r ; dora. p e r s o n a l N r = 132; dora. a b t e i l u n g = & v e r k a u f ; e m i l. p e r s o n a l N r = 141; e m i l. a b t e i l u n g = & v e r k a u f ; l a g e r. a b t e i l u n g s N r = 1 ; l a g e r. l e i t e r = & b e r t a ; v e r k a u f. a b t e i l u n g s N r = 2 ; v e r k a u f. l e i t e r = & dora ; Hinweis: Aufgrund der gegenseitigen Abhängigkeit in den Definitionen der Structs Person und Abteilung benötigen Sie eine Forward Declaration 1. 1 Zur Erinnerung: Im allgemeinen müssen Structs in C++ vor ihrer Benutzung definiert worden sein. Um innerhalb einer Variablen- bzw. Member-Definition einen Pointer auf eine Struct verwenden zu können (wie in obigem Beispiel), genügt es aber auch, diese Struct vorher zu deklarieren (Forward Declaration). Analog zur Funktions-Deklaration gibt die Forward Declaration einer Struct nur den Namen der Struct an, nicht aber deren Inhalt (Member). Die Syntax der Forward Declaration lautet für eine Struct namens S wie folgt (man beachte die fehlenden geschweiften Klammern): struct S; 1
Aufgabe 2 (Zirkulär doppelt verkettete Listen, 10 Punkte) Im Gegensatz zur der in der Vorlesung vorgestellten einfach verketteten Liste enthalten die Knoten einer doppelt verkettete Liste nicht nur Zeiger auf die nachfolgenden Knoten, sondern auch Zeiger auf die vorausgehenden Knoten. In dieser Aufgabe soll die folgende Struct verwendet werden, welche einen Knoten der Liste repräsentiert: s t r u c t Node { i n t data ; Node l e f t ; Node r i g h t ; } data ist ein im Knoten gespeicherter Datenwert. left ist der Zeiger auf den vorausgehenden Knoten der Liste, right der Zeiger auf den nachfolgenden Knoten. In dieser Aufgabe soll eine zirkulär doppelt verketteten Liste verwendet werden, d.h. der left -Zeiger des Listenkopfes (also desjenigen Elementes der Liste, welches wir als erstes Elements auszeichnen) zeigt auf das Listenende und der right -Zeiger des Listenendes auf den Listenkopf, s. Abb. 1 (Somit sind die left / right-zeiger aller Knoten der Liste ungleich NULL.) a) Grundlegende Operationen auf Listen 1. Schreiben Sie eine Funktion Abbildung 1: Eine zirkulär doppelt-verkettete Liste Node newlistnode ( i n t data ) welche eine ein-elementige zirkulär doppelt-verkettete Liste erzeugt (s. Abb. 2), und dessen Kopf (welcher der einzige Knoten dieser Liste ist) zurückgibt. Benutzen Sie dabei den new-operator, um den Knoten der Liste dynamisch zu allozieren: Node n = new Node ; Der Funktionsparameter data bestimmt den Inhalt des Datenfeldes dieses Knotens. 2. Schreiben Sie eine Funktion Node a p p e n d L i s t s ( Node l i s t H e a d 1, Node l i s t H e a d 2 ) welche zwei Listen hintereinanderhängt, deren Köpfe durch die Parameter listhead1 und listhead2 gegeben sind, und einen Zeiger auf den Kopf der resultierenden zusammenhängenden Liste zurückgibt. Wird der Funktion ein NULL-Zeiger als einer der beiden Parameter übergeben, soll dies als eine leere Liste interpretiert werden. 2
Abbildung 2: Eine ein-elementige zirkulär doppelt-verkettete Liste 3. Schreiben Sie eine Funktion void p r i n t L i s t ( Node l i s t H e a d ) welche die in der verketteten Liste enthaltenen Datenwerte der Reihe nach auf der Standardausgabe ausgibt, beginnend mit dem als Parameter listhead übergebenen Listenkopf. 4. Schreiben Sie ein Hauptprogramm, welches von der Standardeingabe Integer-Werte einliest und dabei eine Liste aufbaut, welche diese Werte in der eingegebenen Reihenfolge enthält. Verwenden Sie dabei die Funktionen newlistnode() und appendlists(), um neue Knoten an die Liste anzuhängen. Sobald eine negative Zahl eingelesen wurde, soll die Konstruktion der Liste abgebrochen und der Inhalt der Liste mittels der Funktion printlist () wieder ausgegeben werden. (Es sollen also letzlich die eingelesen Werte wieder in derselben Reihenfolge ausgegeben werden.) Anm.: In dieser (und der folgenden) Aufgabe müssen Sie den allozierten Speicher nicht wieder freigegeben. b) Bubble Sort Ergänzen Sie das Programm aus Aufgabenteil a) wie folgt: Schreiben Sie eine Funktion Node s o r t L i s t ( Node l i s t H e a d ) welche die übergebene Liste aufsteigend sortiert, und einen Zeiger auf den Kopf der sortierten Liste (welcher den kleinsten Datenwert der Liste enthält) zurückgibt. Verwenden Sie dazu den folgenden Algorithmus (Bubble Sort): sorted false while not sorted do sorted true for all E L do if (E is not last node in L) (E.x > E next.x) then swap(e,e next ) sorted false end if end for end while Dabei bezeichnet E jeweils einen Knoten der Liste L und E next den jeweiligen nachfolgenden Knoten (welcher über den right -Zeiger erreicht wird). swap(e,e next ) besagt demnach, dass die Reihenfolge zweier benachbarter Knoten der Liste vertauscht werden soll. Hinweis: Im swap()-schritt können Sie einfach die Datenwerte der beiden Knoten vertauschen. (Alternativ können Sie aber auch die Zeiger umhängen.) Beachten Sie, dass der letzte Knoten in der Liste derjenige ist, dessen right -Zeiger auf den Listenkopf zeigt. Ergänzen Sie das Hauptprogramm aus Aufgabenteil a) um den Aufruf von sortlist (), so dass die eingelesene Liste in sortierter Reihenfolge wieder ausgegeben wird. 3
Aufgabe 3 (Binäre Bäume, 11 Punkte) Ein binärer Baum kann wie folgt rekursiv beschrieben werden: Ein binärer Baum B besteht aus einem Wurzelknoten w, welcher bis zu zwei Kinderknoten hat, die selber wieder Wurzelknoten binärer Bäume bilden (nämlich des linken bzw. rechten Teilbaumes des Knotens w). Außer dem Wurzelknoten w (der keinen Vaterknoten hat) haben alle Knoten des Baumes einen eindeutigen Vaterknoten. Ein Knoten, der keine Kinderknoten hat, wird Blatt genannt. Demgegenüber wird ein Knoten, der mindestens einen Kinderknoten hat, innerer Knoten genannt. In dieser Aufgabe soll die folgende Struct verwendet werden, um einen Knoten des Baumes zu repräsentieren: s t r u c t Node { i n t data ; Node l e f t ; Node r i g h t ; } Hierbei ist, anders als in Aufg. 2, left der Zeiger auf den Wurzelknoten des linken Teilbaums und right der Zeiger auf den Wurzelknoten des rechten Teilbaums. data ist wiederum ein im Knoten gespeicherter Datenwert. Falls left == NULL oder right == NULL besitzt der Knoten keinen linken bzw. rechten Teilbaum. (Bei einem Blatt sind demnach beide Zeiger NULL.) In dieser Aufgabe soll ein sortierter binärer Baum aufgebaut werden, d.h. für jeden inneren Knoten n des Baumes soll gelten: alle Knoten des linken Teilbaumes von n (insofern vorhanden) enthalten nur Datenwerte n. data und alle Knoten des rechten Teilbaumes von n (insofern vorhanden) enthalten nur Datenwerte > n.data (s. Abb. 3). a) Grundlegende Operationen auf Bäumen 1. Schreiben Sie eine Funktion Abbildung 3: Eine sortierter binärer Baum Node newleafnode ( i n t data ) welche einen nur aus einem Blatt bestehenden Baum erzeugt und dessen Wurzelknoten (also das Blatt) zurückgibt. Der Parameter data bestimmt den Inhalt des Datenfeldes dieses Knotens. (Verwenden Sie den new-operator, um den Knoten dynamisch zu allozieren.) 4
2. Schreiben Sie eine Funktion Node i n s e r t L e a f N o d e ( Node treeroot, i n t data ) welche in einen sortierten Baum, dessen Wurzelknoten durch den Parameter treeroot gegeben ist, ein neues Blatt mit dem durch den Parameter data gegebenen Datenwert einfügt und den Wurzelknoten des resultierenden sortierten Baumes zurückgibt. Ein leerer Baum wird durch treeroot == NULL repräsentiert (in diesem Fall muss also ein nur aus einem Blatt bestehender Baum zurückgegeben werden). Hinweis: Sie können diese Funktion wahlweise rekursiv oder iterativ implementieren. Erzeugen Sie das neue Blatt mit der obigen Funktion newleafnode(), und nutzen Sie die Voraussetzung, dass der gegebene Baum bereits sortiert ist, um einen geeigneten Vaterknoten für dieses Blatt zu finden. 3. Schreiben Sie eine rekusive Funktion void p r i n t T r e e ( Node t r e e R o o t ) welche die Datenwerte, die in dem durch den Wurzelknoten treeroot gegebenen sortierten Baum enthalten sind, in korrekter Reihenfolge auf der Standardausgabe ausgibt (also so, dass die Datenwerte tatsächlich in aufsteigender Sortierung ausgegeben werden). Hinweis: Achten Sie auf die korrekte Reihenfolge der rekusiven Aufrufe (und der Ausgabe) innerhalb der Funktion. 4. Schreiben Sie ein Hauptprogramm, welches von der Standardeingabe Integer-Werte einliest und dabei mit Hilfe obiger Funktionen einen binären Baum aufbaut, welcher diese Werte in aufsteigender Sortierung enthält. Sobald eine negative Zahl eingelesen wurde, soll die Konstruktion des Baumes abgebrochen und der Inhalt des sortierten binären Baumes mittels der Funktion printtree () ausgegeben werden. Anm.: Auch in dieser Aufgabe müssen Sie den allozierten Speicher nicht wieder freigegeben. b) Tree to List Obwohl der sortierte binäre Baum aus dieser Aufgabe und die zirkulär doppelt-verkettete Liste aus Aufg. 2 eigentlich zwei grundverschiedene Datenstrukturen sind, fällt auf, dass wir zur Repräsentation der Knoten bei beiden Datenstrukturen eine identisch aufgebaute Struct verwendet haben: bestehend aus einem Datenfeld und zwei Zeigern. Der einzige Unterschied liegt in der Bedeutung der left - und right -Zeiger. Eine interessante Aufgabe besteht nun darin, einen bestehenden sortierten binären Baum in eine zirkulär doppelt-verkette Liste umzubauen, indem man die in den Knoten enthaltenen Zeiger entsprechend umbiegt. Die Liste soll dabei so aufgebaut werden, dass die Sortierung erhalten bleibt (s. Abb. 4). Ein solches Umbauen des binären Baumes in eine zirkulär doppelt-verkettete Liste ist mit einer rekursiven Funktion in linearer Laufzeit (bzgl. der Anzahl Knoten) möglich: jeder Knoten muss insgesamt nur einmal bearbeitet werden. Schreiben Sie also eine rekursive Funktion Node t r e e T o L i s t ( Node t r e e R o o t ) welche, ausgehend von einem sortierten binären Baum mit Wurzelknoten treeroot, die left - und right - Zeiger der Knoten des Baumes so abändert, dass aus diesen Knoten eine zirkulär doppelt-verkettete Liste gebildet wird (unter Aufrechterhaltung der Sortierung). Die Funktion soll einen Zeiger auf den Kopf dieser Liste zurückgeben. 5
Abbildung 4: Umbauen des Baumes in eine Liste: Die left - und right -Zeiger des Baumes (vor der Umformung) sind in schwarz eingezeichnet. Die farbigen Pfeile zeigen die left - und right -Zeiger nach der Umformung in eine Liste. Beachten Sie, dass man, wenn man ausgehend vom Listenkopf head den rot eingezeichneten right -Zeigern folgt, die Zahlen 1 bis 5 in aufsteigender Reihenfolge besucht, genau wie in Abb. 1. Achtung: Sie dürfen hierbei weder neue Knoten allozieren noch die Datenwerte der Knoten umkopieren. Stattdessen sollen nur die left - und right -Zeiger der bereits bestehenden Knoten abgeändert werden. Hinweis: Die Rekursion ist der Schlüssel zur Lösung. Vertrauen Sie darauf, dass der rekursive Aufruf für beide Teilbäume korrekt arbeitet und konzentrieren Sie sich darauf, die resultierenden Listen dieser rekursiven Aufrufe mit dem Vaterknoten korrekt zu einer neuen Liste zusammenzusetzen. Sie können dabei die Funktion appendlists() aus Aufg. 2a) verwenden. Ergänzen Sie dann das Hauptprogramm aus Aufg. 3a), so dass der sortierte binäre Baum mit der Funktion treetolist () in eine sortierte Liste umgebaut wird, und diese Liste anschliessend mit der Funktion printlist () aus Aufg. 2a) ausgegeben wird. 6