3 Binärbäume Vorüberlegungen 2 Prozedural, funktional oder objektorientiert? 2 Der leere Baum 3 Beispiele und Terminologie 4 Allgemeiner Binärbaum 6 Minimaler Befehlssatz 6 Implementierung 7 Generierung von Binärbäumen 8 Elementare Operationen 9 Durchlauftechniken 11 Erweiterte Baumoperationen 15 Die zusätzlichen Baumoperationen im Einzelnen 16 Interaktive Baumerweiterung 16 Der Dialog beim Tierraten und die Erweiterung der Datenbasis. 17 Konstruktion des Huffmanbaumes 17 Aufbau des Strukturbaums 20 Syntaxdiagramme 21 Codeerzeugung und Syntaxprüfung 22 2 Kap. 3 Binärbäume Vorüberlegungen Prozedural, funktional oder objektorientiert? In der Veröffentlichung der Methoden zu Binärbäumen zum Zentralabitur in NRW findet man sowohl Funktionen, die selbst Binärbäume zurück liefern als auch Prozeduren, die in einem gegebenen Baum Veränderungen durchführen. Funktionale Sicht Die Sichtweise, das jede Operation auf dem Baum einen neuen Baum erzeugt, ist eine funktionale. Sie wird in den Baumkonstruktoren angewendet. Dieser Sicht folgende Änderungen von Inhalten führen jeweils zu neuen Bäumen oder liefern Bäume oder Inhalte. Dazu gehören die Konstruktoren mit einem Inhalt und mit einem Inhalt und zwei Teilbäumen und die Abfrage des Inhaltes in der Baumwurzel. Prozedurale Sicht Die übrigen Vorschläge von Operatoren im Zentralabitur folgen einer prozeduralen Sichtweise. Dazu gehört beispielsweise das Anhängen eines linken Teilbaumes an die Wurzel. Das Ergebnis ist kein neuer, sondern eine veränderter Baum. Beide zeigen eine Sicht auf den Baum von außen: Ein Binärbaum ist leer oder ein Element mit einer Verknüpfung mit zwei Binärbäumen Die Verknüpfungen und Operationen werden global gesetzt und gesteuert: Man verbindet von außen zwei Teilbäume zu einem neuen Baum oder regelt von außen das Einfügen. Objektorientierte Sicht Streng dem objektorientierten Paradigma folgend, wird der Baum nicht mehr als ganzes gesehen, sondern als einzelner Knoten, der über genügend Attribute und Methoden verfügt, um Aufträge selbst zu organisieren oder sie an die Nachbarknoten weiter zu leiten. Aus dieser internen Sicht handelt es sich um einen Knoten mit Inhalt und einer Komposition von bis zu zwei Nachbarknoten. Um beispielsweise ein Element, geht der Auftrag an den ersten Knoten. Das Objekt prüft selbst lokal, ob es zuständig ist oder ob ein Nachbarknoten benachrichtigt werden muss, um das Einfügen zu gestalten. Informationen zu diesem Ansatz bietet die Begleit-CD im Ordner binBaumOOP. Die Lösungen zu den Aufgaben sind in der Klasse BinTree zu ergänzen. Methoden und Techniken 3 Vorgehensweise Die Einführung beschränkt sich auf die notwendigsten (meist funktionalen) Methoden. Sie reichen aus, um beliebige Binärbäume zu konstruieren und zu manipulieren. Die prozeduralen Methoden vereinfachen manche Operationen und werden an entsprechenden Stellen eingeführt und implementiert. Die objektorientierte Sicht findet bei geordneten Bäumen Beachtung. Der leere Baum Wann ist ein Baum leer? Bei N.Wirth oder R. Sedgewick beispielsweise ist der leere Baum ein so genannter Nullpointer, also gleich null oder gleich nil. Sein Symbol ist ein Anker ─┤. Die Abiturvorgaben favorisieren stattdessen ein erzeugtes Objekt mit leerem Inhalt und zwei Ankern. Dieser Ansatz führt möglicherweise zu Irritationen, wenn Bäume ihre Informationen nur in den Blättern (s. u.) haben und die übrigen Verzweigung nur Strukturkriterien erfüllen und im Inneren zu leeren Knoten führen. Ein Beispiel ist der nachfolgend vorgestellte Huffmancode. Gemäß den Abiturvorgaben kann ein Baum nur leer sein, wenn er erzeugt ist. So wird er mit new BinTree() oder create(BinTree) implementiert, aber mit dem Ankersymbol dargestellt. 4 Kap. 3 Binärbäume Beispiele und Terminologie Strukturbaum Alternativen: (12 / 4) + (2 * 3) UPN: 12 4 / 2 3 * + Umgekehrt polnische Notation Suchbaum Alle Werte im linken Teilbaum der Wurzel sind kleiner als die Wurzel, alle Werte rechts davon sind größer. Die Knoten mit den Werten 4 und 78 bilden ihrerseits wieder Wurzeln von Binärbäumen, die Teilbäume des gesamten Baumes sind. Jeder Teilbaum unterliegt der gleichen Ordnung. Säugetier nein ja Hai Haustier nein ja Wolf 0 0 Hund 1 1 0 B D 0 C 1 R 1 A Frage-Antwort-Baum Der Binärbaum zeigt eine kleine Wissensbasis, die mit Ja/Nein-Antworten ein Problem eingrenzt. Interessant für Diagnosesysteme wird das Verfahren, wenn die Möglichkeit der Wissenserweiterung auf Grund von Fragen an den Benutzer besteht. Kodierungsbaum D. Huffman schlägt 1952 eine Codierung zur Kompression von Texten vor, die sich an der Häufigkeit der Buchstaben orientieren. Je häufiger der Buchstabe vorkommt, umso kürzer ist sein Code. Im Beispiel hat A den Code 11. Die Codierung von DA lautet: 1011 Die Bilder zeigen unterschiedliche Binärbäume. Bis auf die Endknoten, Blätter genannt, ist jeder Knoten mit ein bis zwei Nachfolgern (links und rechts) verknüpft. Der obere Knoten heißt Wurzel. Die Verbindungslinien wirken wie Zweige in einem nach unten wachsenden Baum. Sie werden in der Notation der Graphen (ein Binärbaum ist ein Graph) als Kanten bezeichnet. Die maximale Anzahl von Kanten auf dem Weg von der Wurzel zu einem Blatt heißt Tiefe (engl. depth) oder auch Höhe des Baumes. Methoden und Techniken 5 Übung 1.1 Zu jedem der oben beschriebenen Bäume bestimme man die Tiefe, die Blätter und zeichne den rechten Teilbaum und beschreibe die Bedeutung des Teilbaumes. Im Strukturbaum beispielsweise stellt der linke Teilbaum beispielsweise den Term 12 /4 dar. Strukturbaum Suchbaum Frage-AntwortBaum Code-Baum (Huffman) Tiefe Blätter Zeichnung des rechten Teilbaumes Interpretation des rechten Teilbaumes Übung 1.2 a) Strukturbaum Man zeichne den Strukturbaum zum Ausdruck 2*(4+7)-9 und gebe den Term in UPN aus. Um den Ausdruck in UPN aus dem Baum zu erschließen, lese man zunächst von der Wurzel ausgehend den linken Teilbaum aus, dann den rechten Teilbaum und schreibe schließlich das Wurzelelement auf. Die linken und rechten Teilbäume der Blätter sind leer. b) Suchbaum Wie viele Vergleiche mit Knoteninhalten sind notwendig, um herauszufinden, dass die Zahlen 3, 8 und 72 nicht im Binärbaum enthalten sind? c) Frage-Antwort-Baum Das gesuchte Tier ist eine Katze. Erweitern Sie die Wissensbasis! d) Huffmancode 1.Man interpretiere den Code 1100011110101110110001111 (aus Sedgewick: Algorithms) Verwenden Sie den Huffmancode aus der obigen Tabelle. 2. Verschlüsseln Sie mit Hilfe des abgebildeten 1 0 E Codes das Wort „SEEMANN“. Wieso eignet sich N der abgebildete Code für das Wort? M A S 6 Kap. 3 Binärbäume Allgemeiner Binärbaum Minimaler Befehlssatz BinTree (bzw. TBinTree) -value: Object -left: BinTree -right: BinTree <<create>> BinTree() // leerer Baum <<create>> BinTree(root: Object); <<create>> BinTree(root: Object, left: BinTree, right: BinTree) +getLeftTree():BinTree +getRightTree():BinTree +public getRootItem(): Object +isEmpty(): boolean Erläuterungen BinTree und TBinTree sowie Object und TObject sind jeweils Synonyme. Weitere Informationen: http://www.learn-line.nrw.de/angebote/abitur-gost/fach.php?fach=15 Konstruktor nachher BinTree() Ein leerer Baum existiert Kommentar: Auf Grund dieses Konstruktors entsteht ein leerer Baum, der sich von null oder nil unterscheidet. Er enthält ein leeres Element. Konstruktor nachher BinTree (Object v) bzw. create (v: TObject) Der Binärbaum existiert und hat einen Wurzelknoten mit dem Inhalt v und zwei leeren Teilbäumen (new Bintree() oder BinTree.create). Konstruktor BinTree (Object v, BinTree left, BinTree right) create (Object v, BinTree left, BinTree right) nachher Der Binärbaum existiert und hat einen Wurzelknoten mit dem Inhalt v, dem linken Teilbaum left und dem rechten Teilbaum right. Anfrage getRootItem(): Object vorher nachher Der Binärbaum existiert und ist nicht leer. Diese Anfrage liefert den Inhalt des Wurzelknotens des Binärbaums. Anfrage getLeftTree(): BinTree vorher nachher Der Binärbaum existiert und ist nicht leer Diese Anfrage liefert den linken Teilbaum des erzeugten Binärbaums. Der Binärbaum ist unverändert. Methoden und Techniken Anfrage getRightTree(): BinTree vorher nachher Der Binärbaum ist nicht leer Diese Anfrage liefert den rechten Teilbaum des erzeugten Binärbaums. Der Binärbaum ist unverändert. Anfrage isEmpty(): boolean nachher Diese Anfrage liefert den Wahrheitswert true, wenn der Binärbaum leer ist, sonst liefert sie den Wert false. Kommentar Die Anfrage bezieht sich nicht auf leere Zeiger wie null oder nil, sondern auf einen erzeugten Baum mit leerem Element und allen Baummethoden. Implementierung Java Delphi public class BinTree{ unit uBinTree; private Object value; interface private BinTree left; type TInhalt = TObject; private BinTree right; type BinTree = class private // Konstruktor 0, leer value : TInhalt; public BinTree() { left this(null, null, null); } : BinTree; right : BinTree; public constructor create; // Konstruktor 1, Wurzel constructor create(v: TInhalt); public BinTree(Object v) { constructor create(v: TInhalt, this(v, null, null); } li, re BinTree); function getRootItem: TInhalt; function getLeftTree: BinTree; //Konstruktor 3,Wurzel+Teilbäume function getRightTree: BinTree; public BinTree(Object v, BinTree li, BinTree re){ implementation value = v; constructor create; left = li; begin right = re; value:=nil; left:=nil; right:=nil; } end; public Object getRootItem(){ constructor create(v: TInhalt); return value; } begin value:=v; left:=nil; right:=nil; end; 7 8 Kap. 3 Binärbäume public BinTree getLeftTree(){ return left; } constructor create(v: TInhalt, li, re BinTree); begin value:=v; left:=li; right:=re; public BinTree getRightTree(){ end; return right; function getRootItem: TInhalt; } begin result := value; end; public boolean isEmpty(){ return value == null; function getLeftTree: BinTree; begin result := left; end; } function getRightTree: BinTree; } // BinBaumEnde begin getRightTree := right; end; function isEmpty :boolean; begin result := value; end; Generierung von Binärbäumen Dass die vorgestellten Konstruktoren ausreichen, um die Binärbäume der beschriebenen Art zu erzeugen, wird am Beispiel des Struktur- und des Suchbaumes vorgestellt. Die übrigen Generierungen gelten als Übungen. Zur Überprüfung liegt auf der Begleit-CD im Ordner Aufgaben zu diesem Kapitel ein Programm vor, das die Ausgabe des erzeugten Binärbaumes per Mausklick ermöglicht, falls er konstruiert ist. Die implementierte Methode zeige() veranlasst die Ausgabe des Binärbaums b, dessen Deklaration bereits erfolgt ist. Um den Algorithmus zur Darstellung des Binärbaumes zu verstehen, muss man sich etwas intensiver mit der Programmiersprache und deren Grafikfähigkeiten beschäftigen, als dies in diesem Kurs vorgesehen ist. Den Algorithmus findet man in der Klasse BBTools, die im Folgenden durch weitere hilfreiche Methoden ergänzt werden soll. Nehmen Sie die Implementierungen im Verzeichnis operationenGUI bei der Klasse BBAufgabenGUI vor. Dort finden Sie die jeweiligen Methodensignaturen zum Ausfüllen. Ein- und Ausgabeüberlegungen erübrigen sich. Der Strukturbaum zu (12 / 4) + (2 * 3) In Java BinTree b; BinTree b1 = new BinTree("/",new BinTree("12"),new BinTree("4")); BinTree b2 = new BinTree("*",new BinTree("2"),new BinTree("3")); b = new BinTree("+",b1,b2); In Delphi var b, b1, b2: BinTree; b1 := BinTree.create('/',BinTree.create('12'),BinTree.create('4')); b2 := BinTree.create('*',BinTree.create('2'),BinTree.create('3')); b := BinTree.create('+',b1,b2); Methoden und Techniken 9 Der Suchbaum (bis zur Tiefe 2) Hier existieren Knoten mit leeren Teilbäumen, die mit new BinTree() oder BinTree.create() zu erzeugen sind. In Java BinTree b; BinTree b1 = new BinTree("4",new BinTree(),new BinTree("34")); BinTree b2 = new BinTree("78",new BinTree("62"),new BinTree()); b = new BinTree("59",b1,b2); In Delphi var b, b1, b2: BinTree; b1 := BinTree.create ('4',BinTree.create,BinTree.create('34')); b2 := BinTree.create ('78',BinTree.create('62'),BinTree.create); b := BinTree.create ('59',b1,b2); Übung 1.3 Geben Sie Konstruktoren zum Frage/Antwortbaum und zum Codierungsbaum nach Huffman an. 0 1 Säugetier nein ja 0 1 0 1 Hai Haustier nein Wolf D B 0 ja Hund C A 1 R Die leeren Knoten beim Huffmancode sind mit Leerzeichen zu bewerten. Elementare Operationen Der Strukturbaum hat gezeigt, dass er je nach Leseart anders interpretiert werden kann. Es kommt darauf an, ob die Wurzel vor den Teilbäumen ausgegeben wird oder später. Durchlauftechniken dieser Art und andere Fragestellungen, zum Beispiel nach der Baumtiefe, sollen im Folgenden in einer (statischen) Werkzeugdatei (BBTools) zusammengestellt und implementiert werden. Damit wird der Boden für komplexere Anwendungen von Binärbäumen bereitet. Berechnung der Baumtiefe: tiefe (BinTree b): integer Der Aufruf erfolgt mit einem Baumexemplar meinBaum in Java: int t = BBTools.tiefe(meinBaum); in Delphi: t: integer; tool : BBTools; 10 Kap. 3 Binärbäume tool := BBTools.create; t := tool.tiefe(meinBaum); Die Tiefe ist gleich 0, wenn der Baum leer ist Die Tiefe des Baumes ist um eins größer, als das Maximum der Tiefen des linken und des rechten Teilbaumes. Betrachten wir zunächst die Funktion zur Berechnung des Maximums zweier natürlicher Zahlen und verwenden diese zur Lösung! In Java private static int maximum (int a, int b){ if (a <= b) return b; else return a; } Programmtechnisch bietet sich die Festlegung einer Tiefe von -1 für den leeren Baum an. public static int tiefe (BinTree b){ if(b.isEmpty())return -1; else return 1+maximum(tiefe(b.getLeftTree()), tiefe(b.getRightTree())); } In Delphi function maximum(a:integer; b:integer):integer; begin if (a <= b) then maximum := b; else maximum := a; end; function tiefe (b: BinTree): integer; begin if(b.isEmpty) then tiefe := -1 else tiefe := 1+maximum(tiefe(b.getLeftTree), tiefe(b.getRightTree)); end; Übung 1.4 a) Bestimmung der Anzahl der Knoten: knotenzahl (b: BinTree b): integer b) Ist eine Zeichenkette im Baum enthalten? enthalten(s: String, b: BinTree):boolean Falls der Baum leer ist, kann die Zeichenkette nicht enthalten sein. Enthalten ist die Zeichenkette, wenn sie mit dem Wert der Wurzel übereinstimmt oder im linken oder im rechten Teilbaum enthalten ist. Methoden und Techniken 11 Übung 1.5 Äußere Knoten in der Vertikalen Im Zusammenhang mit Suchbäumen spielt das am weitesten rechts oder am weitesten links liegende Element des Baumes eine gewisse Rolle, wenn gelöscht werden soll. Es handelt sich bei diesen Elementen um das größte und das kleinste des Suchbaumes. Im Ausgangsbeispiel sind dies die Zahlen 4 und 78. Die Strategie bei der Suche des am weitesten rechts befindlichen Elements liegt darin, den Kanten so weit nach rechts unten zu folgen, bis ein Knoten keinen rechten Nachfolger aufweist. ganzRechts(b: BinTree): Object Falls b leer ist, falls jedoch der rechte Teilbaum leer ist, gebe man den Wert der ist der Rückgabewert null oder nil, aktuellen Wurzel zurück, ansonsten liegt der gesuchte Wert ganzRechts vom linken Teilbaum, Man implementiere die rekursiven Methoden ganzRechts() und ganzLinks(). Durchlauftechniken + Die Knoten eines Baumes können in gewissen Reihenfolgen besucht und ausgegeben werden. Der Strukturbaum lässt unter Anderen die sinnvollen Auslesemöglichkeiten 12/4+2*3 und 12 4 / 2 3 * + zu. / 12 * 4 2 3 Im ersten Fall handelt es sich um eine Ordnung, die mit „inorder“ bezeichnet wird: Besuche zunächst den linken Teilbaum L, danach die Wurzel W und schließlich den rechten Teilbaum R. „in“ bezieht sich auf die Wurzel: 1. inorder: 2. preorder 2. postorder LWR WLR LRW besuche die Wurzel zwischen den Teilbäumen besuche die Wurzel vor den Teilbäumen besuche die Wurzel nach den Teilbäumen Wie der Baum, sind auch die Durchläufe rekursiv aufgebaut. Bei der Ordnung Links-WurzelRechts (lwr, inorder) muss das Pluszeichen warten, bis der linke Teilbaum durchlaufen ist: links linker Teilbaum (+) rechter Teilbaum (/) (+) rechter Teilbaum rechts 12 / 4 + links 12 / 4 + 2 (*) * rechts 3 Jeder Durchlauf besucht Knoten des Baumes in einer gewissen Ordnung und garantiert, dass jeder Knoten genau einmal besucht wird. Bei einer solchen Vorgabe spricht man von einem Euler-Durchgang (engl. euler tour traversal) nach dem Mathematiker Euler. 12 Kap. 3 Binärbäume Übung 1.6 a) Versuchen Sie zu verstehen, wieso der UPN-Term 12 4 / 2 3 * + dem PostorderDurchlauf LRW entspricht. Der Durchlauf erzeugt Postfix-Notation. b) Wie lautet die Ausgabe beim Preorder-Durchlauf WLR? Implementierunsvorschlag für den In-Order -Durchlauf Zunächst sollen die Inhalte in einer Ausgabe sequentiell dargestellt werden. Die entsprechende Methode laute ausgabe(Object). Als Erweiterung liefere die Methode die Ausgabe als Zeichenkette. Java Delphi public void inord(BinTree b){ PROCEDURE inord(b: BinTree); String s; VAR s: String; if (!b.isEmpty()) { BEGIN IF not b.isEmpty THEN inord(b.getLeftTree()); BEGIN inord(b.getLeftTree()); ausgabe(b.getRootItem()); ausgabe(b.getRootItem()); inord(b.getRightTree()); inord(b.getRightTree()); } } END END; Als Schreibziel kommt die Standardausgabe oder ein Textfeld (in TreeGUI: einAusgabe) in Betracht. Übung 1.7 a) Implementieren Sie die drei vorgestellten Baumdurchläufe. b) Statt der sequentiellen Ausgabe sollen die Werte entsprechend der jeweiligen Ordnung in eine Zeichenkette geschrieben werden. Als Trennsymbol eignet sich ein Strichpunkt. c) Die Baumelemente sind ordnungsgemäß in eine Liste zu schreiben. Zur Präsentation der Übungsergebnisse eignet sich eine graphische Benutzeroberfläche der im nachfolgenden Bild dargestellten Art. Der Prototyp liegt auf der Begleit-CD im Aufgabenverzeichnis dieses Kapitels vor. Die Methoden sind in der Klasse BBAufgabenGUI bereits vorbereitet. Es fehlt lediglich die Implementierung. Die grafische Ausgabe erfolgt automatisch. Das Ausgabefeld wird mit schreibe(Zeichenkette) angesprochen. Im Beispiel erscheint die Zeichenkette „Säugetier, Hai, Haustier, Wolf, Hund“, die im Programmverlauf zusammengestellt wird. Methoden und Techniken 13 Abb. 3. 1 Der Frage/Antwortbaum mit Preorder-Durchlauf Übung 1.8 a) Sowohl beim Huffmanncode, als auch im Fragebaum enthalten die Blätter wichtige Informationen. Es ist eine Methode zu entwerfen, die alle Blätter im Inorder-Durchlauf ausgibt. b) Der Huffmann-Baum ist so auszulesen, dass die Buchstaben und ihr Code tabellarisch zusammengefasst sind. c) Ein Dialog führt durch die als Binärbaum strukturierte Wissensbasis beim Raten von Tieren. Falls der gesuchte Tiername nicht enthalten ist, erfolgt eine entsprechende Ausgabe (schreibe(String)) wie „Unbekanntes Tier“ oder „Das Tier wurde erraten“. Es wäre wünschenswerte, die Wissensbasis dynamische erweitern und abspeichern zu können. Diese Erweiterung erfolgt später. Sie finden auf der Begleit-CD in der Datei BBAufgabenGUI Methoden namens blattAusgabe(), HuffmanCode() und tiereRaten(), welche die Funktionen und Prozeduren blaetter(BinTree,String):String, Huffman(String, BinTree): String und dialog(BinTree)aufrufen. Letztere sind zu implementieren. Statt die 14 Kap. 3 Binärbäume Ergebnisse in Zeichenketten zu sammeln, könnte zu diesem Zweck auch Listen in Frage kommen. Zeichenketten bieten den Vorteil der einfachen Ausgabe. Die Berechnung des Strukturbaumes Innere Knoten enthalten die Operatoren, in den Blättern stehen die Zahlen bzw. die Operanden. Für die Berechnung bedeutet diese Konstellation, dass während des Durchlaufs die Operatoren jeweils auf die linken und rechten Teilbäume angewendet werden müssen. Unter der Voraussetzung, dass der Baum nicht leer ist, bietet sich das folgende rekursive Verfahren an: Falls die Wurzel ein Blatt ist, also die linken und rechten Teilbäume leer sind, hat der Baum den Wert der Wurzel. Ansonsten bestimme man die Rechenergebnis der linken und rechten Teilbäume, o lese den Operator aus der Wurzel und o wende den Operator auf die beiden Ergebnisse in der berechneten Reihenfolge an. Übung 1.9 Kodieren Sie eine Methode zur Berechnung eines Strukturbaumes mit den möglichen zweiwertigen Operatoren Dividieren, Multiplizieren, Addieren und Subtrahieren. Sie finden auf der Begleit-CD in der Datei BBAufgabenGUI eine Methode namens berechnen(), welche die Methode berechne(BinBaum):double aufruft. Letztere ist zu implementieren. Ergebnis: 26.0 Wie der Baum mit aus dem Term 2 + (6 – 3) * 8 automatisch erzeugt werden kann, wird später behandelt. Vorerst genügt die Berechnung. Zur Erinnerung an die Konstruktion per Hand: erzeuge einen Baum mit dem Wert 2 baum2 erzeuge den Baum zu 6-3 (-, neuer Baum(6), neuer Baum(3)) baum63 erzeuge einen Baum aus (*, baum63, neuer Baum(8)) baum638 erzeuge einen Baum aus (+, baum2, baum638) b Methoden und Techniken 15 Erweiterte Baumoperationen In den vorangehenden Beispielen entstehen die Binärbäume ausschließlich mit Hilfe von Konstruktoren, die zu Beginn sämtliche Inhalte festlegen. Anhand der Ordnungskriterien muss es möglich sein, einen Baum während der Laufzeit eines Programms verändern oder erweitern zu können. Hier stellt sich die Frage, ob die eingeführten Baummethoden das Einfügen und Löschen erlauben oder, ob weitere Methoden notwendig sind. Schaut man auf die bei learnline veröffentlichten Empfehlungen, findet man weitere Methoden, die einer prozeduralen Denkweise folgen. Funktionale Programmierer benötigen diese zusätzlichen Operatoren nicht. Funktionaler oder prozeduraler Ansatz? Sollen Operationen, wie das Einfügen von Elementen, zu neuen Bäumen führen oder gegebene Bäume verändern? Das Beispiel der Verknüpfung der Wurzel mit einem linken Teilbaum verdeutlicht die Unterschiede von funktionaler und prozeduraler Sicht: Methode b.adtreeLeft zur Veränderung des linken Teilbaumes Funktional: b.addTreeLeft(tree: BinTree): BinTree entspricht <<create>>(b.getRootItem(),tree, b.getRightTree()) Prozedural: b.addTreeLeft (tree: BinTree): void prozedural Ausgangssituation b t b.addTreeLeft(c) b t b.addTreeLeft(c) b b t 5 5 2 funktional 9 2 c c 3 3 4 5 5 9 2 9 c 3 4 4 Beim funktionalen Ansatz wird zunächst mit Hilfe von c, der Wurzel von b und dem rechten Teilbaum von b ein neuer Binärbaum erzeugt (gestrichelter Pfeil). Durch die Zuordnung des neuen Baum zu b entsteht das Ergebnis. Im rechten Bild tangiert die Operation bei b den Baum t nicht, während t im mittleren Bild die Veränderung von b teilt. Ein solcher Fall verursacht einen Seiteneffekt auf t. Gleiches spiegelt sich in der Implementierung wider. Seien value der Wurzelinhalt (d.h. getRootItem()), left der linke Teilbaum (getLeftTree()), und right der rechte Teilbaum (getRightTree()). 16 Kap. 3 Binärbäume Funktional addTreeLeft(tree: BinTree): BinTree Rückgabe: new BinTree(value, tree, right) Prozedural addTreeLeft (tree: BinTree): void left = tree bzw. left := tree; Die zusätzlichen Baumoperationen im Einzelnen public void clear() public void setRootItem (Object pObject) public void addTreeLeft (BinTree pTree) public void addTreeRight (BinTree pTree) Dokumentation der Methoden der Klasse BinTree bei learnline. Auftrag nachher clear() Der Binärbaum ist leer. Auftrag setRootItem (Object pObject) nachher Die Wurzel hat unabhängig davon, ob der Binärbaum leer ist oder schon eine Wurzel hat, pObject als Inhalt. Eventuell vorhandene Teilbäume werden nicht geändert. Auftrag addTreeLeft (BinTree pTree) vorher nachher Der Binärbaum ist nicht leer. Die Wurzel hat den übergebenen Baum als linken Teilbaum. Auftrag addTreeRight (BinTree pTree) vorher nachher Der Binärbaum ist nicht leer. Die Wurzel hat den übergebenen Baum als rechten Teilbaum. Übung 1.10 Die angegeben Methoden sind in BinTree zu ergänzen und zu implementieren. Interaktive Baumerweiterung Der Fragebaum steht exemplarisch für 0/1-Bäume, die auch beim Huffmancode auftauchen. Ähnlichen Kriterien unterliegen Suchbäume. Deren Behandlung erfolgt auf Grund ihrer vielfältigen Anwendungsmöglichkeiten gesondert. Anlagen 17 Der Dialog beim Tierraten und die Erweiterung der Datenbasis. Entscheidende Antworten stehen in den Blättern, wie beispielsweise die Tiergattung Hund. Nehmen wir an, der Dialog hat bis zu diesem Endknoten geführt und es muss mit nein geantwortet werden. Statt des Hundes haben Sie sich nämlich eine Katze ausgedacht. Zu Erweiterung der Datenbasis muss dem System einerseits die Gattung Katze mitgeteilt werden, andererseits aber auch eine Frage, die beide Gattungen Hund und Katze voneinander unterscheidet. Bei der Beantwortung dieser Unterscheidungsfrage mit nein soll zum neuen Namen verzweigt werden. Übung 1.11 Entwerfen Sie einen Algorithmus für die Erweiterung der Datenbasis durch Befragung des Benutzers. Die Situation tritt während des Dialogs auf, wenn ein Blatt durchlaufen wird und die damit verbundene Frage mit nein beantwortet werden muss! Hund Frage: Hund? Antwort: nein Frage: Wie heißt die Gattung? Antwort: Katze Frage: Welche mit ja zu beantwortende Frage führt zum Hund statt zur Katze? Kann es bellen Katze Hund Antwort: Kann es bellen Das Beispiel zeigt, wie das Blatt mit dem Inhalt „Hund“ durch den Inhalt „Kann es bellen“ ersetzt wird. „Katze“ und „Hund“ sind Inhalte zweier neuer Bäume, die links und rechts anzubinden sind. Entwerfen Sie ein Programm zur Erweiterung der Datenbasis im Dialog. Der erweiterte Baum ist zwischenzuspeichern (z.B. b ratebaum ) und beim nächsten Dialogstart bereitzustellen (z.B. ratebaum b; zeige();). Konstruktion des Huffmanbaumes Übung 1.12 (Kreative Aufgabe zum Huffmancode) 18 Kap. 3 Binärbäume Ein ausschließlich aus Großbuchstaben und Leerzeichen bestehender Text ist mit dem Huffmancode zu komprimieren. Die Zeichenfolge „LEERE LEHRE “ diene als Beispiel. Zur Hervorhebung des Leerzeichens wird es als # dargestellt. Lösungsplan 1 Man bestimme die Häufigkeit aller Zeichen des Textes Buchstabe # E H L R Häufigkeit 1 4 1 2 2 2 Eine geordnete Liste diene als Speicher für Huffmancodebäume. Die Knoten des Huffmancodebaumes enthalten nicht nur die Buchstaben, sondern auch deren Häufigkeit. Die Häufigkeit dient als Ordnungskriterium: Wenn die Wurzel eines Baumes Knoteninhalt b1 eine kleineres Häufigkeitsattribut enthält als der eines zweiten Baumes +buchstabe:char +haeufigkeit: int b2, gehört b1 in der Liste vor b2. Zu Beginn bestehen sämtliche Bäume jeweils nur aus der Wurzel (Häufigkeit, Buchstabe). Die Liste: Die Knoteninhalte: # 1 H 1 L 2 R 2 E 4 Der beschriebenen Ordnung entsprechend, befindet sich der E-Baum am Ende der Liste. Huffman schlägt vor, die ersten beiden Bäume zu einem zu vereinigen, dessen Häufigkeit der Summe beider Häufigkeiten entspricht. Die Buchstaben spielen in der neuen Wurzel keine Rolle. Die beiden ersten Listenknoten sind zu löschen und der neue Baum ist gemäß seinem Häufigkeitsattribut in die Liste einzufügen. Die Liste: Die Knoteninhalte: L 2 2 # 1 H 1 R 2 E 4 Anlagen 19 Der neue Baum findet in der Liste als erstes Element seinen Platz, denn das nächste Element zeigt keinen höheren Häufigkeitswert. Anders gestaltet sich Situation im nächsten Schritt, bei dem ein Baum mit dem Häufigkeitswert 4 entsteht. Der Baum gehört in der Liste zwischen Rund E-Knoten. Die Liste: R 2 Die Knoteninhalte: E 4 4 L 2 2 # 1 H 1 Nach zwei weiteren Schritten entsteht schließlich der endgültige Baum, aus dem der Code erschlossen werden kann. Die Liste: E0 R 10 Die Knoteninhalte: # 1100 H 1101 L 111 LEERE LEHRE 11100100110011101101 10 E 4 6 R 2 100 4 L 2 2 # H 1 1 Vorgehensweise Bestimme die Häufigkeit der Buchstaben des Textes Erzeuge eine geordnete Liste der enthaltenen Buchstaben als Wurzel von Binärbäumen die nach der nach Häufigkeit geordnet sind Wiederhole die Zusammenfassung der beiden ersten Bäume der Liste nach dem oben beschriebenen Verfahren so lange, bis die Liste nur noch aus einem Element besteht. 20 Kap. 3 Binärbäume (erzeuge den neuen Baum, lösche die beiden ersten Listenelemente, füge den neuen Baum geordnet ein) Gib die Codetabelle aus. Komprimiere den Text entsprechend Aufbau des Strukturbaums Abb. 3. 2 Entstehung eines Strukturbaumes aus einer als Zeichnkette formuliertem Term Aufgabe Ausgehend von einem gültigen vorzeichenlosen arithmetischen soll der zugehörige Strukturbaum entstehen. Lösungsdiskussion Im ersten Schritt wird die Syntax arithmetischer Ausdrücke anhand von Diagrammen festgelegt. Es folgt die Syntaxprüfung unter Verwendung eines so genannten rekursiven Abstiegs. Im gleichen Durchgang entsteht der Strukturbaum. Der Ausdruck 2 + (6-3)*8 stellt eine Summe aus der Zahl 2 und einem Produkt dar. Anlagen 21 Ausdruck (6 + 2 - 3) * 8 Summand Summand 2 (6 + Zahl - 3) * 8 Faktor 2 + Faktor 6 - 3 * 8 Zahl Ausdruck In der Skizze fällt auf, das die Zahl 2 zunächst als Summand angesehen wird, um sich später als eine einzelne Zahl zu entpuppen. Gleiches gilt auch für den Faktor 8. Vor der endgültigen Auflösung bleibt ein Ausdruck als Faktor, dessen Analyse noch fehlt. Die mit Kreisen umrahmten Symbole lassen sich nicht weiter auflösen. Daher heißen sie terminale Symbole. „Terminal“ bedeutet so viel wie „endgültig“. Der Gesamtausdruck lässt sich gemäß der Skizze beschreiben als Summe von 2 und der Multiplikation eines Ausdruckes mit 8. So genannte Syntaxdiagramme verallgemeinern die Syntax eines beliebigen Ausdruckes: Syntaxdiagramme Ein arithmetischer Ausdruck ohne Vorzeichen ist ein Produkt, oder eine Folge von Produkten, die mit Plus- oder Minuszeichen verbunden sind. + Ausdruck Summand Ein Summand hat einen Faktor, oder er besteht aus einer Folge von Faktoren, die mit dem Mal- oder Teilungszeichen verknüpft sind. * Summand / Faktor Faktor Zahl ( Ausdruck ) Der Faktor kann eine Zahl sein oder ein geklammerter Ausdruck. Mit dem Oval um das Zahlsymbol wird dieses als quasi terminal 22 Kap. 3 Binärbäume angesehen Es handelt sich dabei um Folgen der Ziffern 0, 1, 2 bis 9. Mit dem nicht terminalen Symbol (Nonterminal) Ausdruck entsteht eine rekursive Beschreibung der Syntax. Eine Zeichenfolge, die auf dem gezeigten Graphen vom Startpfeil bis zu einem der Endpfeile führt gehört zur Sprache der auf ganze Zahlen und Grundoperationen beschränkten arithmetischen Ausdrücke. Sie enthält die terminalen Symbole {+, -, *, /, ), (, 0, 1, 2,… 9} und die Nonterminals {Ausdruck, Summand und Faktor}. Gestartet wird mit dem nicht terminalen Symbol Produkt. Jedes Zeichen des Ausdrucks, das nicht zur Menge der terminalen Symbole gehört, beendet den Ausdruck. Falls zu diesem Zeitpunkt der Graph nicht durchlaufen ist, handelt es sich bei der Zeichenkette nicht um einen gültigen Ausdruck. Codeerzeugung und Syntaxprüfung Als Code wird hier der Strukturbaum angesehen, der die Struktur und indirekt den Wert eines Ausdrucks repräsentiert. Zuständig für die Syntaxprüfung ist der Parser. Das Programm richtet sich nach gewissen Regeln: Regeln des Parsers Die Zeichenkette wird von Anfang an zeichenweise bearbeitet Begonnen wird mit dem Lesen des ersten Zeichens Solange gültige Zeichen vorkommen und das Ende der Zeichenkette noch nicht erreicht ist gehe man entlang des Pfades im Grafen Ein Nonterminal führt zum Aufruf eine gleichnamigen Prozedur oder Funktion Ein terminales Symbol führt zum Lesen des nächsten Symbols Das aktuelle Zeichen weist eindeutig den Weg Rückwärts gerichtete Pfeile werden als Schleifen interpretiert Die Strukturbaumerzeugung Ausgehend von einer den Ausdruck repräsentierende Zeichenkette term wird jeweils ein aktuelles Zeichen zeichen gelesen. Leerzeichen zwischen Operatoren sollen erlaubt sein. Sie werden einfach überlesen, da sie keine strukturelle Bedeutung haben. Aus praktischen Gründen wird die Zeichenkette mit einem Endzeichen # versehen. Die Maßnahme beugt einem Laufzeitfehler vor, der entstehen kann, wenn am Ende der Zeichenkette der Versuch unternommen wird, das nächste Zeichen zu lesen. String term; var term: String; char zeichen; var zeichen: char; int position = 0; var position: integer; boolean fehler = false; var fehler: boolean; //-------------------- position := 0; fehler := false; Anlagen 23 //--------------------------public void liesZeichen(){ while(term.charAt(position)== ' ') position++; zeichen = term.charAt(position); position++; } procedure liesZeichen(); begin while term[position]= ' ' do position := position+1; zeichen = term[position]; position := position+1;; end; public boolean istZiffer (){ return (zeichen >= '0' && zeichen <= '9'); function istZiffer: boolean; begin istZiffer := zeichen IN['0'.. '9']; } end; public String liesZahl(){ function liesZahl:String; String z=""; var z:String; while (istZiffer()) { begin z = z+zeichen; z := ''; liesZeichen(); while istZiffer do } // while return z; } //liesZahl begin z := z + zeichen; liesZeichen; end; end; public void fehler(String s){ System.out.println(s); error = true; } procedure fehler(s: String); begin writeln(s); error = true; end; Die Beispiele zeigen nützliche Operationen zum Lesen eines Zeichens, einer Zahl und der Abfrage, ob es sich bei einem Zeichen um eine Ziffer handelt. Falls ein Fehler während des Lesens auftritt, erfolgt eine gezielte Ausgabe. Der Fehler wird protokolliert, damit gegebenenfalls die Syntaxprüfung beendet werden kann. Nachfolgend finden Sie die Übersetzung des Syntaxdiagramms zum Ausdruck in Java, anschließend die entsprechende zum Summand in Delphi (als Prozedur). public BinTree liesAusdruck(){ Ausdruck BinTree baum = new BinTree(); BinTree teilbaum = new BinTree(); + - char op; baum = liesSummand(); while ((zeichen == '+') || (zeichen == '-')){ op = zeichen; Summand 24 Kap. 3 Binärbäume liesZeichen(); teilbaum = liesSummand(); if (error) break; if (op == '+') baum = new BinTree (baum, "+", teilbaum); else baum = new BinTree (baum, "-", teilbaum); }//while return baum; }//liesTerm() In Delphi PROCEDURE liesSummand(VAR b:BinTree); Summand * VAR operand : BinTree; / operator : TObject; BEGIN (*lies_ausdruck*) Faktor liesFaktor (b); WHILE zeichen IN ['+','-' ]DO BEGIN operator := Zeichen; liesZeichen; LiesProdukt (operand); b := BinTree.create(operator,b, operand); END; {WHILE} END; Beide Fälle demonstrieren die Übersetzungsregeln, Nonterminals als Unterprogrammaufrufe zu übersetzen und jedem Terminalzeichen das Lesen des nächsten folgen zu lassen. Die rückwärts gerichteten Pfeile leiten eine Schleife ein, die nur beim Auftreten der entsprechenden Symbole betreten wird. Das Diagramm zum Faktor Faktor Zahl ( Ausdruck ) zeigt die Alternative Zahl oder geöffnete Klammer. Da nur ein Zeichen zur Prüfung vorliegt leitet nur eine Ziffer den Aufruf von liesZahl ein. Falls keines der erwarteten Zeichen vorliegt, endet die Prüfung der Syntax und die Baumkonstruktion mit einem Fehler, da die Syntax nicht stimmt. Im Falle einer Ziffer, wird die Zahl geprüft. Handelt es sich bei dem Zeichen um eine offene Klammer, sind das nächste Zeichen und der Ausdruck zu lesen. Nach der Bearbeitung des Ausdrucks liegt das nächste Zeichen vor. Sein Wert entscheidet über den Anlagen 25 Fortgang entlang der Pfeile. Es muss eine geschlossene Klammer sein. Andernfalls bricht die Prüfung mit der Fehlermeldung „geschlossene Klammer erwartet“ ab. Übung 1.13 Implementieren Sie die Syntaxprüfung (engl. parser) und Erzeugung des zugehörigen Strukturbaumes. Als Eingabe liegt eine Zeichenkette vor, die vor der Überprüfung mit einer Endmarke (z.B. #) versehen wird, die kein terminales Symbol sein darf. Der Ausdruck soll auch Vorzeichen wie + oder – enthalten dürfen. o Ergänzen Sie den Syntaxgraphen o Implementieren Sie den Parser und Baumerzeuger Übung 1.14 (Kreative Vertiefung zur Syntaxprüfung und Strukturbaum) Die folgenden Syntaxdiagramme beschreiben eine Sprache logischer Ausdrücke. Logischer Ausdruck UndAusdruck ~ UndAusdruck + UndAusdruck Faktor Faktor Faktor w f ( OderAusdruck Fehler ) * 26 Kap. 3 Binärbäume 1. Zeichen Sie den Strukturbaum zum Term w + f * (w+f) 2. Skizzieren Sie den Parser und Erzeuger des Strukturbaumes 3. Erläutern Sie, wie der Strukturbaum zur Wertberechnung des boolesches Ausdruckes dienen. Es gilt: * w f w w f f f f + w f w w w f f w ~w f, ~f w