Seite 1 von 68 Technische Universität München Prof. A. Knoll Einführung in die Informatik II Sommersemester 2003 Ausgearbeitet von Coskun Tayfur Vorbemerkung: In dieser Version sind noch diverse kleinere Fehler enthalten, da das korrigieren von diesen Kleinigkeiten sehr sehr viel Zeit in Anspruch nimmt, und ich sowieso schon sehr sehr viel Zeit für dieses Skript investiert habe und nun meine mündliche Diplomvorprüfung nun kurz vor mir liegt und weil gleich danach das Wintersemester beginnt, setze ich die Verion so online. Habt also bitte Nachsicht bei kleineren Rchteschriebfhlern ;-). Stuiden sgaen ürbigens, dsas die Bchusatben bleiebig vretuascht sien können und dsad der Mscenh sie imremncoh lseen knan, so lnag etsrer und ltzeer Bchusatbe rchitig sind. Also seids net so kleinlich Homepage: www.in.tum.de/~coskun Seite 2 von 68 Zusammenfassung Info2 Inhaltsverzeichnis Programmerstellung Definition Programmierung Definition Programm Grundtypen von Programmiersprachen Grundtypen von Programmierstilen Seite 4 Stack- Maschinen und –Sprachen Dreidimesinaler Stack-Maschine „Entwurfsraum“ Forth Entstehung und Verwendung Funktionen durch Wörter Arbeiten mit Zeichen Kontollstrukturen in Forth IF THEN ELSE CASE Zählschleife DO ... LOOP Rekursion in Forth Die Türme von Hanoi In Prolog In Forth Seite 4 - 15 Seite 4,5 Seite 5 - 15 Seite 6 Seite 7 Seite 7 Seite 8 - 9 Postscript Entstehung und Verwendung Vektor Graphikausgabe Einige wichtige graphische Operatoren Vektorfonds Operatoren und Arihtmetik Die Stacks von Postscript: Multi- Stack- Sprache Variablen in Postscript Prozeduren in Postscript Kontrollstrukturen in Postscript IF IFELSE Rekursion in Postscript Schleifen in Postscript Fraktale Wiederholung: Beziehung zwischen Chomsky Grammatik und Automaten. Turing - Maschine Definition der Turing Maschine Beispiel: Inkrementierung Halbierung von Unärzahlen Addition von Binärzahlen Seite 9 - 13 Seite 10- 15 Seite 11- 12 Seite 12-13 Seite 13 - 19 Seite 13 Seite 13- 14 Seite 14 Seite 14 Seite 14- 15 Seite 15 Seite 15 Seite 15 Seite 16 – 18 Seite 18 -19 Seite 20 Seite 20 - 27 Seite 22 Seite 23-24 Seite 25 Seite 26 Seite 3 von 68 Busy Beaver Funktionale Programmierung Rekursion lineare Rekursion Nicht lineare Rekursion Quicksort Permutation auf Sequenzen Verschränkte Rekursion Geschachtelte Rekursion Bsp.: Ackermann Terminierung funktionaler Programme Abstiegsfunktion Beispiel 1 ggT Beispiel 2 y = mn Beispiel 3 Ackermann Beispiel 4 Klaus Beispiel 5 Permutation von Sequenzen Aufwand von Algorithmen Korrektheit von Quicksort MergeSort Rekursive Datensturkturen Verschränkte Rekursion B- Bäume AVL – Bäume Funktoren: Datentypen mit Funktionen als Argument Algorithmus zur Tiefensuche Induktive Beweise über rekursive Sorten Das Element Bottom: Ordnugnen über Mengen Vollständige Halbordnung Anwendung des Kleenschen Satzes auf Strukturen funktionaler Sprachen: Semantik rekursiver Funktionsdeklarationen Beispiel (s. ÜB 5 Aufgabe 26) Funktionale Programmierung und Kalkül . Seite 27 Seite 28 Seite 28 Seite 29 Seite 30 Seite 32 Seite 32 – 33 Seite 33 - 37 Seite 3 Seite 35 Seite 36 Seite 37 Seite 37 Seite 38 – 40 Seite 41 Seite 42 Seite 44 Seite 45 Seite 46 Seite 47 Seite 47 Seite 49 Seite 49 Seite 52 Seite 52 Seite 53 Seite 54 Seite 58 Seite 60 Seite 59 - Seite 4 von 68 Programmerstellung Definition Programmierung ist die Umsetzung eines Algorithmus in eine Form, welche die Maschine interpretieren kann Definition Programm ist die Formuierung von Algorithmen in einer bestimmten Programmiersprache. Grundtypen von Progammiersprachen 1.) Funktionale Programmiersprachen 2.) Logik-basierte Programmiersprachen 3.) Imperative Programmierprachen Grundtypen von Programmierstilen 1.) Funktional 2.) Komponentbasiert imperativ 3.) Ereignisbasiert 4.) OOP (& Aspektorientiert) 5.) Maschinennah Stack- Maschinen und -Sprachen Stack Definition in Ocaml type ’a t = { mutable c : ’a list } exception Empty let create () = { c = [] } let clear s = s.c <- [] let push x s = s.c <- x :: s.c let pop s = match s.c with hd :: tl -> s.c <- tl; hd | [] -> raise Empty let length s = List.length s.c let iter f s = List.iter f s.c Infix- Darstellung (7 + 14) * 2 = 42 Postfix- Darstellung 2 7 14 + * = 2 21 * = 42 Kelleroperationen werden in der Postfix Darstellung gehandhabt. Sehen wir uns ein Beispiel an: in Position Stack 0 1 2 3 4 2 2 2 42 7 21 21 14 * * + * Schritt 1 2 3 4 Dreidimensionaler Stack- Maschine „Entwurfsraum“ 5 6 7 8 Seite 5 von 68 Stack- Maschinen können natürlich auch mehrere Stacks haben, um ihre Mächtigkeit zu steigern. Außerdem kann auch die Größe Variabel sein und auch die Typen, die in einem Stackplatz gespeichert werden können. Stackanzahl: Es können Stacks mit anderen Aufgaben existieren, z.B.: ein Return Stack zur Speicherung von Rücksprungadressen bei Unterprogrammaufrufen. Stackgröße: Wählt man optimal zwischen der Komplexität der zu entwickelnden Ausdrücke und den Kosten für den Speicher. Operatorenlänge: In einem Maschinen Befehlssatz können auch Operatoren durch opcodes gespeichert werden. Diese sind Zeiger auf eine andere Stelle im Computer, üblicher Weise der Hauptspeicher. Das heißt, der opcode gibt diejenige Adresse im Hauptspeicher an, ab die der eigentliche Befehl beginnt. Die Anzahl dieser im Stack aufeinander folgenden codes gibt die Operatorenlänge an. Üblich sind hier 0-, 1-, oder 2- opcode Maschinen. In der ALU (Arithmetic Logic Unit), welches das arithmetische Herz eines Computers ist können arithmetische Operationen wie Addition, Subtraktion, Logische Funktionen wie AND, OR, XOR ausgeführt werden. Das oberste Stackelement steht immer im TOS (Top of Stack) und ist damit für die ALU direkt als Operant verfügbar. Das zweite kann über den Datenbus (im Bild blau) an die ALU weiter gereicht werden. Dabei zeigt PC (Program Counter) immer auf den als nächstes auszuführenden Befehl im Hauptspeicher. Der Return Stack hat üblicher Weise eine Lifo Struktur und leistet hervorragende Dienste bei der Speicherung von Rücksprungadressen. Forth Wir wollen nun eine Stack- orientierte Programmiersprache kennen lernen: Forth. Seite 6 von 68 Entstehung und Verwendung 0- Operand Stack Machines sind in Forth direkt 1:1 umsetzbar. Es wurde von Charles Moore zur (Echtzeit-) Steuerung eines Observatoriums- Teleskops entwickelt. Das Ziel war es, die Programmier- Produktivität gegenüber Assembler Programmen zu steigern. Heute gilt Forth als Programmiersprache zur „direkten“ Kommmunikation zwischen Mensch und Maschine. Daher ist die Syntax auch Maschinenorientiert. Außerdem ist Forth Sprache, Betriebssystem und Maschinen-Monitor in einem. Forth zeichnet sich des weiteren durch seine Flexibilität aus, beispielsweise ist eine Spracherweiterung möglich, was natürlich sehr erwünscht ist. Programmieren mit Forth Es gibt verschiedene standardkonformer Versionen von Forth, sie lassen sich auf folgender site runter laden: www.forth.org/compilers Im Folgenden wird portable ANS FORTH (pforth) verwendet. Der Stack lässt sich durch Eingabe von Operanden ganz einfach füllen. Beipsiel: Begin AUTO.INIT 100 200 ok Stack<10> 100 200 300 + ok Stack10> 100 500 Wir schreiben in den Stack 100 und 200 Aktueller Stack Inhalt Wir schreiben 300 und + in den Stack => 100 200 300 + Da + ein zweitelliger Operand ist, wählt er die letzten beiden Werte aus dem Stack und wendet + darauf an. => 100 500. Da die Syntax sich dadurch von selbst erklärt folgen nun Befehle in Forth und ihre Beschreibungen. . swap rot dup over 2swap 2dup 2drop 2over -rot nip pick Tuck Entfernt TOS aus dem stack und gibt dieses aus. vertauscht die obersten beiden Elemente 1 2 3 4 1 3 4 2Das dritte Element von wird also an den TOS geholt und die letzten beiden nach unten verschoben. Wir rotieren also die ersten drei Elemente ein mal, Kopiert TOS. Wir haben also das oberste Element nun 2 mal dastehen. Kopiert das zweite Element auf den TOS 1 2 3 4 3 4 1 2 obersten 2 Zellen tauschen 12 1212 1 2 _ löscht die obersten 2 Elemente 1234123412 1 2 3 3 1 2 rückwärts rotieren 1 2 2 Entfernt Element unter TOS Position 1 2 3 4 5 1 2 3 4 5 6 7 Stack a b c d 2 a b c d b 12212 Seite 7 von 68 roll Position 1 2 3 4 5 1 2 3 4 5 Stack a b c d 2 a c d b Wir rotieren also durch das Element, was an der Position steht, welches wiederum im TOS angegeben ist – ganz einfach Funktionen durch Wörter Auch in forth sit es möglich Funktionen zu schreiben. Dazu speichert man Folgen von Daten und Opertaionen zusammen unter einem Wort ab und fügt es damit dem Forth- Wörterbuch hinzu,. Syntax: Doppelpunkt leitet Bezeichnung ein, Strichpunkt schleißt folge ab Beispiel: : sqr dup * ; Stack: 100 5 sqr 100 5 dup * 100 5 5 * 100 25 Arbeiten mit Zeichen Aud dem Stack können nur Zahlen abgelegt werden. Daher werden Zeichen und aamti vor allem Zeichenketten durch Zahlen repräsentiert. Beispiel: 72 EMIT 105 EMIT Hi ok Der Operator EMIT wandelt also den TOS gemäß Ascii in das entsprechende Zeichen um. Natürlich ist dies umgekeht mit dem Operator char ebenso möglich. Beispiel: char W ok Stack: 87 Achtung. Der Operator char arbeitet nicht auf dem TOS sondern auf dem folgenden Symbol. Zur Verarbeitung von Zeichenketten eignen sich nullstellige Funktionen: : gemuese .“ Kohl, ...“ Durch Eingabe von gemuese . wird die Zeichenkette dann wieder ausgegeben. Hier ist .“ der Operator, der den beginn der Zeichenktette definiert. Das Leerzeichen ist dabei sehr wichtig, sonst funktionierts nicht und hat schon manchen Forth Programmierer bei der suche nach dem Fehler zur Verzweiflung gebracht, also immer dran denken. “ definiert dann natürlich wieder das Ende der Zeichenkette. Anderes etwas kompleyxeres und mächtigeres Beispiel: : testkey .“ Hit a key: „ KEY CR .“ That = „ . CR ; CR steht hier für Corsor Return und KEY wartet bis der Benutzer eine Taste gedrückt hat und schreibt dann den entsprechenden Wert auf den Stack. Seite 8 von 68 Desweiteren ist es natürlich auch mögich und sinnvoll Programme seperat abzuspeichern und in forth einzulesen. Dies geht mit dem Befehel include. Es gibt Win Versionen von Forth wo dies auch mittels Drag & drop möglich ist. Kontollstrukturen in Forth Explizite Sprünge existieren nicht, da Forth eine strukturrierte programmiersprache darstellt. Jedoch wohl folgende Bedingungsreaktionen: IF (Bedinung) THEN IF (Bedingung) ELSE (false Code) THEN DO … LOOP: Zählschleife BEGIN… WHILE…UNTIL: While-Schleife BNF- Syntax der einfachen Verzweigung: <cond> IF <true-body> THEN . Gibt cond -1 = TRUE zurück, so wird der <true-body> ausgeführt. Bei der allgemeinen Verzweigung mit dem ELSE Zweig sieht die BNF Syntax folgendermaßen aus: <cond> IF <true-body> ELSE <false-body> THEN; Das wirkt recht ungewöhnlich und unnötigerweise kompliziert, ist es auch. Aber schwer ist es nicht, IF und THEN kann hierbei als Klammer betrachtet werden. So ergibt sich doch der Sinn des ganzen auch viel leichter. Wenn auf dem Stack ein IF gefunden wird, wird als erstes der vorangegangene Bool- Wert überprüft. Ist dieser -1, also inforth true, so wird der True Body ausgeführt. Ist der boolsche Wert jedoch 0, so wird der false Body ausgeführt, so einfach ist das. Einfaches Beispiel dazu: Programmname 1000 und > wird auf den stack geschrieben => Stack: x 1000 >. Ist x nun größer als 1000 so wird dies durch x 1000 ersetzt durch -1. ist x kleiner werden diese beiden durch 0, also false ersetzt. Enstprechend werden die Zweige ausgeführt. : gt1000 1000 > IF .“ zu gross“ THEN ; Case- Verzweigung : Testcase case 0 OF .“ Ausdruck 0“ ENDOF 1 OF .“ Ausdruck 1“ ENDOF 2 OF .“ ...“ ENDOF DUP .“ ungueltige Eingabe ENDCASE CR ; Zählschleife: DO ... LOOP Diese einfachste Form aller Schleifen, die Zählschleife hat in Forth folgende Syntax: Seite 9 von 68 <upper> <lower> DO <Loop-Body> LOOP Beispiel: : spell .” ba” 4 0 DO .” na” LOOP ; Was tut die Funktion spell? Nun, sie gibt zunächst einmal die Zeichenkette “ba” aus. In der nächsten Zeile beginnt die Schleife. Die 4 gibt dabei an, von wo an die Schleife beginnen soll. Nachjedem durchlauf wird diese Zahl dekrementiert. Sprich im zweiten durchlauf steht hier nur noch eine 3. Ist die folgende Zahl 0 mit der „Startzahl“ identisch, wird die Schleife verlassen, diese Schleife gibt also 4 mal hintereinander „na“ aus, nach dem sie „Ba“ ausgegeben hat. Der Stack durchlauft also folgende Schritte beim Aufruf von spell. 1.) 2.) 3.) 4.) 5.) 6.) ... spell ... ba 4 0 DO .“na“ ...bana 30 DO .“na“ ...banana 2 0 DO .“na“ …bananana 1 0 DO .”na” …banananana 0 0 Rekursion in Forth Wir zeigen die Rekursion in Forth am besten durch ein einfaches Beispiel: 1 2 3 4 5 6 : fak (n1 -- n2) dup 1 > IF DUP 1- recurse * ELSE DROP 1 THEN ; Analysieren wir mal dieses Programm in dem wir es in seine Einzelteile zerlegen und und überlegen was es macht. Schritt Stack 1 3 fak Beschreibung Hier wird also fak aufgerufen, 3 ist dabei unser n1 im Programm. 2 Und so sieht unser Stack dann aus. Wir können also nun schritt für schritt die Operatoren anwenden. 3 4 3 3 1 > IF DUP 1- recurse * ELSE DROP 1 THEN 3 -1 IF DUP 1recurse * ELSE Drop 1 Then 3 DUP 1 - da 3 > 1. Als nächstes kommt IF. Vor dem IF steht -1, also true. Der True Teil der Verzweigung wird also ausgeführt und der ELSE Teil verworfen. Dup verdoppelt also unsere drei, das 1 – danach zieht Seite 10 von 68 recurse * 5 3 2 recurse * 6 3 2 fak * 7 3 2 1 fak * * 8 9 10 321** 32* 6 dekrementiert die Kopie von unserer drei, so dass unser Stack im nächsten Schritt so aussieht: Recurse ruft die Funktion in der sie steht, also sich selbst noch mal auf, wie wir es von Rekursionen ja bereits gewohnt sind. Nun, hier geschieht das gleiche noch mal, ich verzichte hier auf die gleiche detaillierte Angabe, da alles ganz analog zum ersten rekursions schritt geschieht. So sieht dann unser Stack aus. Hier passiert jedoch was anderes. Denn wir erfüllen hier nicht mehr die Bedingung, dass TOS, also für die Funktion fak unserre 1 größer 1 sein soll. Wir landen also im False Zweig unserer Verzweigung. Das heißt unsere 1 wird gedropped, also fallen gelassen und durch eine 1 ersetzt, ein sehr unnötiger Schritt, meiner Meinung nach. Das Programm dürfte die selber Mächtigkeit haben, wenn man den ELSE Teil weglässt. Nun einfaches Anwenden der Operatoren 2*1=2 3*2=6 Die Türme von Hanoi Aufgabe: Versuche den dargestellten Stapel auf einen der anderen Pfähle umzuschiten. Dabei gilt: 1.) Die größeren Scheiben müssen immer unten liegen. 2.) Es darf nur eine Scheibe auf einmal bewegt werden. Lösungsidee: Die Lösung des Problems lässt sich rekursiv lösen. Dazu überlegen wir uns einfach wir haben einen Hanoi Turm mit n Scheiben. Den führen wir zurück auf n-1 Scheiben. Schauen wir uns das mal bildlich an: Die erste Zeile im Bild ist äquvialent zur zweiten. Wir verschieben anstatt des ganzen Turmes mit n Blöcken nur einen Turm mit n-1 Blöcken und lassen einen Block liegen. Wenn wir diesen Schritt rekursiv zurück denken, können wir so praktisch den ganzen Turm verschieben. Verscuhen wir uns das mal klarer zu machen. Wir fangen bei null an, und haben den Zustand wie in die erste Zeile in der Abbildung zeigt. Seite 11 von 68 - - So, nun nehmen wir den ganzen Stapel bis auf den letzten Block (wir stellen uns vor wir haben eine rekursive Funktion die n-1 Blöcke verschieben kann) und packen den auf third. Wir können nun den letzten Block leicht auf snd verschieben und packen dann wieder rekursiv den Turm mit n-1 Blöcken auf third nach snd. Wie würde diese Rekursion in Prolog aussehen? bewege(A, _, C,1) :- writef(‚Lege die oberste Scheibe von Turm %w auf Turm %w. \n’, [A,C]). bewege (A,B,C,N) :- M is N-1, bewege (A,C,B,M), bewege( A,B,C,1), bewege(B,A,C,M). anoi(N) :- bewege(a,b,c,N). Sehen wir uns ein Beispielaufruf in einem Baumdiagramm an: bw(a,b,c,3) bw(a,c,b,2) bw(a,b,c,1) bw(b,a,c,2) bw(a,b,c,1) bw(a,c,b,1), bw(c,a,b,1) bw(a,b,c,1) bw(b,c,a,1) bw(b,a,c,1), [a,c] [a,c] [a,b] [c,b] [b,a] [b,c] [a,c] Seite 12 von 68 Was sagt uns dieser Baum? Nun die eckig eingeklammerten Ausdrücke sind das Ende einer Rekursion, d.h. wir sind bei der Abbruchbedingun angelant. [a,c] bedeutet z.B.: „Bewege obersten Block von a nach b.“ Die Rekursionen werden der Reihe nach ausgeführt, bis sie jeweils ihre Abbruchbedinung erreichen, in diesem Programm ist die Abbruchbedinung die Stringausgabe in de rersten Zeile. Wir erhalten folgenden Ablauf: a 1 2 3 2 3 3 3 1 1 b c 1 2 1 2 1 2 2 1 3 3 2 3 1 2 3 Wobei 1 der kleinste Turm ist, 2 der mittlere und drei der größte, d.h die Zahlen dürfen nur von oben nach unten in der richtigen Reihenfolge auf einem Turm a, b oder c stehen. Das selbe Program in Forth : PRINTMOVE .“ move“ .. „-->“ . CR; : TOWERMOVE DUP 0 > IF ( Unterprogramname) ( „move“ wird ausgegeben) ( Die beiden obersten stackelemente werden entfernt und ausgegeben) ( Der Pfeil wird als String ausgegeben.) ( TOS wird ausgegeben) ( Ausgabe eines newline und Unterprogamm-Ende) (Wir kopieren TOS und prüfen, ob es geößer null ist) (reagiert auf den durch den Vergleich so eben entstandenen boolschen Wert) 1(dec TOS) 2OVER 2OVER (die letzten 4 Elemente kopieren und auf den Stack legen) >r >r >r >r ( die Kopien scheiben wir dann auf den return stack) 1 ROLL 2 ROLL 3 ROLL 3 ROLL (Stack wird manipuliert) RECURSE 2r@ ( Elemente aus dem Return Stack zurückholen) SWAP PRINTMOVE ( Vertausche und gib aus ) Seite 13 von 68 2DROP 2 DROP 2r> 2r> SWAP 3 ROLL SWAP RECURSE ( 4 Stack Elemente verwerfen) ( 4 zurück vom return stack) (and so an ...) THEN; : HANOI (n -- ) 3 1 2 3 ROLL TOWERMOVE (Stack wird vorbereitet, und die Funktion wird aufgerufen) 2Drop 2Drop; (Stack wird aufgeräumt) 3 HANOI (Aufruf Turmhöhe 3) Mit gutem Willen bin ich an dieses Programm ran gegangen um einen guten Überblick über die Funktion dieses Programms zu schaffen. Beim Versuch Zahlen auf den Returnstack zu schreiben ist allerdings mein PFORTH Scheiß abgestürzt und ich dachte mir: „Scheiß doch auf PFORTH!!!“. Also nächstes Thema: Postscript Entstehung und Verwendung (könnt ihr eigentlich gleich überspringen) Postscript ist eine Bildbeschrebungssprache der Firma Adobe. Das Ziel ist es eine einheitliche Darstellung von seiten auf Bildschrim, Drucker und anderen Peripheriegeräten zu erhalten. 1984 1985 1990 1991 1993 1999 Erste Spezifikation zu Postscript Erster Einsatz im Apple LaserWriter Type 1 Font- Format (heute gibt es melr als 50.000 Fonts die verfügbar sind Postscript Level 2 Adobe Acrobat Level3 Postscript enthällt vollständgie Ausgabe- Geräteunabhängige Beschreibungen Vollständig im Ascii Format => Betriebssystemunabhängig Stack- Maschine basiertes Desgin wie bei scheiß Forth Interpretierte Ausführung (Raster- Image- Prozessoren RIP) Postscript Drucker besitzen spezielle Hardware zur Sprachausführung, oder Hardware- RIP Lösungen zur Interpretation Ghostscript: GNU_ Postscript zur On-Screen Visualisierung Vektor Graphikausgabe Grundlage: Vektormodell (Im Gegensatz zur Rastergraphik) In Postscript stellt der Pfad (path) das wichtigste Gestaltungselement dar. Ein Pfad besteht zunächst aus einer unsichtbaren Folge von graphischen Objekten, welche somit den Umriss eines komplexeren Objektes beschreiben. Kleines Beispielprogramm: Seite 14 von 68 newpath 100 200 moveto 72 0 rlineto % initialisiert einen neuen Pfad % Setzen des Cursors an Koordinate [x] [y] % Zeichnen einer zum Cursor relativen Linie % und versetzen des Cursors den man net sieht. % wir gehen jetzt praktisch nach oben mit cursor und linie % setzen des Pfades (hier standardlinientyp) auf das Blatt % bzw. screen 0 72 rlineto stroke Einige wichtige graphische Operatoren Argumente xy xy xy xyrab xyrab Befehl newpath currentpoint lineto rlineto rmoveto arc arcn closepath stroke fill setlinewidth showpage Erklärung Startet einen neuen Path Aktueller Punkt Gerades Linienelement absolut relativeslineto (also vom cursor aus) relatives moveto Kreisbogensegment (entgegen Uhrzeigersin) ratet mal..... selbe wie grad nur um UZS schließt den Pfad zurück zum ersten Punkt Pfad beenden mei mei, man muss ja nicht alles erklären übersetzt einfach eigentlich auch klar aber trotzdem: Aktuelle Seite wird ausgegeben und gereseted, das ist der eignetliche ausgabe operator Vektorfonds Was sind Vektorfonds? Keine Ahnung, auf jeden Fall sind sie im FontDictionary abgelegt und es gibt über 50.000 Fonds, wie wir sehen drehen wir uns im Kreis in unseren Informationen, aber zurück zur Sache: In einem kleinen Beispielprogramm wird klar, wie man fonts in Postscript verwendet: 100 200 moveto /Helvectica findfont 24 scalefont setfont (Hallo Welt) show % wir setzen wieder unseren Cursor auf koordinaten wie % wir lustig sind % finde diesen font % skaliere diesen % setzen % Wir begrüßen die Welt mit einem netten hallo Operatoren und Arihtmetik Nun auf die Stackoperatoren gehe ich hier mal nicht näher ein, sie sind im wesentlichen ähnlich wie die von forth: pop exch dup usw... Auf die Arithmetik geh ich nur kurz drauf ein. Statt x y + für (x + y) schreibt man in Postscript x y add. Analog dazu x y sub und x y mul... zu den boolschen Operatoen gibt es auch nicht viel zu sagen, statt x y > wie bei Forth schreibt man x y gt bzw x y lt für kleiner. Seite 15 von 68 Die Stacks von Postscript: Multi- Stack- Sprache Postscript eine Multi- Stack- Sprache mit 4 Stacks. 1.) Operanden Stack Enthällt alle Objekte, die Operanden oder Ergebnisse von Operatoren sind. 2.) Ausführungsstack (nicht programmierbar) Enthält alle ausführbaren Objekte, d.h. im wesentlichen Prozeduren und Dateien, die sich noch in der Ausführung befinden. Aufgrund des Ausführung eines anderen Objektes unterbrochene Objekte können so ggf. fortgesetzt werden. Der Satz steht genauso auf den Folien, der verwirrt mich allerdings irgendwie. Ich denk gemeint ist: Wenn die Ausführung eines Objektes auf Grund dessen unterbrochen wurde, dass ein anderes Objekt ausgeführt wurde, kann das unterbrochene Objekt hier fortgesetzt werden. Ich hoffe mein Satz war besser 3.) Dictionary Stack (vgl- forth) Jeder Postscript Befehl wird vor Ausführung im Sict nach LIFO Proinzip gesucht. Explizite Befehlsverdeckung (auch der Systembefehele) ist somit möglich! Strukturierung nach: systemdict, userdict, errordict, Font Dictionary. 4.) Graphik- Status- Stack Aktueller Kontext für die Ausgabe graphischer Elemente Variablen in Postscript Es ist möglich getype Variablen zu definieren: bool, integer, real, string, array /Nummer 5 def /Gruesse (Hallo Welt) def % Nummer = 5 % Gruesse = Hallo Welt Hey sorry bin irgendwie müde und mein Arsch tut von diesem scheiß Stuhl weh, ich überspring die Erklärung da es ja sowieso selbstverständlich ist... Prozeduren in Postscript Prozeduren werden in Postscript ähnlich wie Variablen deklariert und verwendet. Syntax: /<name> {block} def. Die Parameterübergabe erfolgt dabei über den stack. Sehen wir uns dazu mal folgendes Beispiel zur #Berechnung der Hypotenuse an: /hypotenuse { /b exch def % stack: ... x /b exch def .../b x def => b = x /a exch def % das gleiche wie grad nur mit a a dup mult b dup mult add sqrt % Formel für Hypotenuse in Postfix % Darstellung } def Kontrollstrukturen in Postscript Verzweigungen – IF IFELSE Syntax: Einfache Verzweigung: < boole- expr> { <true- body> } if Seite 16 von 68 Verzweigung: < boole- expr> {true- body > } [ <false-body> } ifelse Auch hierfür ein kleines Beispiel: /signum { dup 0> {1} { dup 0 = { 0 } {1 neg} ifelse } ifelse } def Wie wir sehen haben wir hier eine Funktion welche tested, ob die Zahl auf dem TOS negativ, null oder positiv ist. Sie verdoppelt dazu zunächst die Zahl auf dem TOS. Mit dieser Kopie können wir nun einen boolschenVergleich durchführen ( 0 >). Das heißt, es wird getestet, ob die gerade eben verdoppelte Zahl größer null ist. Danach kommen die beiden Blöcke {1} { dup 0 = { 0 } {1 neg} ifelse } Das heißt, wenn die Bedinung war gewesen ist, wird der erste Block ausgeführt, und wenn dies nicht der Fall war, der zweite. Also geben wir, für den Fall dass TOS > 0 war eine eins zurück. Für den Fall dass TOS nicht größer null war muss mit einem weiteren boolschen Vergleich { dup 0 = { 0 } {1 neg} ifelse } erst noch getestet werden, ob die Hzahl auf dem TOS = null gewesen ist. Ist dies der Fall geben wir den ersten inneren Block { 0 } zurück, ist dies nicht der Fall bleibt nur noch, dass TOS kleiner null gewesen ist, das heißt wir geben den zweiten inneren Block zurück, indem die 1 negiert und zurückgegeben wird. Rekursion in Postscript Wie funktioniert Rekursion in Postscript? In Postscript ist dies durch einfachen Aufruf der Funktion selbst im Funktionsrumpf möglich. Wir sehen uns dazu das einfache Beispiel der Fakultätsfunktion an: /factorial { 1 dict begin /n exch def n 1 eq { 1} { n n 1 sub factorial mul } ifelse end } def % Wenn n = 1 ist Abbruchbed. % wenn n nicht eins ist, Rekursion Problem: Die Variable n wird in das Dictionary geschrieben und bleibt dort innerhalb des begin… end BEreiches gültig. Bei großen Argumenten kann es deshalb zu einem Überlauf des Dictionary- Stacks kommen. Auch nichtlinear rekursive Funktionen können in Postscriopt geschrieben werden. Sehen wir uns dazu doch mal die Implementierung von den Türmen von Hanoi in dieser Sprache an: Seite 17 von 68 /bewege { 4 dict begin % bereite im dictionary Platz für 4 Variablen vor /n exch def % n = was auf dem stack for dem n stand /c exch def % c = was vor dem n stand /b exch def % b = was vor dem b stand /a exch def % a = was vor dem b stand n 1 eq { % Block 1 currentpoint (Lege die oberste Scheibe von Turm ) show a show ( auf Turm ) show c show (.) show moveto 0 -12 rmoveto } { } end % Block 2 a c b n 1 sub bewege a b c 1 bewege b a c n 1 sub bewege ifelse } def Im Block rot markiert ist der Vergleich. Ist n = 1 wird wieder Block1 ausgeführt, sonst Block 2. Im Block eins ist die Ausgabe eines Strings programmiert. Diese stellt die Abbruchbedinung der Rekursion dar. Werfen wir noch mal einen Blick auf die Abbruchbedingung in Prolog: bewege(A, _, C,1) :- writef(‚Lege die oberste Scheibe von Turm %w auf Turm %w. \n’, [A,C]). Wir erkennen das diese eigentlich genau das gleiche macht. Genauso analog zum Prologprogramm ist auch die eigentliche Rekursion in Block2: bewege (A,B,C,N) :- M is N-1, bewege (A,C,B,M), bewege( A,B,C,1), bewege(B,A,C,M). vergleicht dies mit dem Inhalt aus Block2 und wir erkennen sofort, dass beide Programme den selben Algorithmus implementieren. Schleifen in Postscript Einfache Zählschleife, Syntax: <lower> >step> <upper> { >body> } for; Der Programmrumpf wird hierbei mehrmals ausgeführt. Bei jedem Durchlauf wird die untere Grenze <lower> um <step> erhöht, bis schließlich <upper> erreicht wird. Schauen wir uns dafür doch mal ein kleines Programm an, welches ein reguläres n- Eck (Radius = 1) am Bildschirm ausgibt: Seite 18 von 68 /drawpolygon { 4 dict begin /N exch def /A 360 N div def 1 0 moveto 1 1 N {A cos A sin lineto /A A 360 N div add def } for closepath end } def Fraktale Was ist ein Fraktal? Ein Fraktal ist eine geometrische Figur, die durch einen mathematischen Algorithmus definiert ist, der zu einer Form führt, welche die Eigenschaft der Selbstähnlichkeit aufweist. Darunter versteht man die Eigenschaft, dass bei der Vergrößerung der Figur immer neue Verzweigungen und Muster erscheinen, wobei sich die Grundformen im Kleinen wiederholen. Ein sehr berühmtes Fraktal, ist der Barnley Farn: Der Farn wird mit einem verblüffend einfachem Algorithmus als eine Folge von Punkten erzeugt, wobei der nächste Punkt vom jeweiligen Vorgängen gemäß: x’ = a*x + b*y +e y’ = c*x + d*y +f abhängt. Diese Art Gleichungen nenn man in der Mathematik liebevoll „affine Transformation“. Barnsley hat für das Farn folgende 4 Konstanensätze definiert. Bei jedem Schritt wird einer davon zufällig ausgewählt: A B C D E F 0 0 0 0.16 0 0 0.85 -0.04 -0.04 0.85 0 1.6 0.2 0.23 0.26 0.22 0 1.6 -0.15 0.26 0.28 0.24 0 0.44 Wollen wir uns nun die Implementierung dieses Fraktals in Postscript aussieht: %!PS-Adope-2.0 4.25 72 mul 1.5 72 mul translate % translate: Zeichen- Ursprungs- Verschiebung 0.8 72 mul dup scale % scale: Skalierung des Koordinatensystems 1 setlinecap % Form der Linienenden 0.005 setlinewidth % Liniendicke 00 % x –y Startwerte belegen 150000 { % 150000 Iterationen rand 100 mod % Zufallswert zwischen 1 und 100 bestimmen dup 1 lt % und Koeffizienten auswählen { pop [0.00 0.00 0.00 0.16 0.00 0.00] } { dup 86 lt { pop [0.85 -0.04 0.04 0.85 0.00 1.60]} { 93 lt {[0.20 0.23 -0.26 0.22 0.00 1.60] } Seite 19 von 68 {[-0.15 0.26 0.28 0.24 0.00 0.44]}ifelse } ifelse} ifelse transform 2 copy moveto % Linear- Transformieren and so an ... 0.01 0.001 rlineto stroke } repeat Und angeblich soll das dabei rauskommen: Nun ok, das sieht ja sowas von geil aus, das sollte man mal ausprobieren.. Probier... Wau... es funzt tatsächlich, probiert es aus... Wie cool! Wiederholung: Beziehung zwischen Chomsky Grammatik und Automaten. Wiederholen wir zunächst die unterschiedlichen Chomsky- Typen von Grammatiken, die wir aus dem ersten Semester kennen: Typ 0: Die Regeln unterliegen keiner Einschränkung aber es gibt welche, wie z.B. für das Deutsche (sog. Phrasenstrukturgrammatiken). Typ 1: Kontextsensitive Grammatiken Regeln der Form wie oben dargestellt. Außerdem: die Länge der abgeleiteten Wörter |w| nimmt nicht ab von Ableitungsschritt zu Ableitungsschritt. Für alle Regeln w1 w2 aus P gilt: |w1| ≤ |w2| Typ 2: Kontextfrei. Für alle Regeln w1 w2 aus P gilt, dass w1 eine Variable ist, d.h. w1 є V. Typ 3: Regulär. Zusätzlich zu Typ2 sind die rechten Seiten der Regeln w1 w2 entweder Terminalsymbole oder ein Terminalsymbol gefolgt von einer Variablen , also w2 є { A A V} Chomsky 3 < DEA (deterministischer endlicher Autaomat) / NEA Chomsky 2 < Kellerautomat Chomsky 1 < ? Chomsky 0 > ? Wie wir wissen, oder zumindest mal wussten und uns in Erinnerung holen sollten sind Chomsky 1 und Chomsky 0 sprachen so nicht zu modellieren. Da aber alles geht, geht auch das . Nur wie? Die Turing Maschine verspricht Hilfe... Zunächst aber noch ein Beispiel für einen Kellerautomaten. Wir wollen einen Automaten der folgende Sprache akzeptiert:L = {a1...an$an...a1 | ai {a,b}}. Ein Wort dieser Sprache wäre also zum Beispiel: abba$abba Seite 20 von 68 Lösungsidee: Immer wenn wir ein a lesen, pushen wir ein A in den Keller. Analog pushen wir ein B in den Keller, wenn wir ein B lesen bis wir beim $ angekommen sind. Hier poppen wir dann die A und die B, sofern wir jeweils auch wieder a bzw. b gelesen haben. Mit dieser Methode kommen wir nur dann beim # im Keller an, wenn das Wort Teil der Sprache L gewesen ist. Also: a,#/ A# b,#/ B# a,A/AA a,B/AB b,A/ BA b,B/ BB Dabei heißt a,#/ A#: auf dem Eingabeband wird a gelesen, und auf dem Keller befindet sich ein #. Da # aus dem Keller beim lesen entfernt wird, müssen wir es wieder hinzufügen, deshalb A#. a,A/ b,B/ ,#/# Z0 $,#/# $,A/A $,B,B Z1 Turing Maschine Nun, um das im letzten Abschitt beschriebene Problem zu lösen hatte Turing (1912 – 1984) eine spitzenmäßige Idee: Der clevere Kerl erfannd die Turing Maschine. Er verwendet dabei einen sequentiellen Speicher in Form eines Speicherbundes, auf das sowohl lesend, als auch schreibend zugegriffen werden kann. Zur Verdeutlichung schauen wir uns folgendes Abbild der Turingmaschine an: e i n g a b e s L R p e i c H e r Endliches steuerlemement hier Rot markiert ist der Schreib. Lesekopf der sich nach links und rechts bewegen kann., dabei schreiben und lesen kann. Die Beschreibung des Arbeitsablaufes erfolgt durch eine Übergangsfunktion: (z,a) = ( z`, b,x) Wir interpretieren das folgenermaßen: Zu Beginn befindet sich die TM im Zustand z und liest a. Im Berechnungsschritt geht die SE (Steuereinheit) von z nach z`, es wird „b“ geschrieben und der Kopf bewegt sich nach x { L (links) ,R(rechts) ,N (nichts) } Graphisch: a /b, x z z’ Seite 21 von 68 Beispiel: Die Sprache L = { anbncn >= 0 } Bekannt: L ist nicht vom Typ 2, kann also nicht von einem KA akzeptiert werden. Um eine Turing Maschine für L zu konstruieren benötigt man das Konzept der TM in voller Allgemeinheit. Insbesondere genügt der sog. LBA – Automat (Linear Bounded Automat, d.h. der Schreibkopf bewegt sich nur auf dem Eingabereich). Lösungsidee: Ausgangszustand: SK (Steuerkopf) steht auf dem ersten Eingabezeichen Markierung des ersten a durch a` Verschiebung des SK nach rechts, bis b gefunden wird; Markierung von b durch b`. Verschiebung des SK nach rechts bis das erste c gefunden wird; Markierung von c durch c`. Verschiebung nach links, bis a` gefunden wird. Wenn kein a mehr gefunden wird, muss sichergestellt werden, dass alle b und c markiert sind fertig. Kurz drüber nachgedacht macht klar, dass des funktionieren muss. Graphisch sieht dieser Automat für die Sprache L = { anbncn >= 0 } also folgendermaßen aus: Graphisch: a/ a`, R 1 b/ b`, R 2 a/ a, R b’/ b’, R 5 / ,R 4 b / b, R c’ / c’, R a`/ a`,R b’/b’,R c/ c`, L 3 b/ b, L b`/b’, L c’/c’, L a/ a, L 6 b’/b’,R c`/ c`,R Machen wir schnell ein Beispiel durch. Wir wissen aabbcc ist Tel der Sprache L. Der Schreibkopf befindet sich also auf dem erssten Element des Wortes aabbcc. Wir befinden uns Seite 22 von 68 im zustand 1, ersetzen also a durch a’ und bewegen den Schreibkopf eins nach rechts. Außerdem wechselt der Zustand von 1 nach 2. Unser Wort sieht also fogemdermaßen aus: a’abbcc wobei sich der Schreibkopf auf dem roten a befindet. Im Zustand 2 finden wir hierfür die Anweisung a/ a, R d.h. wir ersetzen a durch a, tun also nichts und bewegen den Schreibkopf weiter nach rechts und bleiben im Zustand 2. Dies wird solange so weiter gehen, bis entweder ein b oder b’ gefunden wird. Wird ein b’ gefunden sollen diese auf die gleiche Art und Weise übersprungen wie schon die a übersprungen wurden. Schließlich wird ein b gefunden was den autopmaten in den dritten Zustand wechselt und dieses b durch ein b’ ersetz damit markiert. Mit c wird im Zustand 2 ganz analog verfahren. Im Zustand 4 bewegen wir nun den schreibkopf soweit nach links zurück, bis ein a’ gefunden wird. Im Zustand 1 wird nun das zweite a durch a’ markiert usw. Wie wir an der Abbildung des Automaten sehen kann wird erst dann in zustand 5 gewechselt, bis kein a, sondern ein b’ gefunden wird, d.h. alle vorhandenen a wurden durch a’ bereits ersetzt und das wiederum bedeutet, dass auch alle b und c markiert sind, sofern das Wort Teil der Sprache L ist. Deshalb testen wir in Zustand 5 noch, ob tatsächloch alle b und c ersetzt wurden und nur dann, wenn wir nach dem Wort die Leere Speicherzelle, die durch ein Viereck gekennzeichnet wird, erreichen, war das wort tatsächlichn Teil von L. Fertig. Satz (ohne Bew.) 1) Menge der Typ – 1 Sprachen wird genau durch die Menge der LBA- Automaten beschrieben 2) Menge der Typ- 0 Sprachen werden durch die Menge der TM beschrieben. Unterschied zwischen LBA und TM: LBA bewegt sich nur auf dem Eingabeband, TM auch darüber hinaus. Definition der Turingmaschine Die Turingmaschine TM ist ein 7-Tupel: M = { z, A, Γ, , z0, , E } z: A: Γ: : endliche Zustandsmänge Eingabealphabet Arbeitsalphabet: Γ A Z x Γ Z x Γ x {L,R,N} (determ. TM) Z x Γ P (Z x Γ x {L,R,N }, wobei P als „ Platzmenge“ bezeichnet wird (nicht determ. Übergangsfunktion). z0 z: Anfangszustand Γ \ A: Speicherbegrenzungszeichen E c Z : Menge der Endzustände Definition: (Konfiguration einer TM) Eine Konfiguration einer TM ist ein Wort t Γ* z Γ*. Erläuterungen: Die Konfiguration einer TM ist also eine Momentaufnahme einer TM. Sei R = z eine Konfiguration. Dann ist β das Speicherband. Der Lesekopf befindet sich genau über dem ersten Zeichen von β. Seite 23 von 68 Ableitung mit Hilfe von TM Def: Auf der Menge der Konfigurationen einer TM sei folgende 2 – stellige Relation gegeben. m >= 0 , n <= 1 a1...am z b1...bn a a1...am z ' cb2 ...bn , ( z, b1 ) ( z ', c, N ) a1...am cz ' b2 ...bn , ( z, b1 ) ( z ', c, R) a1...an 1 z ' am cb2 ...bn , ( z, b1 ) ( z ', c, L) Sonderfälle : n = 1 und maschine läuft nach rechts. a1...amzb1 a1...am c z’ falls ( z, b1 ) ( z' , c, R) m = 0 und Maschine bewegt sich nach links z b1 ... bn z’ c b2... bn falls ( z, b1 ) ( z' , c, b) Randbemerkung: in der Vorlesung wurde das Relationszeichen ohne Pfeil benutzt. diese Ableitungen sehen kompliziert aus, sind es aber natürlich nicht. Um das primitive in der Sache offensichtlicher zu machen hier nun eine Beschreibung der ersten genannten Ableitung, die oben dargestellt ist. Wir haben also : a1...am z b1...bn a a1...am z ' cb2 ...bn , ( z , b1 ) ( z ', c, N ) ( z, b1 ) ( z ', c, N ) das bedeutet also, wir gehen vom Zustand z in den Zustand z’ zbd ersetzen dabei b1 durch c. N bedeutet dass der Schreibkopf Nichts tut also weder nach rechts noch nach links geht. Nichts anderes steht rechts von dem Relationspfeil. In der zweiten oben genannten Ableitung haben wir die Übergangsfunktion ( z, b1 ) ( z ', c, R) . Der einzige unterschied ist also, dass wir den Schreibkopf eins nach rechts verschieben. Deshalb befindet sich das z’ eins rechts vom c. Ganz einfach also. Um nun auch die Motivation für das ganze ein wenig zu stärken zeigen wir eine einfache Verwendung füt die TM. Beispiel: TM zur Inkrementierung von Binärzahlen Wir überlegen uns zunächst was beim Inkrementieren von Binärzahlen passiert. Wir haben also z.B. b = 1001. b+1 wäre dann 1010. Hätten wir aber 1000 sehe b inkrementiert so aus: 1001. Ist also das erste Bit eine 0, so wird sie einfach durch eine 1 ersetzt. Ist dagegen diese eine 1, so wird die 1 mit einer 0 ersetzt und mit dem zweiten Bit fortgefahren und der gleiche Vergleich ausgeführt. Wie könnte man das in einer TM implementieren? Nun logisch ist, dass wir den Schreibkopf zunächst ans erste Bit bewegen müssen. Dann kann der Vergleich mittels einem Automaten ganz einfach durchgeführt werden. Unsere Lösungidee sieht also folgendermaßen aus: Seite 24 von 68 Lösungsidee: (Beispiel einer Startkonf. Z0 101 ) Bewege Schreibkopf zum rechten Wortende und bleibe in Zustand z 0. Wenn rechtes Ende erreicht (d.h. Kopf über ), dann Zustandsübergang Z0 Z1 und bewege Kopf nach links. Wenn Kopf über 0, schreibe 1 gehe nach Z2 und bewege Kopf weiter nach links Wenn Kopf über 1 gehe nach links Wenn Kopf über (linkes Ende ist erreicht und noch keine Inkrementierung) schreibe 1 und gehe in Endzustand Ze Bewege Kopf solange nach links, bis linkes Ende erreicht wird und gehe dann in Endzustand Z e Wir errinern uns an die Definition der TM: M = { z, A, Γ, , z0, , E } Wir wollen nun die einzelnen Tupel- Glieder angeben. Wir haben also die Zustände : z = (Z0, Z1, Z2, Ze). Unser Eingabealphabet sind entweder „0“ oder „1“. Unser arbeitsalphabet enthällt zusätzlich noch . Die Übergangsfunktion ist . Anfangszustand z0 . Das leere Wort und unser Endzustand ze. Damit hättten wir schon alles was wir brauchen. Also: TM = {(Z0, Z1, Z2, Ze), {0,1}, {0,1, }, , Z0, , Ze } Wobei (Z0,0) = (Z0,0,R) (Z0,1) = (Z0,1,R) (Z0, ) = (Z1,,L) (1) (2) (3) Diese 3 bewegen den Schreibkopf ganz nach (Z1,0) = (Z2,1,L) (Z1,1) = (Z1,0,L) (Z1, ) = (Ze,1,N) (4) (5) (6) Wenn „0“ schreib „1“ und geh Links z1 z2 Wenn „1“ schreib „0“ geh links und bleib in z1 Wenn dann Endzustand. Fertig. (Z2,0) = (Z2,0,L) (Z2,1) = (Z2,1,L) (7) (8) Im Zustand z2 wurde die Binärzahl bereits inkrementiert. Wir verschieben den Sk nur noch (Z2, ) = (Ze, ,R) (9) links bis zum Wortende, der durch markiert ist. rechts nach Beispiel für Ableitung: Z01010 1Z0010 10Z010 101Z00 Z21011 Z21011 Z21011 1010Z0 101Z10 10Z211 1Z2011 Ganz analog: Dekrementierung von Binärzahlen. Lösungsidee: Um Binärzahlen zu dekrementieren, muss ein Suffix der Form 10000 auf die Form 01111 gebracht werden. Beispiel: 1100 – 1 = 1011 Die Regeln der Übergangsfunktion (1) .. (3) wie beim Inkrementieren Statt Regeln (4) und (5) nunmehr: (Z1,0) = (Z1,1,L) Wir bleiben also in z1 (Z1,1) = (Z2,0,L) Wir gehen also in z2 über. Regel (6) tritt nicht auf, weil Binärzahlen keine „führenden Nullen“ haben Regel (7) und (8) wie beim Inkrementieren Statt Regel (9) (Z2, ) = (Z3, ,R) Seite 25 von 68 (Z3,1) = (Ze,1,N) (Z3,0) = (Z3, ,R) Stellen abgeleitet Halbierung von Unärzahlen Zunächst schreiben wir uns 2 Hilfsprogramme für die Turingmaschine, welche das Band ganz nach links bzw. rechts bewegt. Links: Die Maschine L(inks) wird mit folgender Konfiguration gestartet: #|......|start# und endet mit end#|...|#. Die gesuchte Turingmaschine ist hierbei gegeben durch: L = {{start, end, move}, {|}, {|}, L, start,#,end} Wir sehen die Übergansfunktionen aus? Wir lesen also im Startzustand ein ‚#’. Das heißt wir lassen das unverändert, indem wir auch ein ‚#’ schreiben und bewegen den Schreibkopf nach links. Außerdem wechseln wir den Zustand zu move. Hier lesen wir ein ‚|’ und ersetzen dieses wieder durch ‚|’ lassen es also auch unverändert. Wir ändern den Zustand nicht, das heißt wir gelangen rekursiv soweit nach links, bis was anderes als ‚|’ gelesen wird. Wenn wir dann wieder ein ‚#’ lesen, lassen wir dies wieder unverändert, bewegen den Schreibkopf nicht und wechseln in den Endzustand. Fertig. Also sehen wir uns mal die Übergangsfunktionen an: (start,#) = (move, #, L) (move,|) = (move,|,L) (move,#) = (end, #,N) Der passende Automat dazu: (|/|,L) start (#/#,L) move (#/#,N) end rechts: R(echts) ist ganz analog zu L, deshalb lass ich es hier aus. Wir setzen dann im nächsten Schritt voraus, dass wir R schon haben. Div2 Nun soll also diese Unärzahl in Form von ||||... halbiert werden. Das heißt wir müssen es schaffen dass nur noch halb so viele ’|’ auf dem Band sind, wie vorher. Es gibt mehrere Möglichkeiten dies zu Verwirklichen. Wir werden folgende verwenden: Von der EinEingabe werden iom allgemeinen 2 ’|’ Zeichen gelöscht. Links der Eingabe wird eine neue Zeichenkette aufgebaut, die von der Eingabe durch ein # getrennt ist und das Ergebnis enthällt; nach jedem Löschen von 2 solchen Zeichen muss der Ergebnissbereich ums eines erweiter werden. => L= {[start,end,zero,one,two,stepl,append,steprr,stepr},{|}{|},D , start,#,end} Seite 26 von 68 So sieht unser passender Automat dazu aus. Vielleicht ist es hier angebrachtein Beispiel durchzugehen. Nehmen wir also an, wir wollen 4 halbieren. Unser Eingabeband sieht also so aus #||||start#. Leiten wir mal hier einfach der Reihe nach die einzelnen Schritte ab: #||||start# #|||zero|# #||one|## #||two## stepl#||# append##|| steprr|#|| #|#||stepr# #|#||start#... Wir sehen bereits dass es funktioniert, also brech ich ab Addition von Binärzahlen Grundsätzlich Vorgehensweise Da x + y = (x – 1) + (y + 1) nach x- maligem Dekrementieren des 1. Summanden und xmaligem Inkrementieren des 2. Summanden wird der 1. Summand 0 und der 2. Summand ist die Lösung. Damit Addition möglich durch Kombination der TM für Inkrement und Dekrement. Unbedingt noch nachzutragen.... Siehe Übungsblätter... Seite 27 von 68 Die fleißigen Bieber ( „Busy beaver Problem“) Formulierung durch T.Rador 1965 Gegeben: det. TM Arbeitsalphabet = {| , } Unbeschränktes Schreibband, zu Beginn leer, d.h. enthällt nur Übergangsfunktion muss so beschaffen sein, dass Kopf sich nur nach links oder rechts bewegt, darf nicht stehenbleiben. Es gibt genau ein Halteabstand, der nicht zu den Zuständen zählt. Busy- Beaver Funktion T(M): Maximale Anzahl von Strichen, die eine TM mit n Zuständen (ohne Haltezustand) auf das Band schreiben kann und danach hällt. Beispiel n = 3: / |, R | / |, R / |, L q1 q2 | / |, R q3 H / |, L | / |, L (q1, ) (q2,|,R) (q1, |) (q3,|,L) (q2, ) (q1,|,L) (q2, |) (q2,|,R) (q3, ) (q2,|,L) (q3, |) (H,|,R) Ableitung: q1 | q2 | | | | | | | | q1 | | q3 | | q2 | | | q1 | | | | | q2 | | | | ... Seite 28 von 68 Man kann zeigen: T(3) = 6 Eine TM, die die maximale Zahl von Stirchen auf das Band schreibt, heißt „Busy Beaver“. Man kann zeigen, dass die Busy- Beaver- Funktion eine nicht berechenbare Funktion ist, da nicht einmal festgestellt werden kann, ob eine TM mit n Zuständen überhaupt anhällt (Halteproblem der Informatik.) Man kann zeigen: „ 1 2 3 4 5 6 T(n) 1 4 6 13 >= 4098 >= 95524079 Funktionale Programmierung Zentrale Strukturen und Algorithmen Bisher kennen wir rekursive Funktionsaufrufe (linear, einstellig) Datenstrukturen wie z.B.: Listen und Bäume. Ziel dieses Kapitels ist es: 1.) 2.) 3.) und rekursive Vertiefung der Rekursion über Funktion Terminierung rekursiver Funktionen Verallgemeinerung / Vertiefung rek. Datenstrukturen: Polymorphie, Pattern matching Korrektheit von Implementierungen Wichtigsten rekursiv zu formulierenden Algorithmen 4.) 5.) 1.) Vertiefung rekursiver Funktionen Definition (informell): Wir sprechen von Rekursion, wenn auf der rechten Seite einer Funktionsdefinition der Form let f1 = (m x) n: E im Ausdruck E die zu definierende Fkt. F1 auftritt. m x n E Sorte Parametersatz Sorte der Rückgabewerte Berechnungsausdruck Seite 29 von 68 Allgemein: Sei let f1 = ( m1 x1,…, mnxn)n1:E1 eine Funktionsdeklaration. Dann ist f1 rekursiv, wenn der Bezeichner f1 im Ausdruck E1 enthalten ist (also: rein syntaktisch prüfbar!) Lineare Rekursion: Der Bezeichner f1 tritt in E1 innerhalb einer Verzweigung einer Fallunterscheidung höchstens ein mal auf. Beispiel: a) der durch den euklidischen Algorithmus definierte GGT ist linear rekursiv b) Die Funktion lowerpart, welche die Elemente einer liste l berechnet, die kleiner als ein best. Element el sind, ist linear rekursiv let rec lp (el, liste) = match (el, liste) with | (el, []) [] | (el, a :: rest) when (a<el) a::(lp el rest) | (el, a:: rest) lp el rest ;; Nichtlineare Rekursion: Der Bezeichner f1 tritt in E1 innerhalb eines Zweiges einer Fallunterscheidung mind. 2 mal auf. Beispiel: a) Sortieralgorithmus Quicksort (Moore) Sortieren durch Zerlegen o Wähle ein beliebiges Element x aus der zu sortierenden Menge M aus. (x heißt auch „Pirot- Element“) o Zerlege M in die Mengen M< = { L| L<x}, M= = {c| c =x}, M > = {u| u>x} o Sortiere M<, M=, M > o Die sortierte Menge M`ergibt sich dann durch Konkatenation wie folgt: o M`= M< ◦ M= ◦ M >, wenn auf M eine Ordnung existiert. Implementierung Quicksort: Let rec quicksort liste = match liste with | [] [] | [e] [e] | (e::rest) quicksort (lp(e,liste)) @ ep (e, liste) @ quicksort (up(e,liste)) ;; Bemerkung: „ep“ und „up“ sind analog zu „lp“ der equalpart und upponpart Seite 30 von 68 b) Permutation auf Sequenzen ( Erinnerung: Kryptographie, Kombinatorik, Zufallszahlen- Erzeugung) Permutationen sind bijektive Abbildungen : [1,…,n] [[1,…,n]] Gesucht: Rechenvorschrifft, die zu einer Sequenz s die Sequenzen aller Permutationen von s liefert. Zum Beispiel; perm([abc]) = ([abc],[acb],[bac],[bca],[cab],[cba]) Lösungsvorschläge für Berechnung der Permutation: 1.) systemantsiches Vorgehen: „Durchschieben“ des Elements am Kopf der Liste auf alle Listenpositionen und Permutation der aus den anderen Elementen gebildeten Liste. n=1 a n=2 ab ba n=3 abc acb bac cab bca cba n=4 abcd abdc acbd adbc acdb adcb … … 1 2 3 4 5 6 ... „a“ durchschieben je „bc permutieren. Wenn alle Permutationen des Listenrestes bekannt sind, folgt die Permutation durch Durchschieben des Listenkopfes durch alle (Teil)Permutationen. Sehen wir uns das Beispiel mit n = 4 genauer an. Wir sehen wir haben a. a soll durchgeschoben werden, sobald wir alle Permutationssequenzen von bcd angewendet haben. Das heißt wir lassen hier mal a außer acht und tun so, als hätten wir nur bcd. hier wiederholt sich das gleiche Prinzip, so dass wir mal b außer acht lassen, um cd permutieren zu können. Nun die Permutationen von cd sind, wie ja offensichtlich ist und auch in den ersten beiden Zeilen in der obigen Tabelle zu sehen ist cd und dc. Wir haben also alle Permutationen von cd in den ersten beiden Zeilen angewendet. Sprich: Wir können nun b durchschieben. Wir haben also in der dritten Zeile cbd. Nun wenden wir die zweite Permutation von cd, also dc auf diese Sequenz an. Somit ergibt sich für die vierte Zeile dbc. Das a bleibt bisher unverändert vorne angehängt. Wir können nun, da von cd wieder alle Permutationen angewendet wurden b weiterschieben. Es ergibt sich also für die fünfte Zeile cdb. Wir wenden wieder die Permutationen von cd an und es ergibt sich somit für die sechste Zeile dcb. Nun haben wir alle Permutationen von bcd. Das heißt wir können a eins weiter schieben und die einzelnen Permutationen mit nun a an zweiter Stelle anwenden usw. Am Ende haben wir alle Permutationen der Sequenz abcd erhalten. Formulierung unter Nutzung von Hilfsfkt. wie folgt: a) Funktion „StickOn“: ’a x list x list list x list Seite 31 von 68 Platziert ein Element x an den Anfang jedes Elements jeder Teilliste innerhalb einer übergebenen Gesamtliste. let rec stickOn x l = match l with | [] [] | (h :: t) (x :: h) :: stickOn x t;; Aufruf: stickOn 3 [[1;4;5];[4;1;5];[4;5;1]];; b) Funktion putInAll ’a x list list x list let rec putInAll x l = match l with | [] [[x]] | (h::t) (x::l) :: stickOn h (putInAll x t);; c) Funktion permuteAux: ’a *list*list list * list die unter Nutzung von putInAll ein Element x durch die Liste von Teilpermutationen schiebt und daraus die Ergebnisliste erzeugt. let rec permuteAux x l = match l with | [] [] | (h::t) putInAll x h @ permuteAux x t;; let rec permute l = match l with | [] -> [[]] | (h :: t) -> permuteAux h (permute l);; 2. Möglichkeit: Lösung unter Verwendung einer nichtlinearen rekursiven Funktion mit Einbettung Lösungsidee: - Sei t eine Sequenz, die bereits ein Präfix der Permutation enthällt. - Die Sequenz lhinters besteht aus allen Zeichen, die noch nicht in der Teilpermutation enthalten sind. - Der erste rekursive Aufruf berechnet also alle Permutationen, die das Präfix t @ [ x ] haben. - Der zweite rekursive Aufruf berechnet alle Permutationen, die dieses Präfix nicht enthalten (also logischerweise nur das Präfix t) Implementierung: let rec permEmb (t,h,s) = match (t,h,s) with | (t,h[]) [ ] | (t,h,[x]) [t @ [x]] | (t,l,x::rest) permEmb(t@[x], [ ], l @ rest) @ permEmb(t, l@[x],rest);; Aufruf mit let permutation (s) = permEmb([ ],[ ],s) ;; Seite 32 von 68 Verschränkte Rekursion: Definition: Bei einem System von rekursiven Funktionsdeklarationen f1…fj, bei dem die Bezeichner f1… fj in E1…Ej auftreten, spricht man (für j >= 2) von verschränkter Rekursion (führt zu zyklischem Aufruf) Beispiel: let rec qisodd n = match n with | 0 false | n when (n mod 2 = 0) gisodd (n/2) | n qiseven (n/2) and rec qiseven n = match n with | 0 true | n when ( n mod 2 = 0) qiseven(n/2) | n qisodd(n/2);; Warum and? Weil dies eine verschränkte Rekursion ist, und die Funktion qisodd qiseven sonst noch nicht kennen würde. Achtung, man lässt in der ersten Funktion dann auch die Strickpunkte weg. Geschachtelte Rekursion Definition: Treten bei einer rekursiven Funktion in den aktuellen Parameterausdrücken des rekursiven Aufrufs weitere rekursive Aufrufe auf, so spricht man von geschachtelter Rekursion ( rested recursion). Bekanntest Beispiel: Ackermannfunktion (eingeführt von Hilburst und Ackermann in der Berechenbarkeitstheorie). let rec ackermann (m,n) = match (m,n) with | (0,n) n + 1 | (m,0) ackermann (m - 1, 1) | (m,n) ackermann (m - 1, ackermann (m,n-1));; Funktionswerte der Ackerman- Funktion wachsen mit steigenden n außerdordentlich schnell an. Ackermannfunktion ist ein Beispiel für eine sogenannte - rekursive Funktion ( 4. Semester). Wesentliches Kennzeichen: Rekursionsschleife lässt sich nicht abschätzen, sie sind nicht durch äquivalente Schleifenumformungen berechenbar. Ein Beispielsaufruf bringt das benötigte Gefühl in die Sache. Im folgenden Beispiel ist fett markiert, was als nächstes geändert wird und unterstrichen, was zuletzt geändert wurde. am (2,2) am (1, am (2,1)) am (1, am (1,am (2,0))) am (1, am (1, am (1,1))) am (1, am (1,am (0, am (1,0))) am (1, am (1,am (0,am (0,1))) am (1, am (1, am (0, 2))) am (1, am (1, 3)) am (1, am (0, am (1,2))) am (1, am(0, am(0, am (1,1))) am (1, am(0, am(0, am (0, am (1,0))))) am (1, am(0, am(0, am (0, am (0,1))))) am (1, am(0, Seite 33 von 68 am(0, am (0, 2)))) am (1, am(0, am(0, 3))) am (1, am(0, 4)) am (1, 5) am (0, am(1,4)) am (0, am (0, am (1,3))) am (0, am (0, am (0,am (1,2))) am (0, am (0, am (0, am (0, am (1,1))))) am (0, am (0, am (0, am (0, am (0, am (1,0)))))) am (0, am (0, am (0, am (0, am (0, am (0,1)))))) am (0, am (0, am (0, am (0, am (0, 2))))) am (0, am (0, am (0, am (0, 3)))) am (0, am (0, am (0, 4))) am (0, am (0, 5)) am (0, 6) 7 Bemerkung zum Wachstum: Sei Bn: IN IN spezifiziert durch Bn(m) = ackermann (m,n), dann lässt sich durch Induktion zeigen (Hausaufgabe!) B0(m) = m +1 additive B1(m) = m +2 B2(m) = 2*m +3 m-fache additive Hier trifft unser obiges Beispiel zu: B2(2) = 2*2 +3 = 7 B3(m) = 2**(m+3) -3 m-fache Multiplikation B4(m) = ? d.h. bereits B4(m) kann nicht mehr durch elementare Fkt. Dargestellt werden. Im wesentlichen entsteht damit Bn+1(m) durch m- fache Iteration der Funktion Bn. Letzter Errinerungspunkt: Repetitiver Rekursion (Fall- Rekursion). Erscheint in einer linear rekursiven Funktionsdeklaration in einem Zweig einer Fallunterscheidung der rekursive Aufruf als letzte („äußerste“) Aktion, so heißt die Rekursion repetitiv. Beispiel: Eingebettete Fakultätsfunktion let rec facEmb(m,n) = match (m,n) with | (m,1) m | (m,n) facEmb(m*n, n-1);; Bei repetitiver Aufrufstruktur erfolgt Auswertung besonders effizient. Bevor mit Auswertung eines rekursiven Aufruf fortgefahren wird, sind alle Auswertungen des davorliegenden Ausdrucks ausgeschlossen. Parameter müssen also nicht auf einem Stack abgelegt werden, repetitve rekursive Funktionen lassen sich also sehr einfach in imperative Prozeduren (Schleifen) transformieren. Terminierung funktionaler Programme Obwohl die Terminierung von rekursiven Rechenvorschirfften für den Menschen oft offensichtlich ist, kann bei vielen anderen rekusiven Funktionen oft nicht direkt abgelesen werden, ob die jeweilige Funktion nun terminiert, oder nicht! Im Beispiel der vorher erwähnten Ackermannfunktion ist dies z.B. schon bedeutend schwieriger. => Wir bentöigen einen festen Formalismus. Dabei betrachten wir Rechenvorschrifften der funktionalität fct f = (m x)n :E Seite 34 von 68 Sei M die Trägermenge der Sorte m und M- = M \{ }. Hier verstanden als Ergebniss einer nicht terminierenden Funktion Das bedeutet M- ist diejenige Trägermenge, welche nur Funktionen enthällt, die auch tatsächlich terminieren. Dann terminiert die Funktion f xM, wenn xM: f(x) ≠ Um die Terminierungseigenschaft einer Fkt. zu zeigen, verwendet man eine sog. „Abstiegsfunktion“ h: M- IN0; h gibt eine Abschätzung für die Anzahl rekursiver Aufrufe bei linearer Rekursion bzw. der Höhe des Aufrufbaums bei nichtlinearer Rekursion. Zum Beweis der Terminierung muss folgendes Prädikat gezeigt werden: P [k IN 0 , x M , h( x) k f ( x) ] Es muss also für alle k gezeigt werden, dass P gilt, bzw. Berechnung der Funktion f umfasst max. k Schritte für Argument x. Nachweis durch Induktion: Für den IA (k=0) ist zu zeigen: x M , wenn h(x) = 0 => f ( x) , d.h. f(x) terminiert. IS: # Ind.annahme: P gilt für alle k’ k # z.z ist dann, dass aus (h( x) k 1) die Gültigkeit von f ( x) folgt. 1. Beispiel: Terminierung der FKT ggT(a,b) (mit a und b größer gleich 1) let rec ggT(a,b) = match (a,b) with |(a,b) when a= b a |(a,b) when a<b ggT(a,b-a) |(a,b) when a>b ggT(a-b,b);; 1.Schritt: Definition einer Ableitungsfunktion h : INxIN IN0 für a=b (Erzwinge ‘0’ für Terminierungsfall) 0 h( a, b) für (a>b) v (b>a) a b Es geht in diesem Schritt immer darum eine Fkt. zu finden, die in Abhängigkeit der Argumente des Fkt.aufrufes monoton fällt, und beim Terminierungsfall 0 landen wird. Induktionsanfang: h(a,b) = 0 => a = b => ggT(a,b) = a Induktionsschritt: - Induktionsannahme: a, b Sei also : : h(a, b) k ggT (a, b) Seite 35 von 68 0 h( a, b) k 1 h(a, b) a b k 1 (a k ) (b k ) weil a 1 b 1 Fall I (a b) ggT (a, b) ggT (a b, b) Für (a b, b) ist jedoch h(a-b,b)= a k => h(a-b,b) k=>ggT(a-b,b) ≠ dann mit Definition der FKT ggT(a,b) terminiert auch ggT(a,b) Fall II (a<b) analog Prinzipielle Vorgehensweise Sei f(x) = E die zu untersuchende Fkt. 1.Schritt: 2.Schritt: 2.Beispiel: Zeige, dass unter der Voraussetzung h(x)=0 der Ausdruck E zu einem Ergebnis führt. (Ind.Schluss) Unter der Voraussetzung h( x) k 1 forme den Ausdruck E so um, dass nur noch rekursive Aufrufe vorhanden sind, für die h(x) k gilt. Schnelle Berechnung von y = mn, m,n IN Naive Implementierung: y m m ... m n 1 Multiplikation Geschicktes Vorgehen: Rückgriff auf bereits berechneten Teilprodukte, Zwei Fälle: I.) y m m ... m m m ... m = A A für geradzahlige n A II.) y m m ... m m m ... m m = A A m für geradzahlige n. A also: A n 2 m 2 y 2 n21 m A für gerades n für ungerades n Damit fkt.: 1. Variante: let rec expo (m,n) = match n with | 0 -> 1 | n when (n mod 2 = 0) -> expo(m,n/2)*expo(m,n/2) | n -> m * expo (m,(n-1)/2)*expo(m,(n-1)/2);; Seite 36 von 68 2. Variante: let expo2 m = let rec aux(n)=match n with | 0 ->1 | n -> let y = aux(n/2) in if (n mod 2 = 0) then y*y else m * y * y in aux ;; Für Terminierung : Mögliche Abstiegsfunktion h(n) = n Bei jedem rek. Aufruf Integer- Division Terminierung bei 0 gewährleistet. Bemerkung: Häufig lässt sich die Terminierung durch strukturelle Ind. schnell zeigen. Vorgehen: - sei f(x) die Fkt. und x M-; - wähle für die Argumente der Fkt eine ’geeignete fundierte Ordnung’ - IA: zeige f(x min) ≠ wo x min die minimalen Elemente der Ordnung sind. - Induktionsschluss: Wenn aus der Gültigkeit von f(z) ≠ für alle z = y die Gültigkeit von f(y) ≠ folgt, dann ist f(x) ≠ für alle Elemente der Ordnung Fkt. terminiert Beispiel 3: Ackermann: n 1 für m 0 A(m, n) A(m 1,1) für m 0, n 0 A(m 1, A(m, n 1)) für m, n 0 - a) b) fundierte Ordnung: lexikographische Ordnung über IN0xIN0 also: (0,0)<(0,1)<(0,2)< … <(1,0)<(1,1)<… Damit Beweisführung über geschachtelte Ind. # über m (äußere Ind) # über n bei festem m (innere Ind)) Induktionsanfang: m= 0 A(0,n)=n+1 ≠ Induktionsschritt: Voraussetzung äußere Induktion A(k,n) ≠ für 0 k < m und beliebigen n - Innere Ind. über n zum Schluss auf m o Anfang n = 0: A(m,0) = A(m-1,1) ≠ nach Voraussetzung der äußeren Induktion o Voraussetzung: A(m,l) ≠ für bestes m und alle l = n o Schluss auf n: A(m,n)= A(m-1,A(m,n-1)) A(m,n-1) terminiert nach Voraussetzung und erzeugt k. also: Seite 37 von 68 A(m -1,k) und dies terminiert nach äußerer Voraussetzung Also terminiert A(m,n) für alle drei Rekursionsfälle. Bsp 4: (Klaus Funktion): Rechenvorschrifft, bei der bis heute nicht bekannt ist, ob sie für alle Eingaben terminiert: Ausgehend von a0 IN und a0 > 0 erzeuge Zahlenfolge a0,a1…,an,… in der folgenden Weise: Brich ab, falls an=1 Bsp. Aufruf: für a0 = 3 3,10,5,16,8,4,2,1 Es ist offen, ob bei beliebiger Eingabe a0 die Länge der Liste endlich ist Nehmen wir hier ein weiteres Beispiel aus den Übungsblättern her: Bsp. 5: Permutation von Sequenzen Wir kennen bereits 2 Varianten dies zu implementieren. Wir werden nun die Terminierung der eingebetteten Version beweisen. Zur Wiederholung noch mal die Funktion in Ocaml: let perm (s) = permEmb ([],[],s];; let rec permEmb (t,h,s) = match (t,h,s) with | (t,h,[]) -> [] | (t,h,[x] -> [t @ [x]] | (t,l, x :: rest ) -> permEmb (t @ [x],[], l @ rest) @ permEmb ( t, l @ [x], rest );; Aufgabe ist es nun, eine geeignete Abstiegsfunktion zu wählen und damit zu zeigen, dass die Funktion terminiert. Der gleiche Satz wie grad eben formal ausgedrückt: Zu zeigen ist, dass die Funktion permEmb (t , l , s ) für beliebige Tripel (t , l , s) M (c * xC * xc*) . Es bezeichnen n1 und ns die Längen der Sequenzen l und s. Wir wählen folgende Abstiegsfunktion: 0, ns 1 h(t , l , s) ns , nl 0 (ns nl ) ns sonst Auch bei diesem Beispiel ist mir die Abstiegsfunktion leider ein Rätsel. Ein Versuch von mir selbst h(t,l,s) zu erklären (ohne Gewähr): die erste Zeile für den Fall das ns kleinergleich 1 ist, gibt 0 aus. Dies modelliert die beiden Terminierungsfälle in unserem OcamlProgramm in den ersten und zweiten Pattern Matching Zweigen. Der zweite Fall trifft zu, wenn die Länge von l = 0 ist. Hierbei wird dann die Länge von s ausgegeben. Welchen Fall das in unserem Ocaml Programm abdecken soll? Keine Ahnung. Für mich sieht das so aus, als wären die ersten beiden schon abgedeckt. Die dritte Zeile: ... Bei bestem Willen: Keine Ahnung. Seite 38 von 68 Leider ist auch keinerlei Erklärung angegeben, wenn ich noch was darüber herausfinde, werde ich das nachtragen, nehmen wir jetzt mal die Abstiegsfunktion als gegeben an. Wir beweisen nun dass diese terminiert mittels Induktion. Zu zeigen ist das Prädikat: P(k ) x M : h( x) k permEmb( x) Induktionsanfang: Zu zeigen ist x M : h( x) 0 permEmb( x) . Da die Abstietgsfunktion h, genau dann Null ist, wenn ns 0 ns 1 und permEmb in diesem Fall per Definition terminiert, ist der IA gezeigt. Induktionsschluss: Gemäß Induktionshypothese terminiert die Funktion permEmb für alle x M-, für die h(x) k. Dann bedeutet dies, dass der Aufruf der Funktion permEmb terminiert für alle Tripel (t,l,s) mit ns l [] ns 1 k h(t , l , s ) (ns nl ) ns l [] ns 1 Wir betrachten nun alle x M- für die 0 < h(x) k+1. Es sind nun also Argumenttripel zu bterachten, bei denen die Sequenz l oder s um ein Element größer wurde. l x :: l; Gemäß Funktionsdefinition hat der Aufruf permEmb (t, x::l,s) Aufrufe mit den Argumenttripeln ( t @ [y] ,[],(x::l)@rest(s)) und (t, (x::l) @ [y], rest(s)) zur Folge. Da nun h( t @ [y] ,[],(x::l)@rest(s))= ns + nl k und h( t ,(x::l)@[y],rest(s))= (ns + nl + 1) * ns –(nl + 1) k s x :: s; Gemäß Funktionsdefinitoon hat der Aufruf permEmb ( t, l, x :: s) Aufrufe mit den Argumenttripeln ( t @ [x], [], l @ s) und (t,l,@[x],s) zur Folge. Für erstes erhalten wir die Absitiegsfunktion h(t@[x],[],l@s) = nl + ns k und damit terminiert die Funktion permEmb für den entsprechenden Funktionsaufruf. Bei dem Argumenttripel (t, l @ [x],s) wächst die Sequenz l um ein Element. Für derartige Funktionsauifrufe wurde jedoch bereits oben gezeigt, dass die Aufrufe terminieren. Aufwand von Algorithmen Speicher – bzw. Berechnungsaufwand eines Algorithmus hängen typischer Weise empfindlich von den Parametern ab, die den Algorithmus übergeben werden. Beispielsweise benötigen wir zur Berechnung von fak(n) n rekursive Aufrufe, zur Berechnung der Permutation einer Sequenz der Länge n ca. n! rekursive Aufrufe. Um den Aufwand abschätzen Komplexitätsklassen ein. zu können führt man sogenannte Seite 39 von 68 Definition: O( f (n)) {g (n) | c 0, n0 IN mit 0 g (n) c f (n) für n n0 } O(f(n)) definiert eine Klasse von Funktionen. Eine Funktion g(n) ist element der Klasse O(f(n)), wenn ab einem bestimmten n0 die Funktion c*f(n) eine andere Schranke für g(n) bildet. Also: g(n) wächst höchstens so schnell wie f(n). Wichtig ist das asymptotische Verhalten für n unendlich Beispiel: n2 2 O(n 2 ) n O(n 2 ) n log n O (n 2 ) n3 O(n 2 ) Bei der Fakultätsfunktion ist der Berechnungsaufwand proportional zu n => gehört der Komplexitätsklasse O(n) an. Intuitiv erkennt man, dass der Berechnungsaufwand bei der Fakultätsfunktion propotional zu n ist, wenn man annimmt, dass Multiplikationsaufwand konstant ist. Auch möglich: Berechnungsaufwand hängt ab von Anzahl übergebener Parameter, z.B. bei Summenberechnungen Beispiel: DFT, diskete Fourirtransformation Aufgabe: Transformation eines Zeitreienabschnittes in ein „Spektum“, welches die einzelnen in der Zeitreihe enthaltenen Frequenzkomponenten repräsentiert. Anwendung: Filterung, (Bild,- Audio -) Kompression, Spektralanalyse. Seite 40 von 68 also: DFT: Abbildun IRn n oder: Abbildung n N 1 Rechenvorschrifft: F f n e j 2 kn N n wo j²=-1 n 0 Für jeden der k 0 … N-1 Spektralwerte sind N Operationen auszuführen, für komplettes Spektrum also N². Die schnelle Fourirtransformation FFT macht sich die Tatsache zu Nutze, dass bei der Summenauswertung sehr oft gleiche Resultate erneut berechnet werden (z.B. gleiche Produkte k n ), und sie speichert diese ab. Damit Reduktion des Aufwandes auf N log N. Weiteres Beispiel: Sortieren durch Einfügen. Beim Sortieren durch Einfügen wird aus einer zu sortierenden Quellliste ein beliebiges Element entnommen und an der „richtigen Stelle“ in die Ergebnissliste eingefügt. let rec insert (el, s) = match (el,s) with | (el,[]) [el] | (el, x::rest) when (el >= x) el :: s | (el, x::rest) x :: insert (el,rest);; let rec insertSeq (s,r) = match (s,r) with | (s,[]) s | (s, x::rest) insertSeq(insert(x,s),rest);; Quellliste, umsortiert Seite 41 von 68 let insertSort(s) = insertSeq([],s) Rechenaufwand: Sei n die Länge der zu sortierenden Liste, dann wird insertSeq n mal aufgerufen. Bei „Gleichverteilung“ wird die Funktion n/2 – mal aufgerufen. Also; n²/2 rekursive Aufrufe, Rechenaufwand O(n²). Vergleich mit Quicksort: Bei zufälliger Wahl des Pivot-Elements wird die Liste bei jedem Aufruf im Mittel halbiert. Spätestens nach log2n rekursiven Aufrufen gelagt man zur 0- bzw. 1elementiges Liste. Das Aufteilen einer Teilliste in lower, upper- equal-Part erfordert im Mittel n/2 Schritte. Also: Bei Quicksort im Mittel Zeitaufwand O(nlogn) . Bemerkung: Kurioserweise kommt Quicksort auf den Aufwand O(n²), wenn die Liste schon sortiert ist. Häufig vorkommende Komplexitätsklassen: N log N linear logarithmisch N log N Fakultät Suche in Binärbäumen, schnelle Exp. Quicksort (im Mittel) FFT Nm polynomiell DFT mN exponentiell Pfadsuche in Graphen, z.B. „Traveling Salesman“ Problem Korrektheit von Quicksort Hier nun ein kleiner Einschub. Wir wollen die Korrektheit von Quicksort beweisen. Diese Aufgabe besteht aus folgenden drei Teilaufgaben: a) Geben sie ein Prädikat an, das die Anforderungen an einen Sortieralgorithmus formal spezifiziert! b) Zeigen sie, dass die aus der Vorlesung bekannte Implementierung des QuicksortAlgorithmus partiell korrekt ist. c) Erläutern sie informell, weshalb der mittlere Aufwand beim Sortieren gemäß Quicksort durch O(n log n) gegeben ist. Die erste Teilaufgabe a) sieht auf den ersten Blick komplizierter aus, als sie ist. Hier soll lediglich angegeben werden, was der Quicksort- Algorithmus machen soll: a) Im folgenden sei l eine Liste von natürlichen Zahlen: Bei Eingabe von l gibt der Algorithmus Quicksort eine aufsteigend sortierte liste Quicksort(l) zurück, die genau aus den Elementen von l besteht. Seite 42 von 68 b) Sei {} die leere Liste; dann ist Quicksort durch die beiden folgenden Gleichungen rekursiv definiert: QuickSort({}) := {} QuickSort(m :: l) := QuickSort(left(split(l,m))) ° (m) ° QuickSort(right(split(l,m))) Was hier geschieht ist uns bereits bekannt und wurde weiter oben auch schon erklärt. Dennoch hier noch mal die formale Erklärung ddes Algorithmuses: m :: l ist eine liste mit Kopf m und Rest l. split erwartet 2 Argumente, das Pivotelement (hier m) und eine Liste (hier l). Sie gibt ein Tupel aus 2 Elementen zurück, welche wiederum 2 Listen l1 und l2 sind. Dabei besteht l1 aus den Elementen k l so dass k m und l2 aus k l mit k > m. left und right geben jeweils den linken bzw. den rechten Teil eines Tupels zurück. Sprich wir haben hier die selbe Funktion wie vorhin die Lösung mit lp ep und up. Wir gehen davon aus, dass die eben erwähnten Algorithmen richtig sind, und beweisen auf deren Grundlage die Korrektheit von Quicksort: Wir wissen, dass left(split(l,m)) und right(split(l,m)) Listen zurück geben, deren Längen jeweils kleiner gleich der Länge von l sind. Induktionsannahme: Für alle Listen l mit einer Länge < n arbeitet Quicksort korrekt. Induktionsanfang: Für l = {} gilt nach der ersten Gleichung die Annahme. Induktionsschritt: Zu zeigen ist dass Quicksort auch auf Listen der Länge n korrekt arbeitet. Sei also l = m::r eine Liste der Länge n. Dann ist r eine Liste der Länge (n-1), und left(split(r,m)) und right(split(r,m)) jeweils Listen mit einer Länge < n. Für diese kann man die Induktionsannahme anwenden und erhällt dass Quicksort auf diesen Listen korrekt arbeitet. Denn für Listen der Länge < n arbeitet Quicksort laut Induktionsannahme schließlich korrekt. Da nun die Elemente aus left(split(r,m)) alle kleiner gleich m sind, und da die Elemente aus right(split(r,m)) alle größer m sind haben wir eine aufsteigend sortierte Liste, welche genau aus den Elementen besteht, aus der l bestand, qed. c) Der Zeitaufwand bei der Sortierung durch Quicksort lässt sich folgendermaßen abschätzen: Jeder Aufruf von Quicksort hat zwei rekursive Aufrufe zur Folge. Im Mittel wird die Länge der zu sortierenden Teilsequenzen halbiert. So gelangt man nach log2 n Schritten zu einer Liste der Länge n < 2, die per Definition sortiert ist. In jedem Schritt muss die zu sortierende Liste darüber hinaus in up, ep und ep aufgespalten werden. Das erfordert im Mittel n/2 rekursive Aufrufe der entsprechenden Funktionen. Wir bekommen also insgesamt ca. 1,2n log2 n Aufrufe und damit einen Zeitaufwand von O(nlogn). MergeSort Der MergeSort Algorithmus beruht auf dem vereinigen (merge) von in sich sortierten Listen. Die vereingte Liste ist dann wieder sortiert. Seite 43 von 68 Hier eine Lösung in Ocaml: let rec merge l1 l2 = match (l1,l2) with | ( _ , [] ) -> l1 | ( [],_ ) -> l2 | (a :: xs, b :: ys) when a < b -> a :: (merge xs (b::ys)) | (a :: xs, b::ys) -> b :: (merge (a::xs) (ys) ;; Wir verwenden hier eine weitere Variante von MergeSort die zuerst aus der Liste eine Liste von einelementigen Listen macht und diese dann nacheinander mit der Funktion merge vereinigt, bis nur noch eine einzige, sortierte, Liste vorliegt. In jedem Schritt vereinigen wir die jeweils zwei hintereinander stehenden Listen zu einer. let rec mergesort_step l = match l with | [] -> [] | [a] -> [a] | a::b::xs -> (merge a b) :: (mergesort_step xs) ;; Falls die Liste nur noch aus einer Liste besteht sind wir fertig, ansonsten das ganze nochmal. let rec mergesort l = match l with | [] -> [] | [a] -> a | _ -> mergesort (mergesort_step l) ;; Jetzt noch eine Hilfsfunktion, um die Liste von Listen zu erzeugen: let rec explode l = match l with | [] -> [] | a :: xs -> [] :: (explode xs) ;; Und in Prolog: Zunächst brauchen wir eine Funktion, die zwei sortierte Listen zu einer sortierten Liste zuzammensetzt, wie in Ocaml auch: merge([],List, List). merge(List, [], List). merge( [A|Alist], [B|Blist], [B|Rlist]) :merge(A|Alist], [B| Blist], [B|Rlist]) :- A =< B, merge( Alist, [B|Blist], Rlist). A > B, merge([A|Alist],Blist,Rlist). Wir brauchen noch eine Funktion die eine Liste in zwei (ungefähr gleich große Teile zerlegt. Wir spalten immer die ersten beiden Elemente ab und veteilen sie abwechselnd auf die beiden Listen. Seite 44 von 68 split([],[],[]). split([A][A],[]). split([A,B|T], [A|L1], [B|L2]) :- split(T,L1,L2). Jetzt wieder die eigentliche Funktion: Die Liste wird sortiert indem wir sie zuerst in zwei Teile aufspalten die jeder für sich sortiert werden; diese beiden sortierten Listen vereinigen wir dann wieder zum Ergebnis. mergesort([],[]). mergesort([A],[A]). mergesort(List,Rlist) :- split(List, Alist, Blist), mergesort(Alist, Asort), mergesort(Blist,Bsort), merge(Asort, Bsort, Rlist). Rekursive Datenstrukturen Die allgemeine Syntax einer Datentypdefinition ist wie folgt. „Topic“ type <Tvar1> <Topi> = <C11> of <T11> |… | < T1n> … and <Tvarm> <Topm> = <Cm1> of <Tm1> |…| <Cmn> of <Tmn> wo: <Topi>: <Tvari>: <Tij>: <Cij>: Name des jeweiligen Datentyps Sequenzen von Typvariablen Ausdruck von Typen Konstruktionen, die Lemente vom Typ <Tvar> <Top> enthalten. Beispiel: a) Linear rekursive Sorten z.B. die bekannte Lifo-Listen (siehe Folie Seite 102). b) nichtlinear rekursive Sorten: Typdefinition enthällt in mindestens einem Typausdruck mindestens zweimal den zu definierenden Typ. Bäume type (’a,’b) tree = Leaf of ’a | Node of ((‘a,’b) tree * b * (‘a,’b) tree);; Baum, dessen Blätter vom Typ ’a sind, und dessen Knoten Elemente vom Typ ’b enthalten (Achtung: Kein a-b-Baum!) Spezialiesierung: ’a = (); entspricht einem Baum des Typs type ’b tree = Empty | Node of ’b tree * b * ’b tree Funktionen zum Aufbau solcher Bäume: let rec insertInnerTree x tree = match tree with | Leaf () Node(Leaf(), x, Leaf()) | Node(l,el,r) when (el < x) Node (insertInnerTree x l, el,r) Seite 45 von 68 | Node (l,el,r) Node(l,el,insert Inner Tree x r);; let buildTree list = match list with | [] tree | x:: rest v (insertIInnerTree x tree) rest in f (Leaf()) list;; Verschränkte Rekursion Typdefinition umfasst mehrere neue Typen und in den Typausdrücken können alle Typen auftreten. type `b wald = LeerW | Wald of `b * `b wald and ‘a baum = LeerB| Baum of ‘a * (‘a baum) wald;; Der erste Teil dieser Typdef. hat hierbei die Struktur einer Liste, der zweite führt zur Verschränkung durch Rückgriff auf „wald“. Durch verschränkt rekursive Sortendeklaration werden auch die darauf arbeitenden Funktionen rekursiv. Beispiel: Funktion zur Suche nach einem Teilbaum mit einer bestimten Wurzel x. let rec baumsuche(x,baum) = match baum with | LeerB LeerB | Baum(y,wald) when x = y Baum(y,wald) | Baum(y,wald) waldsuche(x,wald) and waldsuche (x,wald) = match wald with | LeerW LeerB | Wald (ersterB,restWald) let t = baumsuche (x,ersterBaum) in match t with | LeerB waldsuche (x,restWald) |_t ;; Weiteres Beispiel für Bäume mit mehreren „Kindbäumen“: (a,b)- Bäume. Ein (a,b)-Baum ist ein externer Suchbaum (d.h. alle Informationen befinden sich in den Blättern), für den gilt: - alle Blätter haben die selbe Tiefe - für die Anzahl N der Kinder eines jeden internen Knotens (außer der Wurzel) gilt: a < N < b - für die Wurzel gilt: 2 < N < b außerdem gilt für b: b 2a 1 Spezialfall: b = 2 a -1, hier spricht man von B-Bäumen. B-Bäume finden vor allem als Datenbank- Externspeicher Verwendung. Seite 46 von 68 Aufgabe 19 B- Bäume Bei einem B- Baum vom Grad t haben die Knoten zwischen up(t/2) und t Kinder, wobei up(x) x aufrundet. Die Wurzel hat zwischen 2 und t Kinder. Alle Bläter haben gleiche Tiefe. a) Zeigen sie, dass in einem B- Baum bom Grad t bei einer Zahl von n Knoten die n 1 Höhe h immer begrenzt ist durch h 1 log t . up ( ) 2 2 Berechnen sie damit die maximale eines Baumes vom Grad 128 für eine Millionen bzw. eine Millarde Knoten. b) B- Bäume werden gerne für das Suchen in grossen Datenbanken verwendet. Was für einen Voteil bietet diese Datenstruktur hierfür? c) Wir fügen nun die Zahlen1, 2, 3, ... aufsteigend sortiuert in einen anfangs leeren B- Baum bvom Grad t = 128 ein. Bei welcher Zahl hat der B- Baum erstmalig die Tiefe 4? d) Worauf muss man beim Löschen aus einem B- Baum achten? Lösung zu Aufgabe 19 a) Zu zeigen ist, dass in einem B- Baum vom Grad t bei einer Zahl von n Knoten die n 1 Höhe h immer begrenzt ist durch h 1 log t . up ( ) 2 2 Ein B- Baum mit n Einträgen hat n + 1 Blätter. Wegen der Mindestzahl von 2 für die Wuzel, bzw. up(t/2) für andere Knoten ist die Anzahl von Einträgen auf Ebene 1, 2, 3, ... immer grösser als 2, 2* up(t/2), 2* up(t/2) * up(t/2)... Damit erhällt man: n 1 2 (up(t / 2)) h 1 n 1 h 1 log up (t / 2) 2 max.h(Millionen) = ca. 4 max.h(Millarden) = ca. 5 b) Da bei heutigen Massenspeichern, insbesondere magnetische Festplatten die Daetenzugriffszeit weit über der (kontinuierlichen) Leserate liegt, ist es sinnvoll grosse Datenmengen immer im Block auszulesen. c) Wir fügen nun die Zahlen 1, 2, 3, ... aufsteigend sortiert in einen anfangs leeren BBaum vom Grad t = 128. Bei welcher Zahl hat der Baum erstmalig die Tiefe 4? Jeder Knoten kann zwischen up(t/2) und (t-1) in unserem Fall also zwischen 64 und 127 Einträge haben. Hier hätt ich gern die Zeichnungen des Lösungsblattes eingetragen, die wirklich sehr sehr schön gemacht sind, nur leider funktioniert im moment meine Zwischenablage nicht und ich hab, nachdem ich 6 Stunden gestern nacht mich um dem Computer eines Freundes gekümmert habe echt keine Lust mich darum auch noch zu kümmern, kuckt für die Lösungen einfach das Übungsblatt an! Seite 47 von 68 AVL Bäume AVL Bäume sind Binärbäume, für die gelten, dass sich die Tiefe des linken Teilbaumes maximal um 1 von der Tiefe des rechten Teilbaumes unterscheidet. Damit ergibt sich ein guter Kompromiss zwischen einer geringen Gesamthöhe und dem Aufwand beim Einfügen oder Löschen von Elementen. Eine geringe Gesamthöhe ist erwünscht, da sich die Laufzeit beim Suchen nach Elementen proportional zur Höhe des Baumes verhällt. Ein völlig ausgeglichener Baum erreicht eine Laufzeit von O(log n ) [n = Anzahl der Elemente], und auch die Suche im (theoretisch) am schlechtesten ausgeglichenem AVL- Baum ist nur geringfügig langsamer. Aufgrund des gegenüber normalen Binärbäumen aufwendigeren Einfügens und Löschens sind AVL- Bäume besonders da attraktiv, wo auf einem fest vorgegebenen Datenbestand viele Zuigriffe notwendig sind, insbesondere dann, wenn auf alle Knoten im Mittel gleich oft zugegriffen wird. Die AVL Eiugenschaft wird durch spezielle Algorithmen beim Einfügen und Löschen von Knoten in/aus dem Binärbaum erreicht. Dabei wird für jeden Knoten zuzätzlich ein Wert Balance mitgespeichert, der den Uinterschied zwischen der Tiefe des linken und des rechten Teilbaumes beinhaltet. Wird diese Balance kleiner als -1 oder größer als +1 wird dieser Knoten rotiert um die Balance wieder auszugleichen. Dabei unterscheiden wir zwischen links-, rechts-, linksrecht-, rechtslinks- Rotation. Funktoren: Datentypen mit Funktionen als Argument. Bis hierher: Datentypen enthielten bisher Konstruktoren, deren Argumente wieder Datentypen sind. Bei den häufig verwendeten Sorten wie etwa stacks, Listen etc. sind die Zugriffsfunktionen immer dieselben und eng mit der Datenstruktur verbunden. Deshalb sinnvoll: Funktionen werden zum Bestandteil der Datenstruktur (ebenfalls zentrales Konzept oder OO. Programmierung) type ’a listFum = Wert of ’a | Fum of (‘a ‘a) * ‘a listFum;; Der Konstruktor “Fum” enthällt hierbei aber als erstes Argument die Signatur ‘a ‘a und als zweites Argument ein Element des zu definiernden Datentyps. Soll ein Objekt dieses Datentyps erzeugt/ instanziert werden, so müssen dem Konstruktor geeignete Eigenschaften übergeben werden, z.B.: let succ = (+) 1;; let div8= (/) 8;; let a = Fum(div8, Fum(succ, Wert 3));; Der Datentyp listFum ist also Listenstruktur, wobei der Kopf der Liste eine Funktion ist, die auf den Rest der Liste angewendet werden kann. Ausgeführt werden könnte eine solche Funktionsdeklaration mit Hilfe der Funktion: so: oder so: let rec compute = function let rec compute l = match l with | Wert v v | Wert v -> v | Fum (f,x) f(compute x);; | Fum (f,x) -> f (compute x);; hiermit ergäbe sich für compute a = 2. Seite 48 von 68 Beispiel 2: Graphen und Suche in Graphen. In einfacher Weise charakterisieren wir einen Graphen, indem wir jedem Knoten die Menge seiner direkten Nachfolger angeben. Graph besteht also aus einer Funktion, die zu jedem Knoten die Menge seiner Nachfolger berechnet bzw. zuordnet. type ’a graph = Graph of (’a ‘a list) Beispiel: Graph zur Darstellung der echten Teiler einer natürlichen Zahl. 12 13 14 8 9 6 4 7 5 2 3 1. Schritt: Definition der NachfolgerFunktion. let teiler n = let rec f m = match m with | m when (m < 2) [] | m when ( n mod m = 0)& ( m < = n /2) m :: f(m-1) | m f (m-1) in f n;; 2. Schritt: Erzeugung des Objekts let graph1 = Graph(teiler);; Start Ziel In derart definierten Graphen soll nun nach Elementen gesucht werden, die gewisse Prädikate pred erfüllen. Vorgehen: 1.) Ausgehend vom Startknoten werden die direkten Nachfolgerknoten ermittelt. Seite 49 von 68 2.) 3.) Bei Breitensuche werden zuerst alle Nachgfolger geprüft und dann erst die Nachfolger der Nachfolger; bei Teifensuche umgekehrt. Jeder Nachfolgerknoten verweist seinerseits auf eine Menge von Knoten, hierbei sollen nur die Knoten besucht (geprüft) werden, die vorher nicht besucht wurden ( Vermeidung von Zyklen) Algortihmus zur Tiefensuche: type ’a reachable = OK of ’a | Fail;; type ‘a graph = Graph of (‘a ‘a List);; let getSuccFum(Graph(f))= f;; let rec isElem list a = match list with | [] -> false | hd :: rest when a = hd -> true | hd:: rest -> isElem rest a;; let depthsearch graph pred startnodes= let rec find visited startnodes = match startnodes with | [] Fail | a:: rest when isElem (a,visited) -> find visited rest | a:: rest when (pred a) OK a | a:: rest find (a:: visited) ((( get SuccFum graph)a) @ rest) in find [] [startnodes];; Induktive Beweise über rekursive Sorten Definition: (Partielle Korrektheit) Eine Funktion heißt partiell korrekt, wenn sie keine falschen Resultate (im Sinne der Spezifikation) liefert. Bemerkung: Funktionen, die für gewisse Argumente nicht terminieren, sind auch partiell Korrekt Eine partiell Korrekte Funktion, die stets terminiert, heißt total Korrekt. Um partiell Korrekte Funktionen zu zeigen, ist das Prädikat f Im p ( x) f spez ( x) zu zeigen. Verallgemeinerung: Man benötigt eine Technik, um ein Prädikat P über einer Datenstruktur zu zeigen. => strukturelle Induktion. Notwendig: Begriff der freien Algebra (Informell). Die Werte der freien Algebra sind die Ausdrücke, die aus den Konstruktoren gebildet werden Seite 50 von 68 Jedem Konstruktor wird eine Funktion zugeordnet, die auf den Werten der Algebra operiert. Sei type t = C0 | C1 of t | C2 of t * t;; ein Datentyp. Dann ist C0 der einzige Ausdruck, der durch einmaliges Anwenden eines Konstruktors entsteht. (Term der „Tiefe 1“). Durch zweimaliges Anwenden entstehen C1(C0); C2(C0,C0); dreimaliges Anwenden: C1(C1(C0))); C1(C2(C0,C0));… usw. Die Vereinigung aller derart Konstruiierbaren Werte bildet die Menge der Werte der freien Algebra: Funktionen der freien Algebra: C0: C1: C2: t tt: t*tt (Konstante) C1(E1) = E2 C2(E1’,E2’) = E3 Strukturelle Induktion: Offenbar werden alle Werte der freien Algebra aus (endlichen) Werten geringerer Tiefe erzeugt. Das Prinzip der strukturellen Induktion lässt sich also auf derartige freie Algebren anwenden. Gegeben sei ein Datentyp t mit den Konstruktoren C0,…,Cn: Dann gilt das Prädikat x t : P ( x) wenn für alle nullstelligen Konstruktoren Ci das Präsikat P(Ci) gilt. (IA) wenn für Konstruktoren Ci vom Typ T1 * … * Tj (j >0) die Gültigkeit von P(Ci(x1,…,xj)) aus der Gültigkeit von P(x1),…, P(xj) folgt. Beispiel: Binärbäume type ’a btree = Leaf of ’a | Node of ‘a btree * ‘a btree; Es soll nun x (T btree) : P( x) gelten. es gilt P(Leaf(a)) es gilt P(Node(t1,t2)), wenn P(t1) P(t 2) gilt. Wir zeigen nun, dass die Funktion let rec reflect tree = match tree with | Leaf(a) -> tree | Node (t1,t2) -> Node(reflect(t2), reflect(t1));; die Bedingung : Seite 51 von 68 reflect(reflect(x))=x erfüllt. Beweis : Induktionsanfang : x= Leaf(a) reflect(reflect(Leaf(a)) reflect ( Leaf (a)) Def Leaf (a); qed Def Induktionsschluss: Induktionsannahme: Es gelte reflect(reflect(ti)) =ti Dann folgt: i Element {1;2} reflect (reflect ( Node(t1, t 2))) reflect (( Node(reflect (t 2), reflect (t1))) Def Node(reflect (reflect (t1), reflect (t 2))) Def Node(t1, t 2);qed I . A. Denotationale Semantik rekursiver Strukturen Problematik: Einfache Ausdrücke let add x y = x+y;; type farbe =Rot | Gelb |… intuitiv klar. schwieriger : let rec add (x,y) = match x with | 0 -> y | z +1 -> add (z,y) +1;; type lifo =Empty | App of int * Lifo;; unverständlich let f x y = f x y;; type t =T of t;; Erläuterung zur Vorgehensweise: Um die Semantik (Bedeutung) rekursiver Struktren zu definieren, verwenden wir im Weiteren ein iteratives Vorgehen Seite 52 von 68 Wir beginnen mit einer vollkommen undefinierten Sturktur. Über iterative Rechenvorschrifften erweitern wir sukzesive den Definitionsbereich und gelangen schließlich zu einer Funktion mit max. Definitionsbereich. Das Element Bottom: Jedes Funktionsresultat soll eine Bedeutung (Sinn) haben Auf der anderen Seite gibt es Funktionen, bei denen nicht entscheidbar ist, ob sie terminieren (d.h. ein sinnvolles Resultat liefern) Folgerung: Wir müssen ein Objekt einführen, das nicht terminierende Funktionsresultate beschreibt => Dieses Objekt ist ein Wert (mit Semantik) wie beispielsweise 1;2;7… Ordnungen über Mengen M u { } = M Bei der Diskrepanz der Semantik rekursiver Strukturen muss die Wertemenge dieser Funktionen, d.h. M u { } diskutiert werden. D.h. wir benötigen geeignete Ordnung für M , => Definition einer speziellen Ordnung (flache Ordnung): x y x y In Worten: x ist genau dann dem y untergeordnet, wenn x nicht terminiert, jedoch y sehr wohl. (sprich: x ist schwächer definiert als y). Zusammen mit Gleichheit bildet „ “ eine partielle oder Halb-Ordnung, denn es gibt nicht vergleichbare Elemente, Beispiel: in sind alle Zahlen nicht miteinander vergleichbar. Flache Ordnung für ...-2 -1 0 1… Diese Ordnungen lassen sich verallgemeinern auf Tupel und insbesondere Funktionen ( x, y) ( x ', y ') x x ' y y ' f g x M : f ( x) g ( x) Die Ordnung „ “ über Funktion drückt aus, wie „definiert“ eine Funktion ist. n f …-2 … -1 0 1 2 … Seite 53 von 68 g h ... ... 3 1 1 2 ... ... => f g; f h g und h können nicht zueinander in Relation gesetzt werden. Bemerkung: Durch Einführung des Objektes lassen sich partielle Funktionen zu totalen machen. Vollständige Halbordnung Eine Halbordnung heißt vollständig, wenn jede aufsteigende Kette, c1 c2 ... , eine kleinste obere Schranke, d.h. ein Supremum, besitzt. Beispiel: Für flache Ordnungen, beispielsweise über , kann eine derartige Kette höchstens 2 Elemente haben. 3 3 ... Monotonie: Eine Abbildung f heißt monoton, wenn x y f ( x) f ( y ) Bemerkung: Wenn x = und y dann muss f ( x) sein. f(x) kann durch Anwendung von f nicht „stärker“ werden als f(y). Stetigkeit Sei fi eine Folge von Funktionen mit vollständiger Halbordnung. Dann heißt eine Abbildung über diesen Funktionen stetig: sup [ fi ] [sup fi ] mit i IN Fixpunkt: Sei : M N eine Abbildung, dann heißt ein Element a M mit a [a ] ein Fixpunkt der Abbildung . Satz von Kleene: Sei eine stetige Abbildung, dann ist der kleinste Fixpunkt von [f] identisch mit: f sup fi ; i wobei f 0 ; fi 1 [ fi ]; Beispiel: Heron- Verfahren zu Berechnung von 2. Seite 54 von 68 1 2 y² 2 y y 2 y y [ y ] Iterationsvorschrifft: y0 1 1 2 yn 1 yn ; n 0 2 yn Der Fixpunkt dieser Abbildung ist Satz ist das der Fixpunkt. 2 . Denn y ist 2 und nach dem Kleenschen y0 1 1 2 3 y1 1 1,5 2 1 2 13 2 y2 2 1, 42 22 3 … Frage: Halbordnung für Heron Verfahren: x 2 y ( x ² 2) ( y ² 2) y0 2 y1 2 y2 ... Weiteres Beispiel: Diff. Gleichung: y d y dx y [ y] => Lösung ist ex Anwendung des Kleenschen Satzes auf Strukturen funktionaler Sprachen: (1) Rekursive Datentypen Beispiel: Liste type seq = Empty | Prepend od int * seq;; (*) Seite 55 von 68 Vorgehensweise: (Zur Lösung derartiger Gleichungen) I. Übetragung der sprachspezifischen Form in eine mathematische II. identifizieren des Fixpunktoperators III. Berechnung der Lösung mittels des Kleenschen Iterationsverfahrens Die Gleichung (*) definiert eine Abbildung zwischen Mengen. Sei seq Trägermenge der Sorte seq. => : Seq Seq ( s) n, r ; n , r S Es ergibt sich also die Fixpunktgleichung: (**) S [S ] Da (**) nicht nach S auflösbar ist, bleibt nur die iterative Lösung nach Kleene. Iterationsvorschrifft: S0 Si 1 [ Si ] Wir bilden also Ketten: S0 S1 [S0 ] S2 [S1 ] ... Bemerkung: (Stetigkeit von ?) . Beweis, indem man die Stetigkeit für die Vereinigung , Tupelbildung etc. …, zeigt Durchführung der Iteration S0 {} S1 [ S0 ] S1 { } n, r : n , r {} S1 { } {} S 2 [ S1 ] , (0, ), (1, ),... S3 { , (0, ), (1, )......(0, (1, )), (0, (2, ))...} => Nach der i-ten Iteration erhällt man alle Listen mit maximal(i-1) Elementen (2) Anwendung von „Kleene“ auf rekursive Funktionen: Seite 56 von 68 Fakultätsfunktion: let rec f n = match n with | 0 ->1 | n -> n * f(n-1);; (I) Zuordnung des entsprechenden .Operators n 0 [ F ](n) 1 n 0 n F (n 1) n 0 Bemerkung: wird häufg Funktional genannt, d.h. eine Abbildung die Funktionen auf Funktionen abbildet. Fixpunktgleichung: f (n) [ f ](n) Frage: Welche Halbordnung ist zu verwenden? => Halbordnung über Funktionen: f f ' x : f ( x) f '( x) Iterative Lösung von f (n) [ f ](n) : f 0 ( n ) (n) fi 1 (n) [ f i ](n) f 0 (n) f1 (n) [ f 0 ](n) n 0 n 0 f1 (n) 1 n 0 1 n 0 n n 0 n 0 n 0 f 2 (n) [ f1 ](n) 1 n 0 n f (n 1) n 0 Seite 57 von 68 n 0 1 n 0 f 2 (n) n n 1 0 n 0 n 1 n 1 0 n 0 n n 1 0 n 0 n 0 n 0 1 n 0 1 n 0 f 2 ( n) n n 0 1 n 1 n 1 n 1 n 0 1 n 0 f3 (n) 1 n 1 2 n 2 n 2 Folgerung: nach der i-ten 0 n i bekannt. n f0 f1 f2 f3 Tabelle: -1 Teration 0 1 1 1 ist der 1 1 1 Wertebereich 2 2 der Fakultätsfunktion 3 Umsetzung in ein Ocaml Programm: type intbot = Bottom | int;; let rec tau f x = match x with | 0 -> 1 | x -> x * f(x-1) | n when n <0 -> Bottom;; let rec iteration tau n =match n with | 0 -> Bottom | n -> tau (iteration tau (n-1));; Beispiel: (6- Iteration) Der Aufruf iteration tau 6;; liefert eine Funktion, die die Werte der Fakultätsfunktion für 0 n 6 berechnet. 4 für Seite 58 von 68 Semantik rekursiver Funktionsdeklarationen Beispiel (s. ÜB 5 Aufgabe 26) Gegeben sei folgende rekursive Funktionsdeklaration: fxt f = (natx, nat y) nat: if y = 0 then 1 else x* f(x,y-1) fi Wir wollen nun herausfinden was diese Funktion macht. Dazu geben wir das zu f zugehörige Funktional an: falls x y [ g ]( x, y) 1 falls x y 0 x * g ( x, y 1) falls x y 0 Nun berechnen wir die ersten vier Approximationsfunktionen und werden dann sicher sehen, was diese Funktion tut. f 0 ( x, y ) fi 1 ( x, y ) [ f i ] falls x y falls x y 0 1 f1 ( x, y ) [ f 0 ] 1 x* falls x y 0 ( y 1 y 1 1) x x *1 falls x y 1 0 falls x y y 1 falls x y 0 falls x y 1 Ich habe gerade mühsehlig die ganzen Berechnungsschritte in einem Formeleditor eingegeben. Durch einen blöden leichtsinnsfehler habe ich alle Daten bis auf die ersten beiden Approximationsfunktionen verloren. Bin zu genervt und hab auch besseres zu tun, als das ganze noch ein mal nieder zu tippen, also habt Nachtsicht mit der Qualität folgender Ersatzabbildung: Seite 59 von 68 Funktionale Programmierung und Kalkül . Beim Entwurf funktionaler Programme spielt der Funktionsname eine zentrale Rolle. Das zentrale Konzept der Rekursion stütz sich ebenfalls auf benannte Funktionen. Aber es gibt zahlreiche Fälle, bei denen man auf Namensgebung verzichten möchte, z.B.: o Beim Arbeiten mit Funktionalen (also z.B.: Fixpunktoperatoren) möchte man beteiligten Funktionen keinen festen Namen geben; er soll vielmehr variabel sein o Bei der Umformung von algebraischen Ausdrücken arbeiten wir mit Namen und geben „Zwischenausdrücken“ keinen Namen. Die Idee, auf benannte Funktionen zu verzichten, stammt von Russell (1910). Er schrieb Ausdrücke der Form x t(x) in der Form t[ x ]. Da Typograph das „^“ nicht setzen konnte, wurde zunächst ^x.t(x) daraus (von Church (1930) verwendet) und schließlich x.t ( x) Kalkül von Church systematisch ausgearbeitet. Literatur: Barendrengt (1984): Calculus . Es gelang Church, mit dem Kalkül alle mathematischen Ausdrücke, Zahlen, Funktionen, Boolsche Werte auszudrücken. Darin liegt die Beduetung für die Informatik. Mit Hilfe des Kalkül ’s lassen sich die zentralen Elemente funktionaler Sprachen ( = Datenstrukturen und Funktionen ) einheitlich beschreiben und auswerten. Syntax des (reinen ) Kalkül ’s) Terme im Kalkül sind folgendermaßen definiert: t := c | x | (t1t2) | x.t Hierbei heißt: (t1 t2): Applikation, d.h. eine Funktion t1 wird auf ein Argument t2 angewendet (t1(t2)) x.t : Abstraktion; represäntiert eine Funktion mit formalen Paramter x und dem Funktionsrumpf t; x ist in t gebunden. c: Konstante x,y,z: Variablen Bemerkungen Applikationen sind linksassoziativ: (t1 t2 t3 t4) = (((t1 t2)t3)t4) bindet so weit wir möglich nach rechts Seite 60 von 68 x.xx x.( xx) ( x.x) x Bemerkung: - Terme lassen sich als Bäume darstellen Term Baum c c x.t x x x (t1t2) t1 t t2 ( x. fx) y Beispiel: x f y x Ob eine Variable in einem Term frei oder gebunden ist, wird durch Funktionen „free“ bzw. „bound“ bestimmt: free(x) = {x} free(t1 t2) = free(t1) free(t2) free( x.t ) = free(t)\{x} bound(x)= bound(t1 t2) = bound (t1) bound(t2) bound( x.t ) ={x} bound(t) Beispiel: In den Ausdrücken y x IN : P( x, y)oder x ²dx oder 0 y x² ist jeweils x gebunden und y frei. x 0 Definition : - Beispiel: Ein Kombinator ist ein -Ausdruck , der keine freien Varialen besitzt. Currying: Reduktion nachstelliger Funktionen auf solche mit nur einem Argument Seite 61 von 68 INxIN IN Funktion y: ( x, y ) x y Darstellungn im -Kalkül: y = x. y. x y d.h. y: IN IN IN Auswertung dieser Funktion im -Kalkül. y 5 3 = (x. y.+x y) 5 3 = (y.+5 y) 3 =+53 Bemerkung: wird verändert ist gerade verändert worden Systematik des -Kalküls: siehe unten, Bedeutung und Verwendung der bisher undefinierten Terme „+“, „5“ und „3“ wird unten erläutert Reduktionsregeln im -Kalkül 1.) - Konversion „Gebundene Variablen können beliebig umbenannt werden“ Jede Abstraktion x.t darf ersetzt werden durch y. (t[ y / x]) „links bleibt stehen“ Beispiel: x( x. y. x y) x( z. y. zy ) not z ( z. y.zy ) not x( x. x.xx) 2.) - Konversion: Durch - Konversion werden - Terme vereinfacht, Konversion ist die zentrale Operation bei der Berechnung von - Ausdrücken Jeder Ausdruck ( x.t )t ' darf ersetzt werden durch t[t '/ x ] , d.h. „ x.“ fällt weg und alle Vorkommen von x in t werden durch t’ ersetzt. Gegebenenfalls müssen vorher - Konversionen durchgeführt werden. Durch - Konversion wird also eine Relation C Expr x Expr mit Expr gleich der Menge aller - Terme definiert mit ( x.t )t ' t[t '/ x] Beispiel: Seite 62 von 68 1.) ( f. x. fx) x ( f. x’. fx’) x ( x’. fx’)[x/f] ( x’.x x’) 2.) f = ( x.(y(y x))) y = ( y.y) Berechnung von f( z.z): ( x.(y(y x))) ( z.z) (y(y x)) [( z.z)/x] y(y( z.z)) y(( y.y)( z.z)) y(y[ z.z/y]) y( z.z) ( y.y)( z.z) ( z.z) Bemerkung: y ist Identitätsfunktion 3.) -Konversion: Die -Konversion ist ein Spezialfall der -Konversion; Relation Expr x Expr x.tx t falls x free(t ) Begründung: (x.tx) f (t x) [f/x] t f , falls x nicht frei in t ist. * Bezeichen wir mit den reflexiven und transitiven Abschluss der Relation , so lassen sich Normalformen definieren: * Ein - Term t’ ist die Normalform von t, falls gilt t t’ und kein t’’ Expr existiert, so dass t’ t’’. 28.05.03 www.teachscheme.org Bemerkung: Es gibt Terme, die keine Normalform besitzen ( x.xx)( x.xx) ( xx)[( x '.x ' x ') / x] ( x.xx)( x.xx) Seite 63 von 68 Satz: (ohne Beweis): Jeder - Term besitzt höchstens eine Normalform Darstellung zentraler Datentypen funktionaler Sprachen im - Kalkül. Der reine -Kalkül beschäftigt sich nur mit Ausdrücken. Dennoch können damit auch Objekte der üblichen Datentypen dargestellt werden: Boolesche Werte und Zahlen durch geschlossene Terme in Normalform. Boolsche Werte: Darstellung von „True“ und „False“ stützt sich auf ihre Verwendung in bedingten Ausdruck if b then e1 else e2 Im Sinne dieses Ausdrucks kann b als Funktion aufgefasst werden, die zwei Argumente e1 und e2 besitzt, ist sie „True“, dann wird erstes Argument ausgewählt; ist sie „False“, dann das zweite. Wir definieren: true x. y.x false x. y. y cond b. x. y. b x y mit b Element {True, False} Beispiel: true e1e2 (( x. y.x)e1 )e2 (( y.x)[e1 / x])e2 ( y.e1 )e2 e1[e2 / y ] e1 * Analog: False e1 e2 e2 Für den Aufruf if true then e1 else e2 schreiben wir: 2 2 Cond true e1 e2 = ((b. x. y b x y )true)e1e2 ((( x. y.true x y )e1 )e2 true e1 e2 e1 Natürliche Zahlen (Church- Numerale) Wie Boolsche Werte, so werden auch natürliche Zahlen durch geeignete -Terme dargestellt. Man nennt solche Zahlen „ Church- Numerale“. Trick: Die Zahl n wird durch ein Funktional repräsentiert, das eine Funktion f n-mal auf ein Argument x anwendet. Also: Seite 64 von 68 0 f . x.x 1 f . x.( f x) 2 f . x. f ( f x) ... n f . x. f n x Anzahl der Anwendungen von f repräsentiert die Zahl Bemerkung: Bei der Codierung von 0 f . x.x ist x.x Identitätsfunktion. Nachfolgerfunktion und Vergleichsoperator über Church-Numeralen succ n. f . x. f ( n Argument f x ) wirdBenötigt ,UmNumeral ApllikationAuszuführen 1 Beispiel: 1 1 succ 1 ( n. f . x. f (n f x)) ( f . x. f x) f . x. f (( f . x. f x) f x) f . x. f (( x. f x) x) f . x. f ( f x) 2 Vergleichsoperator für Vergleich mit 0 isZero n.n( x.False)True Beispiel: 1 1 isZero 1 ( n.n( x.False)True) ( f . x f x) ( f . x ( f x)) ( x.False)True ( x '.( x .False) x ')True , ( x .False)True False Additionsfunktion für Church- Numerale add m. n. f . x. m f (n f x) Also: add n m 2 2 , f . x.m f (n f x) f . x.m f ( f n x) f . x. f m ( f n x) f . x. f m n x m n Datentype „Liste“ in - Kalkül Seite 65 von 68 Neben elementaren Datentypen wie Zahlen und Wahrheitswerte sind Listen unverzichtbarer Bestandteil funktionaler Sprachen: Die Liste war ursprünglich der einzige Datentyp in Lisp. Zum Aufbau einer Liste werden Sprachmittel für die leere Liste und Konstruktoren benötigt. Im - Kalkül ist der Listen- Konstruktor eine dreistellige Funktion mit den Argumenten für den Kopf, für den Rest der Liste und Selektorfunktion zur Extraktion des Kopfs. cons h.t. s.s h t cons CAR CDR Die Selektoren head und tail geben Kopf und Rest zurück; sie werden wie folgt definiert: head l.l True tail l.l False wobei True und False die bekannten zweistelligen Funktionen sind, die das erste bzw. zweite Argument extrahieren. Beispiel: head(cons a b) = ( l.l True)(( h.t. s.s h t )a b) ((h.t.s.s h t) True) ( a b ) (s. s a b) True True a b a - Terme in angereicherten -Kalkül Für das Weitere gehen wir davon aus, dass die Grundtypen für Boolean und IN, sowie die wichtigsten Operationen auf diesen Datentypen definiert sind. Berechnung von Ausdrücken wird dadruch wesentlich übersichtlicher. Ein Ausdruck wie let x = 5 and y = 3 in x + y;; schreiben wir in der Form: (x.y.add x y) 5 3 add 5 3 8 Rekursion Im - Kalkül Seite 66 von 68 Rekursion als wesentlichen Elements funktionaler Sprachen ist im - Kalkül leicht darstellbar. Hierbei lassen sich rekursive Funktionen sowohl über rekursive - Terme (intuitiv) als auch über nicht rekursive Terme und geeignete Operatoren darstellen. Im Folgenden wird zunächst die intuitive, anschließend die operator gestützte Variante betrachtet. Grundidee: Ein benannter - Ausdruck enthällt seinen Namen im Rumpf. Beispiel: Paritätsberechnung einer Zahl u IN let even n = let rec even2 n = match n with | 0 -> True | 1 -> false | n -> even2 (n-2) in even;; Diese Funktion lässt sich in - Kalkül wie folgt berechnen ( die Funktion sub, prod, isZero und isOne seien gegeben). even = n.(isZero n) true ((isOne n) False (even 2 (sub n 2))) (isZero 2) True ((isOne 2) False (even2 (sub 2 2 ))) False True (isOne 2)…) * (isOne 2) False (even2 (sub 2 2)) 0 * False (rekursiver Aufruf) * even2 0 = n.(isZero n) True ((isOne) False (even (sub n 2))) 0 * isZero 0 True * True Rekursion im - Kalkül mittels Fixpunktoperatoren In der vorigen Darstellung spielte der Name eine wesentliche Rolle.Wichtiger Vorteil des - Kalküls ist es jedoch, auf Namen verzichten zu können. Mit Hilfe spezieller Operanten kassen sich „namenlose“ rekursive Funktionen verwirklichen. Wie errinern uns: Fakultätsfunktion ließ sich als Lösung der Fixpunktgleichung n 0 f (n) [ f ](n) mit [ f ](n) 1; n 0 n f (n 1); n 0 Seite 67 von 68 verstehen. As - Term ergäbe sich also: f = tau f mit tau = f.n.t wobei t ein geeigneter Term zur Umsetzung der jeweiligen Funktionen ist, imFalle der Fakutätsfunktion wäre z.B. t = (isZero n) 1 (mult n f (pred n)) Sei fix eine (von mehreren möglichen) Fixpunktoperatoren mit der speziellen Eigenschaft * für beliebige - Terme t fix t t (fix t) Dann kann die Lösung der obigen Fixpunktgleichung durch f = fix tau (anzuwenden auf f(n)) definert werden. Bemerkung: Der Operator fix iteriert auf der Funktion. Beispiel: Rekursive Definition der Additionsfunktion y für x 0 add ( x, y ) add ( pred x, succ y ) sonst Dazu lautet der -Term: add = x.y.(isZero) y (add (pred x) (succ y )) bzw. add = (f.x.y. (isZero x) y ( f (pred x) (succ y))) add Der tau- Operator hat also für diese Funktion die Gestalt: => tau+ = f.x.y. (isZero x) y (f pred x) (succ y)) add = fix tau+ = (f.x.y.(isZero x) y (f (pred x) (succ y)) (fix tau+) 1 2 * (isZero 1) 2 (fix tau (pred 1) (succ 2)) false 0 3 Seite 68 von 68 (Rekursion durch fix) fix tau 0 3 tau (fix tau) 0 3 * 3 Für den Gebrauch von Fixpubnktoperatoren siehe Übung Von hier an hat Knoll wieder Folien benutzt. Es ist also nicht mehr sinnvoll hier weiter zu machen. Siehe Folien von Knoll zu Beziehen auf http://www6.in.tum.de/info2/index.html