skript informatik grundkurs teil B 1 Inhaltsverzeichnis 1 Analyse von Algorithmen 1.1 Laufzeiten . . . . . . . . . . . . . . . . . 1.2 Eine erste Analyse eines Algorithmus . . 1.3 Weitere Untersuchungen an Algorithmen 1.4 Das Wachstum von Funktionen . . . . . 1.5 Die Komplexität von Algorithmen . . . 1.6 Praktische Grenzen von Algorithmen . . 1.7 Das Traveling Salesman Problem (TSP) 1.8 Theoretische Grenzen von Algorithmen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3 3 4 8 10 12 14 17 19 2 Endliche Automaten 2.1 Einführung . . . . . . . . . . . . 2.2 Nichtdeterministische Automaten 2.3 Äquivalenz von Automaten . . . 2.4 Exkurs : Zelluläre Automaten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23 23 26 28 32 3 Sprachen 3.1 Formale Sprachen . . . . . 3.2 Grammatiken . . . . . . . 3.3 Reguläre Grammatiken . . 3.4 Reguläre Ausdrücke . . . 3.5 Arithmetische Ausdrücke . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38 38 39 41 43 48 4 Aufbau eines Rechners 4.1 Der Weg zum modernen Rechner . . . . . . 4.2 Die von Neumann-Architektur . . . . . . . . 4.3 Ein Modellcomputer . . . . . . . . . . . . . 4.4 Von der Hochsprache zur Maschinensprache . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51 51 53 55 58 . . . . . . . 62 62 64 66 68 72 75 81 . . . . . . . . . . . . . . . . . . . . 5 Kryptologie 5.1 Einige klassische Verfahren . . . . . . . . 5.2 Das One-Time-Pad . . . . . . . . . . . . . 5.3 Rechnen mit Resten . . . . . . . . . . . . 5.4 Lineare Kongruenzen . . . . . . . . . . . . 5.5 Einwegfunktionen . . . . . . . . . . . . . . 5.6 Asymmetrische Verschlüsselungsverfahren 5.7 Signaturen . . . . . . . . . . . . . . . . . . 2 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 Analyse von Algorithmen 1.1 Laufzeiten Niemand wartet gerne und gerade beim Arbeiten mit Computern gibt es oft Situationen, in denen der Rechner scheinbar ewig für eine vorgesehene Aufgabe benötigt. Die Zeit, die ein bestimmter Rechner zum Ausführen einer Routine braucht, wird Laufzeit genannt. Die Aussagekraft der Laufzeit ist jedoch meist nicht sehr groß, da die gestoppten Zeiten eben nur für den untersuchten Rechner gelten. Dennoch kann man über ein Programm schon einiges aus der Laufzeit ableiten. Schon bei der Untersuchung von Sortierverfahren (siehe Teil 1) zeigte sich das quadratische Anwachsen von z.B. Bubblesort bereits in den gemessenen Laufzeiten. In Java ist es möglich mit der Methode System.nanoTime() die Zeit zu erhalten, die seit einem vom System willkürlich gewählten Zeitpunkt vergangen sind. Das scheint wegen der Willkür des Startpunktes wenig hilfreich, aber der Trick besteht darin, sich diese Zeiten vor und nach der Ausführung eines Programms zu holen und die verstrichene Zeit als Differenz zu erhalten. Allerdings darf man sich nicht wundern, dass bei System.nanoTime() sehr große Zahlenwerte zurückgegeben werden. Eine Sekunde unterteilt sich ja in eine Milliarde Nanosekunden, so dass hier schnell sehr große Werte entstehen. Um auch solche Zahlen zu erfassen, benötigten wir den größtmöglichen Integerbereich durch Verwendung von long. long startZeit=System.nanoTime(); // Hier jetzt etwas tun long endZeit=System.nanoTime(); // benötigte Zeit : endZeit-startZeit Beispiel 1. Wir erstellen eine Methode sternquadrat(int n), das in der Konsole Reihen mit dem Sternzeichen ausgibt. Dabei soll der Parameter n angeben, wie viele Sterne und wie viele Zeilen wir ausgeben (siehe Beispiel). Abbildung 1.1: Ausgabe der Sternenreihe für n = 10 3 1 Analyse von Algorithmen Zeit in Mikrosekunden Die Methode gibt die benötigten Zeiten in der Konsole in Mikrosekunden aus. Dabei ergeben sich von Durchlauf zu Durchlauf gewisse Schwankungen in den Zeiten (laut Abbildung z.B. 158 µs und 183 µs), da unser Rechner ja nicht allein unser Programm ausführt, sondern im Hintergrund auch das Betriebssystem unterschiedliche Anforderungen an die CPU stellt. Durch mehrfaches Wiederholen und Bestimmen der Mittelwerte erhalten wir genauere Laufzeiten. Das folgende Diagramm 1.2 zeigt die Ergebnisse auf einem älteren iMac. Die verschiedenen Anzahlen finden sich auf der waagerechten Achse, während die gemittelten Laufzeiten vertikal aufgetragen sind. n (=Anzahl der Reihen) Abbildung 1.2: Laufzeiten beim Schreiben der Sternreihen Offensichtlich zeigt sich hier kein linearer Zusammenhang zwischen der Anzahl n und der Laufzeit t(n) . Die Datenpunkte liegen näherungsweise eher auf einer Parabel, d.h die Laufzeiten wachsen quadratisch an. Wieso das ? Die einzelnen Zeichen werden mit der üblichen Ausgabe System.out.print(” ⇤ ”); erzeugt und wir gehen vereinfachend davon aus, dass das Schreiben eines Sterns in der Konsole immer eine bestimmte Zeit T benötigt. Es kommt dann nur noch darauf an zu bestimmen, wie oft diese Zeit T auftritt. Da wir bei n Sternen in waagerechter und n in senkrechter Richtung auf n2 zu schreibende Sterne kommen, ergibt sich näherungsweise eine Laufzeit von t(n) = T · n2 . Dies entspricht einer quadratischen Funktion1 vgl. wie bei f (x) = T · x2 . Lässt man das Programm auf einem anderen Rechner ablaufen, so erhält man nicht die gleichen Zeiten, aber auch wenn wir die Hardware ändern, so werden die Laufzeiten zu einem quadratischen Zusammenhang führen. Die genauen Zeiten sind daher völlig unwichtig und ändern sich mit jeder Hardware. Was aber bleibt, ist die Einteilung eines ablaufenden Programms in die Art des zeitlichen Ansteigens (quadratisch, linear, . . . ). So gesehen können wir bereits jetzt schon wieder aufhören, genaue Zeiten zu messen und sollten ein Programm bzw. einen Algorithmus lieber mathematisch untersuchen. 1.2 Eine erste Analyse eines Algorithmus Wir betrachten als ein spezielles Beispiel das Sortierverfahren SelectionSort ( dt. Sortieren durch Auswahl ) und versuchen dort das Vorgehen des Verfahrens mathematisch zu analysieren. Zur Erinnerung : Generell liegen die zu sortierenden Daten in einer großen Anzahl vor. Konkret könnten das etwa ganze 1 Wertet man das zweite Diagramm genauer aus, so zeigt sich beispielsweise bei n = 40 eine Zeit von t(40) = 3100 µs. Daraus folgt, dass in dieser Zeit genau 402 = 1600 Sterne gezeichnet wurden. Pro Stern dauert dies ca. 1, 9 µs. 4 1 Analyse von Algorithmen Zahlen, Buchstaben oder ganze Wörter in einem großen Array sein. Das Sortierverfahren SelectionSort geht dann folgendermaßen vor : • Suche im unsortierten Bereich das kleinste Element. • Bringe durch Tauschen dieses Element an den Anfang des unsortierten Bereichs. • Verschiebe die Grenze zwischen sortierten und unsortierten Bereich um eine Position. • Wiederhole das Vorgehen wie in Schritt 1 bis der unsortierte Bereich abgearbeitet ist. SelectionSort ( Sortieren durch Auswahl ) Das Bild zeigt den Ablauf des Verfahrens in einem einfachen Fall : sortiert tauschen tauschen sortiert A C D L X E C L A D X E kleinstes Element kleinstes Element aktuelle Position aktuelle Position tauschen tauschen sortiert sortiert A C D E X L A L C D X E kleinstes Element kleinstes Element aktuelle Position aktuelle Position tauschen sortiert sortiert A C D E L X A C L D X E letzter Buchstabe automatisch einsortiert kleinstes Element aktuelle Position Abbildung 1.3: Ablauf beim SelectionSort Verallgemeinern wir das Beispiel mit den Buchstaben, so können wir festhalten : • Bei SelectionSort werden n Elemente in n automatisch seine korrekte Position. ) 1 Durchgängen sortiert. ( Das letzte Element hat • Bei jedem Durchgang wird das kleinste Element gesucht und dabei Elemente miteinander verglichen. Dieses Vergleichen benötigt anfangs viel Zeit, geht aber immer schneller voran, da ja der unsortierte Bereich kleiner wird. • Es kommt bei jedem Durchgang zu einer Vertauschung. Notfalls wird auch ein Element mit sich selbst vertauscht. Definition. Sind bei einem Verfahren n Daten zu sortieren, so bezeichnen wir mit V G(n) und V T (n) die insgesamt nötigen Vergleiche und Vertauschungen, bis die Daten sortiert sind. Die Summe V G(n)+ V T (n) bezeichnen wir als Aufwand T (n) des Verfahrens. Im Falle von SelectionSort können wir die nötigen Vergleiche und Vertauschungen Schritt für Schritt durchgehen : 5 1 Analyse von Algorithmen Durchgang 1 2 3 4 5 1 2 3 4 5 1 2 3 4 5 ... ... ... n-1 n n-1 n n-1 n Nr Anzahl Vergleiche 1 n-1 1 2 n-2 1 3 n-3 1 n-1 1 1 n automatisch richtig automatisch richtig Anzahl Vertauschungen ... 1 2 3 4 5 1 2 3 4 5 ... ... n-1 n n-1 n Abbildung 1.4: Analyse von SelectionSort Die Zahlen V G(n) und V T (n) können wir diesem Schema durch Addieren entnehmen : V G(n) = n 2 + ··· + 3 + 2 + 1 1+n V T (n) = 1| + 1 + 1 + ·{z · · + 1 + 1 + 1} = n 1 n 1 mal Um die Summe bei V G(n) zu vereinfachen, hilft entweder der Blick in eine Formelsammlung oder ein alter Kniff, der der Legende nach vom Mathematiker Gauß bereits in der Grundschule benutzt wurde. Satz. Die Summe aufsteigender Zahlen lässt sich vereinfachen und es gilt : a) 1 + 2 + 3 + · · · + n = n(n+1) 2 b) 1 + 2 + 3 + . . . + n 1 = (n 21)n Beweis. a) Die einzelnen Summanden 1, 2, 3, . . .,n stellen wir uns als Kästchen vor, die zu Balken aufgestapelt wurden. Benutzen wir ein weiteres Mal diese Summe bzw. ihre Veranschaulichung als Balken, so können wir diese beiden Anordnungen aufeinander legen und erhalten ein Rechteck. Das Rechteck ist n Kästchen breit und n + 1 Kästchen hoch. Zählen wir alle Kästchen zusammen haben wir das Doppelte der gesuchten Summe und es gilt : 2 · (1 + 2 + 3 + . . . + n) = n(n + 1) Daraus ergibt sich sofort : 1 + 2 + 3 + ··· + n = 6 n(n + 1) 2 1 Analyse von Algorithmen 1 2 3 4 ... n n-1 1 2 3 4 ... n n 1 2 3 4 ... n Abbildung 1.5: Anordnen der Summanden b) Ersetzen wir in der Formel aus a) das n durch n 1 + 2 + 3 + ... + n 1= (n 1, ergibt sich : 1)(n 2 1 + 1) = (n 1)n 2 Damit können wir jetzt den Aufwand von SelectionSort berechnen : T (n) = V G(n) + V T (n) n(n 1) = +n 1 2 n2 n = +n 1 2 n2 n = + 1 2 2 Die entscheidende Potenz ist n2 und bei großen Werten von n gilt T (n) ⇡ n2 2 Der zeitliche Aufwand steigt daher näherungsweise quadratisch an. Verdoppeln wir die Datenmenge, so vervierfacht sich der zeitliche Aufwand. Verschiedene Fälle bei der Analyse In unserem mathematischen Abzählen bei den Durchgängen von SelectionSort haben wir angenommen, dass jedes Mal eine Vertauschung nötig ist. Programmiert man das Verfahren, kann man aber abfragen, ob das gerade gefunde kleinste Element eventuell schon an der richtigen Stelle steht. Dadurch lassen sich Vertauschungen eines Elements mit sich selbst vermeiden und der zeitliche Aufwand sinkt. Man unterscheidet daher bei der Analyse eines Verfahrens unterschiedliche Fälle : • worst case - Betrachtungen = maximale Anzahl bei V G(n) und V T (n) • best case - Betrachtungen = minimale Anzahl bei V G(n) und V T (n) • average case - Betrachtungen = durchschnittliche Anzahlen bei V G(n) und V T (n) Unsere Analyse war demnach eine worst case Untersuchung. Was ändert sich, wenn wir den bestmöglichen Fall ( best case ) angehen? 7 1 Analyse von Algorithmen An den bisherigen Vergleichen bei der Suche nach dem Minimum kommen wir nicht vorbei. Das Verfahren basiert ja nun einmal darauf, das kleinste Element zu finden. Sparen könnten wir allenfalls bei den nötigen Vertauschungen. Wenn jedes gefundene, kleinste Element immer schon am Anfang des unsortierten Bereichs steht, fallen keine Vertauschungen an. Daher ergibt sich dann : V G(n) = (n 1)n 2 , V T (n) = 0 Der gesamte Aufwand bleibt aber quadratisch : T (n) = (n 1)n 2 = n2 2 n 2 Wieder ergibt sich für große Werte von n: T (n) ⇡ n2 2 Satz. Für den zeitlichen Aufwand T beim Verfahren SelectionSort gilt in allen Fällen ( worst case, 2 best case ) ein quadratisches Ansteigen, d.h. T (n) ⇡ n2 . 1.3 Weitere Untersuchungen an Algorithmen Wie im vorherigen Abschnitt 1.2 untersuchen wir einzelne, spezielle Algorithmen beim Sortieren : Sortieren durch Einfügen ( InsertionSort ) Bei vielen Kartenspielen (Rummy, Canasta, ...) sortieren die Spieler ihre Karten auf der Hand auch aufsteigend aber meist verwenden sie dazu ein anderes Verfahren als Sortieren durch Auswahl. Oft nehmen sie eine Karte nach der anderen auf und ordnen jede dazukommende Karte in die bereits aufgenommenen und sortierten Karten richtig ein. Ein derartiges Verfahren nennt man Sortieren durch Einfügen (oder auch Insertion Sort). Bei diesem Algorithmus ist der unsortierte Teil zu Beginn leer. Jedes neue Element des noch unsortierten Teils wird dann an der richtigen Stelle eingefügt. sortiert aktuelles Element sortiert A C D L X E C L A D X E sortiert aktuelles Element sortiert C L A D X E sortiert aktuelles Element aktuelles Element A C D L X E aktuelles Element sortiert A C L D X E A C D E L X Abbildung 1.6: Sortierverfahren InsertionSort 8 1 Analyse von Algorithmen Schritt für Schritt wird das gerade einzusortierende Element so lange von rechts kommend mit seinen Vorgängern verglichen, bis man ein Element gefunden hat, das kleiner ist. Dann hat man die richtige Position gefunden, an der es einsortiert werden muss. Bei der Arbeit mit einer einzigen Liste muss jetzt aber erst der Platz an der entsprechenden Stelle durch Weiterrücken aller anderen Elemente geschaffen werden. Wird z.B. im letzten Schritt das E nach vorne gebracht, so wird der Buchstabe E zwischengespeichert, dann das X in das ehemalige Feld vom E geschrieben und das L in das ehemalige Feld vom X geschrieben. Schließlich kann der zwischengespeicherte Wert ( E ) den Platz vom L einnehmen. A C D L X E E 1. E zwischenspeichern A C D L X X E A C D E L X 2. X und L verschieben 3. E aus Speicher einfügen 4. fertig Abbildung 1.7: Details bei InsertionSort Um dieses Verfahren mathematisch zu analysieren, betrachten wir es wieder Schritt für Schritt und achten auf die nötigen Vergleiche und Vertauschungen ( siehe Abschnitt 1.2 ). worst case bei InsertionSort Wie im vorherigen Abschnitt erläutert gibt es verschiedene Ausgangssituationen ( best case, worst case, average case ), die man als Grundlage der Analyse verwenden kann. Wir betrachten den schlimmstmöglichen Fall, dass alle Buchstaben genau umgekehrt sortiert vorliegen. In diesem Fall wird bei jedem dazukommenden Element viel verglichen und letztlich das neu hinzugekommene Element ganz rechts eingefügt. Bei einer ähnlichen Betrachtung wie in 1.2 kommen wir zu folgendem Ergebnis : Durchgang 1 2 3 4 5 1 2 3 4 5 1 2 3 4 5 ... ... ... n-1 n n-1 n n-1 n Nr Anzahl Vergleiche Anzahl Vertauschungen 1 automatisch richtig automatisch richtig 2 1 2 3 2 3 ... 1 2 3 4 5 1 2 3 4 5 ... ... n-1 n n-1 n n-1 n-2 n-1 n 1 n-1 n Abbildung 1.8: Analyse von InsertionSort (worst case) Dazu ein paar Erläuterungen : 9 1 Analyse von Algorithmen • Das hinzukommende Element vergleicht ja von rechts kommend alle Elemente mit sich um seinen Platz zu finden. Bei falscher Reihenfolge bedeutet dies ein Vergleich mit allen Elementen im sortierten Bereich. • Beim Vertauschen beachten wir die Zuhilfenahme des Speichers ( oben erläutert ). Daher liegt bereits beim ersten Fall nicht ein einfacher Tausch vor sondern bereits zwei. Daraus ergibt sich : V T (n) = 1 + 2 + + · · · + n V G(n) = 2 + 3 + 4 + · · · + n 2+n 1 1+n Wie zuvor lassen sich beide Summen vereinfachen : V T (n) = (n 1)n 2 = n2 2 V G(n) = (1 + 2 + · · · + n n 2 1 + n) 1= = n(n + 1) 2 1= n2 n + 2 2 1 Also insgesamt : T (n) = V T (n) + V G(n) = n2 1 Das Ergebnis wird wieder von der größten Potenz von n bestimmt, d.h. für große Werte von n gilt: T (n) ⇡ n2 Satz. Für den zeitlichen Aufwand T beim Verfahren InsertionSort gilt im worst-case ein quadratisches Ansteigen, d.h. T (n) ⇡ n2 . 2 Im direkten Vergleich mit SelectionSort ( T (n) ⇡ n2 ) erkennt man als Gemeinsamkeit ein quadratisches Ansteigen des Aufwands. InsertionSort fehlt aber der Faktor 12 , so dass wir im worst case noch größere Zeiten erwarten. Sollten die Daten allerdings bereits vorsortiert sein ( best case ), so profitiert InsertionSort davon enorm. Satz. Für den zeitlichen Aufwand T beim Verfahren InsertionSort gilt im best-case ein lineares Ansteigen, d.h. T (n) = n 1 . Beweis. Als Übung selber erledigen. 1.4 Das Wachstum von Funktionen Fassen wir unsere erste Betrachtung der Sortierverfahren etwas allgemeiner zusammen : Wir gehen von einer gegebenen Angabe n aus, analysieren den Ablauf des Algorithmus und gelangen dann zu einem Zeitaufwand T (n). Ein solches Vorgehen ließe sich auch bei anderen Algorithmen anwenden, die überhaupt nichts mit Sortieren zu tun haben. Beispiel. Ein Computer soll im Telefonbuch einer Großstadt ( z.B. Mexico City mit 8,85 Millionen Einwohnern ) einen Namen finden. Das naheliegende Verfahren ist es für einen Computer vorne zu beginnen und alle Namen immer mit dem zu suchenden Namen zu vergleichen. Nennen wir n die Anzahl der Einwohner, so muss das Verfahren im schlimmsten Falle alle n Namen vergleichen. Man erkennt, dass die einfache Suche im worst case zu einem linearen Ansteigen von T (n) führt. 10 1 Analyse von Algorithmen Letztlich kann man also jedem Algorithmus, der zu seiner Ausführung von n Daten, Zeit benötigt, eine mathematische Funktion T (n) = · · · zuordnen. Verschiedene Verfahren miteinander zu vergleichen und zu beurteilen, welches Verfahren besser geeignet ist, heißt demnach mathematische Funktionen und ihr Ansteigen des Graphen zu vergleichen. f (x) x!1 g(x) Definition. Eine Funktion f wächst langsamer als eine Funktion g, wenn lim f (x) x!1 g(x) Eine Funktion f wächst schneller als eine Funktion g, wenn lim = 0. = 1 und f wächst gleich (x) schnell wie eine Funktion g, wenn lim fg(x) = c mit c als Konstante. x!1 Anmerkung : In Informatik verwenden wir oft statt der Variablen x die Variable n. p Beispiel. Bei einem zu lösenden Problem existieren zwei Verfahren mit T1 (n) = 300 n und T2 (n) = 1 400 n. Wir können diese per Grenzwert vergleichen : p p T1 (n) 300 n n 1 lim = lim = lim 120000 = lim 120000 p = 0 1 n!1 T2 (n) n!1 n!1 n!1 n n 400 n Daraus ergibt sich, dass T2 schneller wächst als T1 und damit bei großen Werten von n das ungeeignetere 1 Verfahren ist. Beachte, dass sowohl der kleine Vorfaktor 400 als auch der große Vorfaktor 300 keinen Einfluss auf das Ergebnis des Grenzwerts haben. Die hier neu eingeführte Schnelligkeit des Wachsens verhält sich bei den aus dem Mathematikunterricht bekannten Funktionen fast immer wie zu erwarten. Das beispielsweise T (n) = n4 schneller wächst als T (n) = n2 lässt sich zwar auch über die Grenzwerte begründen, ist aber auch schon anschaulich klar. Allerdings muss man an einer Stelle umdenken: Betrachten wir die zwei Funktionen T1 (n) = 10n2 und T2 (n) = n2 dann war bisher anschaulich klar, dass der Vorfaktor 10 für ein rascheres Ansteigen sorgt. Bei der Grenzwertberechnung erhalten wir aber : lim n!1 T1 (n) 10n2 = lim = lim 10 = 10 n!1 T2 (n) n!1 n2 Dieses Ergebnis sagt uns, dass hier ein gleich schnelles Anwachsen vorliegt. Wie passt das zusammen ? Unsere neue Definition ist so aufgebaut, dass konstante Vorfaktoren letztlich keinen Einfluss auf den Grenzwert haben. Lediglich die Art und Weise in der n auftritt ( d.h. quadratisch, linear, ... ) entscheidet darüber, wie eine Funktion ansteigt. So betrachtet treten im letzten Beispiel eben zwei quadratische Funktionen mit quadratischem Wachstum auf. Eine genauere Unterteilung von “quadratisches Wachstum” liefert unsere Definition nicht. Folglich lassen sich statt einzelner Funktionen ganze Klassen von Wachstum ( z.B. quadratisches Wachstum ) festlegen. lineares Wachstum T(n)=k · n logarithmisches Wachstum T(n)=k · log(n) quadratisches Wachstum T(n)=k · n2 Diverse T(n)= n · log(n) exponentielles Wachstum T(n)=k · an Wachstum Wurzelfunktion T(n)= k · n Abbildung 1.9: Wachstum in Klassen aufgeteilt 11 1 Analyse von Algorithmen 1.5 Die Komplexität von Algorithmen Die Analyse von InsertionSort ( Abschnitt 1.3 ) zeigte, dass es nicht nur vom Verfahren sondern auch von den gegebenen Daten selber abhängen kann, wie sich der Aufwand mathematisch darstellt. Dies Verfahren lieferte im best case ein lineares Ansteigen und im worst case ein quadratisches Wachstum. Wo soll man es einsortieren ? Wir schätzen in Zukunft ein Verfahren dadurch ab, indem wie der Aufwand des Verfahrens höchstens wächst. Für diese Angabe verwendet man die sogenannte O-Notation, die auf den Mathematiker Landau zurückgeht. f (x) x!1 g(x) Definition. Es seien f, g zwei Funktionen und es gelte lim = c < 1. ( Auch c = 0 ist möglich ! ). Dann wächst f höchstens so schnell wie g und man spricht davon, dass f zur Ordnung von g gehört. Als kurze Schreibweise : f (x) = O(g(x)) . Üblicherweise benutzt man für g einfache Funktionen wie x, x2 , x3 , log(x), . . . und meint dann mit O(x), O(x2 ), O(x3 ), . . . eine Menge von Funktionen, die alle gleich schnell oder langsamer wachsen als die im O angegebene Funktion2 . Die Angabe von O(· · · ) ist also im Wesentlichen die im letzten Abschnitt genannte Einteilung in verschiedene Klassen. So sind mit O(x) bzw. O(n) alle Funktionen gemeint, deren Aufwand maximal linear ist. Für das bereits erwähnte InsertionSort wäre dann T (n) = O(n2 ) , d.h. der zeitliche Aufwand ist maximal quadratisch aber kann - wie im best case - auch geringer ansteigen. Beispiel. a) n4 + 3n2 = O(n4 ) b) p 100 + 0, 5n3 = O(n3 ) c) 1 + n2 = O(n) Die O-Notation zeigt also schnell und übersichtlich an, mit welcher Art von Wachstum man zu rechnen hat. Diese Angabe markiert also den Aufwand eines Algorithmus und wird auch als Komplexität eines Algorithmus bezeichnet. Oft wird neben dem Zeitfaktor, den wir hauptsächlich betrachtet haben, noch der von einem Verfahren benötigte Speicherplatz für Berechnungen oder Zwischenschritte ins Auge gefasst. Davon sehen wir hier in diesem Skript aber ab. Definition. Die Komplexität eines Algorithmus ist eine Angabe zum benötigten Aufwand und bezieht sich oft auf den benötigten zeitlichen Bedarf. Mit Hilfe der O-Notation lässt sich die Komplexität bequem angeben. Je nachdem wie schnell die zugehörige mathematische Funktion wächst, kann man die unterschiedlichen Komplexitäten auch einteilen. Beachte, dass man bei Algorithmen immer ein langsames Wachstum bevorzugt. logarithmisch O(1) O( log x) linear O( x ) O(x) O(x log x) quadratisch exponentiell O(x2) O(2x) langsam wachsend O(xx) O(x!) schnell wachsend Abbildung 1.10: Verschiedene Komplexitäten 2 Streng genommen wäre also ein f (x)2O(g(x)) als Zugehörigkeit zur Menge O(g(x)) korrekter aber nicht immer setzt sich die korrektere Schreibweise durch. 12 1 Analyse von Algorithmen Komplexität bei Sortierverfahren Unsere bisherigen Überlegungen zu den verschiedenen Sortierverfahren lassen sich in folgender Übersicht zusammenfassen. Dabei ist Quicksort als bislang unbekanntes Verfahren nur aufgelistet um zu zeigen, dass es auch Sortierverfahren gibt, deren Komplexität geringer als O(n2 ) ist3 . Details zu diesem Verfahren kannst du dem Anhang entnehmen. Beachte ferner, dass vor allem der average case hervorgehoben ist als der in der Praxis wichtigste Fall. best case average case worst case Sortieren durch Auswahl (SelectionSort) O(n2) O(n2) O(n2) Sortieren durch Einfügen (InsertionSort) O(n) O(n2) O(n2) Bubblesort O(n) O(n2) O(n2) Quicksort O( n·ln(n) ) O( n·ln(n) ) O(n2) Abbildung 1.11: Komplexität verschiedener Sortierverfahren Ein Rechenbeispiel soll den großen Unterschied zwischen dem besten Verfahren mit O(n · log n) und den anderen Verfahren mit O(n2 ) verdeutlichen. Beispiel. Angenommen, dass sich bei einem Onlinespiel n = 10000 Personen angemeldet haben. Jede Person habe ferner eine gewisse Punktzahl, die den Erfolg beim Spielen angibt. Möchte man alle Spieler nach dieser Punktzahl sortieren, so muss man sich für ein Sortierverfahren entscheiden. Nehmen wir weiter an, dass sich dabei folgende Zeiten ergeben : tInsertion = 0, 6 s, tQuicksort = 0, 3 s Schlagartig wird das Spiel so bekannt, dass die Anzahl der Spielenden auf n0 = 66000 = 6, 6·n ansteigt. Welche neuen Zeiten ergeben sich daraus ? InsertionSort ist ein quadratisches Verfahren, daher ist : tInsertion,neu = (6, 6)2 · 0, 6 s = 43, 56 · 0, 6 s = 26, 1 s 3 Statt dem Term O(n ln(n)) findet sich auch vielfach O(n log(n)). Im Matheunterricht wird gezeigt, dass alle Logarithln(n) men nur Vielfache voneinander sind. So ist z.B. log10 (n) = ln(10) = 0, 434 ln(n). Daher spielt es im Grunde keine Rolle, welcher Logarithmus gemeint ist. Üblicherweise kürzt man aber in der höheren Mathematik mit log(n) den natürlichen Logarithmus ab. 13 1 Analyse von Algorithmen Bei Quicksort mit O(n · log n) gehen wir der Einfachheit halber von t(n) = C · n · log n aus. Mit dem Wertepaar n = 10000, t(n) = 0, 6 s können wir die Konstante C bestimmen. C= t(n) 0, 6 s = = 6, 51 · 10 n · log n 10000 · log 10000 6 s Dadurch, dass wir C jetzt kennen, lässt sich auch t(66000) berechnen : t(66000) = C · 66000 · log 66000 = 4, 77 s InsertionSort benötigt jetzt wesentlich länger. War Quicksort am Anfang doppelt so schnell, so steigt der Vorteil auf den Faktor 5, 47 an. Je größer die Anzahl der Spielenden, um so mehr gerät InsertionSort ins Hintertreffen. Interpretation der O-Schreibweise Kennt man bei einem Algorithmus die zugehörige Komplexität in der Form von O(· · · ) , so lassen sich daraus keine konkreten Laufzeiten auf einem bestimmten Computer berechnen. Allerdings sieht man, wie das Verfahren mathematisch von der Datenmenge n abhängt und kann daraus wie im letzten Beispiel Überlegungen anstellen, wenn die Datenmenge um einen bestimmten Faktor wächst. So bewirkt ein Verdoppeln von n bei einer linearen Komplexität auch eine Verdoppelung des Aufwands, während bei einer quadratischen Komplexität der vierfache Aufwand entsteht. Die folgende Tabelle zeigt erneut mögliche Komplexitäten und versucht diese auch sprachlich auszudrücken : kein Wachsen Ein Verdoppeln von n hat keine Auswirkung auf die Funktion f. f(n)=O(log n) logarithmisches Wachsen Ein Verdoppeln von n lässt die Funktion immer um einen konstanten Wert wachsen. f(n)=O( n) Wachsen wie die Wurzelfunktion Ein Verdoppeln von n lässt die Funktion um den Faktor 2 wachsen. f(n)=O(1) f(n)=O(n) lineares Wachsen Ein Verdoppeln von n lässt die Funktion auch auf das Doppelte ansteigen. f(n)=O(n log n) f(n)=O(n2) f(n)=O(2n) quadratisches Wachsen Ein Verdoppeln von n lässt die Funktion auf das Vierfache ansteigen. exponentielles Wachsen Steigt n um 1, so verdoppelt sich die Funktion. f(n)=O(n!) Abbildung 1.12: Übersicht der Komplexitäten 1.6 Praktische Grenzen von Algorithmen Eine weitere bekannte Aufgabenstellung in der Mathematik/Informatik besteht darin, bei einer gegebenen Zahl herauszufinden, ob sie eine Primzahl ist oder nicht. Primzahlen erkennt man daran, dass 14 1 Analyse von Algorithmen sie nur genau zwei Teiler, nämlich die 1 und sicht selbst, aufweisen. Alle anderen ganzen Zahlen heißen zusammengesetzt und weisen irgendeinen weiteren Teiler auf. Aus der Grundschule erinnert man sich auch noch an die ersten Primzahlen : 2, 3, 5, 7, 11, 13, 17, 19, 23, . . . Bei zunehmender Größe der Zahlen wird es aber durch bloßes Draufschauen schon sehr schwierig zu entscheiden, ob eine Zahl prim ist oder nicht. Ist 1247 eine Primzahl ? An dieser Stelle benötigen wir ein Verfahren, das ein Computer durchführen kann, um zu testen, ob eine Primzahl vorliegt oder nicht. Test auf Primzahl - erste Variante Die erste naheliegende Idee bei einer gegebenen Zahl n ist es, einfach alle Zahlen von 2 bis n 1 durchzugehen und zu schauen, ob irgendeine davon ein Teiler von n ist. Bei einer Zahl n benötigen wir daher im schlimmsten Fall n 2 Überprüfungen. Mit n als vorgegebener Zahl haben wir also ein Verfahren der Komplexität O(n) gefunden. Test auf Primzahl - verbesserte Variante Wie so manchmal im Leben ist die erste Idee nicht automatisch auch die beste. Ist unsere Zahl n keine Primzahl, d.h. eine zusammengesetzte Zahl, so ist n = a · b . Beide Zahlen a, b können aber nicht p gleichzeitig größer als n sein. Warum? p p p p Aus a > n und b > n würde dann ja sofort folgen : n = a · b > n · n = n . Letztlich wäre dann n > n, was Unsinn ist. p Somit ist eine der beiden Zahlen mit Sicherheit nicht größer als n, d.h. es gilt für einen der gesuchten p Teiler ( z.B. a ) : a n. p Zusammengefasst reicht es aus, beim Primzahltest von n nur den Bereich von 2 bis n durchzugehen. p Dadurch lässt sich die Komplexität auf O( n) reduzieren. Beispiel. Bei n = 1247 müssten wir nach der ersten Variante noch p die Zahlen 2, 3, . . . 1246 testen. Beim verbesserten Verfahren reichen die Zahlen von 2, 3, . . . , 35 aus ( 1247 ⇡ 35, 3). Die schlechte Nachricht Im Computerbereich werden Zahlen im Binärsystem mit Hilfe der Ziffern 0 und 1 bitweise dargestellt. Wenn Computer dann mit Zahlen rechnen, wird im Speicher eine gewisse Zahl an Bits für die Zahl verwendet. Nehmen wir an, dass eine Rechner 16 Bit für eine Zahl zur Verfügung hat. Dann lassen sich damit verschiedene Zahlen darstellen. Manche von ihnen benötigen gar nicht wirklich die vollen 16 Bit, andere schon : 20 = #00000000 00010100 100 = #00000000 01100100 800 = #00000011 00100000 3000 = #00001011 10111000 9000 = #00100011 00101000 55000 = #11010110 11011000 Während alle diesen Zahlen in unserem Zehnersystem immer mehr Stellen einnehmen, bleibt es beim Computer konstant bei 16 Bits. Daher verwendet man bei Primzahltests n eben nicht als Zahl selbst, sondern benutzt n als Anzahl der Bits. Mit n Bits ergibt sich als größte Zahl dann z = 2n 1 . Definition. Bei manchen Algorithmen, die Zahlen untersuchen, bezeichnet man mit n nicht die Zahl selbst, sondern die Bitlänge der Zahl. Dann gilt Zahl = z = 2n 1. Hat das eine Auswirkung auf unsere beiden Verfahren? 15 1 Analyse von Algorithmen Verfahren 1: Wir durchsuchen alle Zahlen von 2 bis z 1 = 2n 2 ⇡ 2n . Schlagartig besitzt diese Methode eine Komplexität von O(2n ), d.h. ein exponentielles Ansteigen. Verfahren 2: Vielleicht haben wir mehr Erfolg beim verbesserten Verfahren? Dort mussten wir ja nur bis zur Wurzel p der Zahl suchen, d.h. bis z. Nun ist aber ⇣ 1 ⌘n p n p p p 1 n z = 2n 1 ⇡ 2n = (2n ) 2 = 2 2 = 2 2 = 2 p n Es ergibt sich erneut eine exponentielle Komplexität O( 2 ). Der einzige Vorteil ist, dass hier die Basis p nur 2 ⇡ 1, 41 anstatt 2 wie im ersten Verfahren ist. Ein exponentielles Ansteigen der Zeit ist aber in p n der Praxis sehr ungünstig. Dazu brauchen wir uns nur die Funktion 2 für verschiedene Werte von n anzusehen : n p n 2 5 10 15 20 25 30 50 100 5,66 32 181 1024 5793 32768 33,55 Millionen 1,13 Billiarden Hierin zeigt sich das bekannte rasante Ansteigen der Exponentialfunktion und diesmal hat es dramatische Auswirkungen. Gehen wir von einem sehr schnellen Rechner aus, der vielleicht jede Sekunde eine Million Zahlen als mögliche Teiler überprüfen kann. Dann wären laut oberer Tabelle bei einer 25 Bit-Zahl maximal 5793 Division nötig und das wäre dann in weniger als 6 Millisekunden erledigt. Ein Zwinkern mit dem Auge. Verdoppeln wir aber die Länge der Zahl auf 50 Bit, so sind es 33,55 Millionen mögliche Divisionen und der Rechner braucht auf einmal 33,55 Sekunden. Kein Augenzwinkern mehr aber noch gut auszuhalten. Weiteres Verdoppeln (n = 100 Bit) führt dann aber auf 1,13 Billiarden= 1, 13 · 1015 Divisionen und mit einer Million Divisionen pro Sekunde benötigen wir plötzlich 1, 13 · 109 s = 35, 8 a. Schlagartig müssten wir fast ein halbes Leben damit verbringen auf die Antwort zu warten, ob eine Zahl eine Primzahl ist oder nicht. Und noch p schlimmer : Geben wir der Zahl nur 2 Bit mehr, also jetzt 102 Bit, dann haben wir den Faktor 2 auch zweimal mehr, d.h. die Zeit verdoppelt sich. Wir warten dann 71,6 Jahre und erleben vielleicht die Antwort gar nicht mehr. Vielleicht ist ja aber auch einfach nur der Rechner zu langsam ? Der aktuelle Rekord bei Rechnern liegt bei 5 Petaflops, d.h. 5 · 1015 Rechenoperationen pro Sekunde. Eine Division erfordert eigentlich mehrere Rechenschritte aber gehen wir einfach davon aus, dass in ein paar Jahren ein Rechner jede Sekunde 5 · 1015 Teiler überprüfen kann. Dann ist alles wieder gut, oder? Die 100 Bit-Zahl schafft dieser Rechner in 0, 23 s statt in 36 Jahren und selbst 102 Bit erhöhen die Zeit dann nur auf 0, 46 s. Letztlich bleibt aber die Exponentialfunktion Sieger. Bereits bei 150 Bit kommen p 150 wir auf 2 = 3, 78 · 1022 Divisionen für den Primzahltest und selbst der modernste Supercomputer braucht dafür 87,5 Tage. Bei 200 Bit rechnet er aber schon 8 Millionen Jahre und bei 230 Bit bräuchte man 263 Milliarden Jahre. So lange existiert noch nicht einmal das Universum. Zusammengefasst : Liegt bei einem Algorithmus eine exponentielle Komplexität vor, d.h. O(n) = an , so ergibt sich unabhängig vom konkreten Rechner - das Problem, dass bei großen Werten von n die benötigte Zeit so schnell ansteigt, dass es in der Praxis unmöglich ist, auf ein Ende des Ablaufs zu warten. Anders formuliert : Es gibt Verfahren, die ein Problem zwar rein theoretisch lösen ( und dies bei kleineren Eingabewerten auch konkret tun) aber bei großen Zahlen keine Ergebnisse in der Praxis liefern. 16 1 Analyse von Algorithmen 1.7 Das Traveling Salesman Problem (TSP) Ein weiteres bekanntes Beispiel für das schnelle Zunehmen von Möglichkeiten zeigt das Problem des Handlungsreisenden ( Traveling Salesman Problem oder kurz TSP ). Problem. Ein Vertreter einer großen Firma ist für einzelne Filialen im gesamten Bundesgebiet verantwortlich. Jeden Monat muss er jede Filiale einmal besuchen. Zwischen den einzelnen Städten gibt es entsprechende Distanzen, die er per Auto zurücklegen muss. Der Vertreter sucht jetzt nach einer Tour, die in einer Stadt beginnt, ihn jede Stadt genau einmal besuchen lässt und am Ende wieder zum Ausgangspunkt zurückführt. Dabei hat er natürlich im Sinn, eine solche Route zu finden, die eine möglichst geringe Gesamtstrecke aufweist. Beispiel. Die Städte nennen wir A,B,C,D und das folgende Bild gibt exemplarisch die Distanzen4 wieder : Probieren wir einige Routen aus und notieren dabei die Städte durch Buchstaben: B 121 A 95 139 145 C 105 100 D Abbildung 1.13: Traveling Salesman Beispiel • ABCD : 121 + 95 + 100 + 105 = 421 • ABDC : 121 + 139 + 100 + 145 = 505 • CBAD : 95 + 121 + 105 + 100 = 421 • DBCA : 139 + 95 + 145 + 105 = 484 Diese Beispiel zeigen einerseits, dass es nicht egal ist, welche Tour man einschlägt und andererseits, dass jede Auflistung der 4 Buchstaben eine Tour erzeugt. Somit bräuchten wir für eine Untersuchung aller Touren nur die vier Buchstaben in irgendeiner Reihenfolge hinschreiben. Wie viele solcher Folgen gibt es aber in diesem Beispiel? Beim ersten Buchstaben haben wir noch alle 4 Möglichkeiten. Bei der zweiten Stadt reduziert sich dies schon um eine Möglichkeit. Daher gibt es hier 4·3·2·1=24 verschiedene Buchstabenfolgen, die man notieren könnte. Bei einer derart geringen Zahl kann der Handlungsreisende schnell mit dem Taschenrechner alle Buchstabenfolgen ausprobieren. Definition. Für das Produkt n · (n 1) · (n 2) · . . . · 2 · 1 schreiben wir in Zukunft n! und sprechen von der Fakultät von n bzw. kürzer von n Fakultät. 4 Dabei muss mit “Distanz” nicht automatisch nur die Entfernung in km gemeint sein. Allgemein weist man zwei Städten eine Verbindung zu und gibt dieser Verbindung eine Zahlenangabe, die irgendwie den Aufwand ( Strecke, Fahrzeit, Kosten, ... ) berücksichtigt um von der einen Stadt zur anderen zu kommen. 17 1 Analyse von Algorithmen Allerdings sollten wir noch einmal kurz innehalten. Für das Hinschreiben von n Buchstaben gibt es n! Möglichkeiten aber nicht jede Buchstabenfolge ist automatisch eine neue, noch nicht betrachtete Reiseroute. Bedenken wir Folgendes: • Jede gegebene Tour lässt sich auch in umgekehrter Richtung abfahren und die Gesamtlänge bleibt dabei gleich. So habenABCD und DCBA die gleiche Gesamtlänge und sind einfach nur gespiegelt aufgeschriebene Buchsstabenfolgen. • Bei einer gefundenen Tour können wir den Startpunkt entlang der Tour beliebig verschieben und erhalten insgesamt dennoch die gleiche Gesamtlänge. Die Touren ABCD und BCDA oder DABC erzeugen am Ende die gleiche Gesamtroute. Die Buchstabenfolgen sind lediglich verschoben worden. Da wir jede Stadt als Ausgangspunkt verwenden können, haben wir hier vier Buchstabenfolgen, die alle zur gleichen Route führen. Somit sind es bei vier Städten zwar 4! = 24 Buchstabenfolgen aber nur Routen (nämlich ABCD, ACBD und ACDB ). Wir verallgemeinern: Satz. Für das Traveling Salesman Problem(TSP) mit n Städten gibt es 1 2 · (n 1)! 2 1 4 · 4! = 3 verschiedene mögliche Routen. Beweis. Wie am Beispiel mit 4 Städten starten wir mit n Städten und könnten jede Stadt durch einen Buchstaben abkürzen. Das Vertauschen der Buchstaben liefert n! mögliche Buchstabenfolgen. Jede Route lässt sich aber in umgekehrter Reihenfolge notieren, so dass wir als Zwischenschritt nur noch 12 · n! Routen haben und wenn wir dann noch berücksichtigen, dass wir jede Buchstabenfolge verschieben (und damit jede Stadt der Route als Startpunkt wählen) können, reduziert sich die Zahl der Routen auf 12 · n1 · n! = 12 · (n 1)! = (n 2 1)! Erhöht sich also durch Vergrößerung der Firma die Zahl der zu besuchenden Städte auf 11, so hat der Vertreter plötzlich die Auswahl aus 10! 2 = 1814400 Touren. Da erscheint der Einsatz eines Computers doch viel geeigneter. Allerdings geht auch jeder moderne Rechner schnell in die Knie angesichts des Wachstum der Fakultät. Wir hatten schon in einem früheren Abschnitt beim Wachstum der Funktionen festgestellt, dass n! sogar noch schneller wächst als 2n . Erhöht man die Zahl der Städte z.B. auf 120, so kommt es zu einer riesigen Anzahl von 119! = 2 196 2, 79 · 10 möglichen Touren durch diese Städte. Bei einer derart großen Zehnerpotenz brauchen wir unseren Rechner gar nicht erst alle Wege durchsuchen lassen. Das einfache Durchsuchen aller Methoden dauert viel zu lange. Um so wichtiger sind daher Algorithmen, die es erlauben, den besten Weg in viel schnellerer Zeit zu finden. Der dt. Mathematiker Martin Grötschel konnte so z.B. bereits 1977 das Problem mit 120 dt. Städten ( damals ausschließlich westdt. Städte der BRD und Berlin) vollständig lösen und die optimale Route finden. 18 1 Analyse von Algorithmen Abbildung 1.14: Optimale Route durch 120 Städte im Jahre 1977 Zusammengefasst : Beim TSP liegt ein Beispiel für ein Problem vor, bei dem man zwar genau weiß, was ein Computer alles berechnen müsste, um die gesuchte Lösung zu erhalten, aber die dafür benötigte Zeit sprengt in der Praxis den Rahmen. Dieser Fall tritt bei vielen Algorithmen auf, deren Komplexität exponentiell oder größer ist und die sich damit einer konkreten Berechnung entziehen. 1.8 Theoretische Grenzen von Algorithmen Im letzten Abschnitt ist die Erkenntnis gereift, dass es auch für Computer gewisse Grenzen gibt. Oft reicht die Zeit nicht aus, um bei einem Algorithmus zu einem Ende zu kommen. Eigentlich klingt das nicht so richtig nach einer Niederlage für den Computer, denn immerhin gab es doch bei den in 1.7 vorgestellten Problemen einen Lösungsweg und einen Algorithmus. Nur der Abschluss des Algorithmus ( auch Terminierung genannt ) dauerte zu lange. In diesem Abschnitt diskutieren wir eine neue Art von Grenze, die nichts mit der zeitlichen Durchführung zu tun hat, sondern eher eine Art logische Grenze darstellt. Die leidigen Endlosschleifen Wann immer man selbst Programme schreibt, kommt man irgendwann an die Stelle, dass man eine Endlosschleife programmiert hat. Dadurch gerät der Computer bei der Ausführung in eine Schleife, die er immer und immer wieder abarbeitet und die - durch den Fehler des Programmierers - nie endet. Algorithmus 1.1 Beispiel für eine Endlosschleife int i=0; while (i<10){ println("Es ist i=" + i ); // i=i+1; wurde leider vergessen } Nicht immer sind diese logischen Fehler allerdings so einfach zu finden wie in diesem Fall. Unsere 19 1 Analyse von Algorithmen Programmierumgebung ( Processing o.a. ) finden zwar fehlende Semikolon oder Klammern aber warnen uns leider nie vor logischen Fehlern. Wäre es nicht schön, wenn es ein Hilfsprogramm gäbe, das uns bei der Suche nach Fehlern unterstützt. Betrachten wir zwei konkrete Programme ( hier A und B genannt ), denen man beim Aufruf in einem Parameter n noch eine Zahl mit übergeben kann. A B // Programm A // n wird beim Aufruf übergeben // Programm B // n wird beim Aufruf übergeben while (n==n+1) { println("In der Schleife."); } println("Ende erreicht."); while (n==n) { println("In der Schleife."); } println("Ende erreicht."); Abbildung 1.15: Zwei mögliche Programme Der Quelltext A zeigt, dass wir unabhängig vom genauen Wert von n niemals in die Schleife kommen und das Programm immer endet. Die Bedingung n = n + 1 lässt sich ja für keinen Wert erfüllen. Bei B ist die Schleifenbedingung n = n aber immer korrekt, so dass wir in eine Endlosschleife geraten sind. Hätten wir jetzt unser neues Superhilfsprogramm namens AnhalteRichter, so könnten wir diesem Programm den Quelltext von A und irgendwelche Werte von n übergeben und AnhalteRichter sagt uns dann schon im Voraus, ob unser Programm zu einem vernünftigen Ende kommt oder ob es in einer Endlosschleife gefangen ist. Dazu reichen ja schon als Rückgabe die booleschen Werte true ( = Programm hält brav an ) und false ( = Programm hat Endlosschleife ). Das wahre Problem ergibt sich aber dann, wenn der Richter über sich selbst entscheiden soll. Dazu erstellen wir das folgende Programm C, das in sich selbst schon den Anhalterichter aufruft: Die knifflige Stelle ist die while-Schleife. Ihr Durchlaufen bzw. Nichtdurchlaufen hängt davon ab, wie der AnhalteRichter das Programm bewertet. Ist z.B. AnhalteRichter(C, n) = f alse , so wird die Schleife übersprungen und C wird beendet. Aber wenn C terminiert, dann wäre ja eher AnhalteRichter(C, n) = true korrekt gewesen. Seltsam! Gehen wir die beiden Fälle nochmal einzeln durch : • Das Programm C wird beendet, d.h. dann wäre die Bewertung AnhalteRichter(C, n) = true . Mit dieser Zeile kommt es aber beim Ablauf von C zu einer endlosen while-Schleife, d.h. das C dann nicht endet. Kurz : C endet =) C endet nicht • Das Programm C wird nicht beendet, d.h. es ist AnhalteRichter(C, n) = f alse. Dann wird die while-Schleife in C aber übersprungen und ein vernünftiges Ende von C erreicht. Also : C endet nicht =) C endet Wie wir es auch drehen und wenden, beide Varianten erzeugen Unsinn. Wo liegt der Fehler? Die Annahme, dass es ein Programm AnhalteRichter gibt, führte überhaupt erst zu den seltsamen, logischen Widersprüchen. Folglich kann es ein solches Entscheidungsprogramm nicht geben. Etwas verallgemeinert bedeutet dies, dass es mathematische Berechnungen gibt, die klar und eindeutig formuliert sind ( -> Algorithmus ) aber dennoch nicht bei jeder Eingabe berechnet werden können. Unser 20 1 Analyse von Algorithmen AnhalteRichter Programm A n=4 true, Terminiert Programm A n=12 true, Terminiert Programm A n=382 true, Terminiert Programm B n=5 false, Endlosschleife !!! Programm B n=32 false, Endlosschleife !!! Abbildung 1.16: Der Anhalterichter beschriebenes Beispiel zeigt ja gerade ein Programm namens AnhalteRichter , das nicht mit jeder Eingabe zurechtkommt. Die Grundlage dieser Programmidee geht auf den englischen Mathematiker Alan Turing und seine Ideen zum sogenannten Halteproblem5 zurück. Satz. ( von Turing ) Es gibt kein Programm, das für jedes Programm und jede Eingabe n die Frage beantwortet, ob das Programm nach endlich vielen Schritten anhält oder nicht. 5 Bereits 1918 formulierte ein anderer englischer Mathematiker namens Bertrand Russell ohne Bezug auf einen Computer ein ähnliches Paradoxon : In einer Stadt rasiert ein Barbier nur die Bärte von all denen Männern, die sich nicht selbst rasieren. So weit so gut, aber wer rasiert den Bart des Barbiers? 21 1 Analyse von Algorithmen C // Programm C // n wird beim Aufruf übergeben while ( AnhalteRichter(C,n)=true ){ println("In der Schleife."); } println("Ende erreicht."); Abbildung 1.17: Programm C 22 2 Endliche Automaten 2.1 Einführung Bildverarbeitung Stellen wir uns einen Science Fiction-Film vor, in dem ein Gebäude durch Roboter bewacht wird. Diese Roboter sind so konstruiert, dass sie beweglich sind, Bilder aufnehmen und nach bekannten Menschen durchsuchen können und sogar über Infrarot-Kamera kleine Laserwaffen zum Angreifen verfügen. Dadurch sollten sie in der Lage sein, das Gebäude zu bewachen und gut zu verteidigen. Andererseits Handlaserwaffe könnte ja der Fall eintreten, dass Wartungsarbeiten an den Robotern nötig sind oder man das Gebäude betreten muss. Daher gibt es einen Ausweis, der vom Roboter erkannt wird und den Besitzer als ungefährlich einstuft. Diese Sätze zur Beschreibung der Arbeitsweise können wir auch in ein kurzes Diagramm übertragen. Dabei verAbbildung 2.1: Fiktiver Bewachungsroboter wenden wir Zustände, in denen sich der Roboter gerade befindet und überlegen uns danach nur noch, wie der Roboter von einem Zustand in einen anderen kommt. Die Abbildung zeigt in einem Zustandsdiagramm, welche Aktionen den Roboter von einem Zustand in einen anderen versetzen. kein Mensch START Androide läuft an der Wand hin und her Sichtkontakt Androide bleibt stehen und Mensch analysiert Bild Androide wartet auf Ausweis Ausweis ungültig Androide greift an Ausweis akzeptiert kein Sichtkontakt Abbildung 2.2: Zustände und Übergänge beim Roboter Betrachten wir noch zwei weitere Beispiele : Beispiel. a) In einem Parkautomat lässt sich nach Einwurf von Geldmünzen ein Parkticket zum Kurzzeitparken kaufen. Es werden 20 ct, 50 ct und 1 € akzeptiert. Der Höchstbetrag liegt bei 1 € 23 2 Endliche Automaten insgesamt. Das Zustandsdiagramm zeigt die verschiedenen Übergänge der Zustände beim Einwerfen der Münzen. Parkuhr, Einwurf von 20ct, 50ct, 1 Ä maximaler Betrag : 1 Ä 0Ä 0,2 Ä 0,4 Ä 0,5 Ä 0,6 Ä 0,7Ä 0,8 Ä 0,9 Ä 1Ä Abbildung 2.3: Beispiel Parkticket b) In vielen Egoshootern trifft man auf Gegner, die vom Computer gesteuert werden. Damit diese sich einigermaßen sinnvoll durch die jeweiligen Levels bewegen, kann man ihnen verschiedene Zustände zuschreiben. Passiert eine gewisse äußere Aktion ändern sie entsprechend ihren Zustand. Diese Änderung kann man auch in Form einer Zustandstabelle angeben, die in der ersten Spalte alle möglichen Zustände auflistet und dann weiter erklärt, bei welchem Übergang man in einen anderen Zustand gelangt. Aktueller Zustand Folgezustand Bedingung Weglaufen AllesSicher Patrouille Angreifen SchwächerAlsGegner Weglaufen Patrouille Patrouille Bedroht & StärkerAlsGegner Bedroht & SchwächerAlsGegner Angreifen Weglaufen Abbildung 2.4: Zustände einer Computerspielfigur Prinzipiell lässt sich diese Tabelle natürlich auch in ein Zustandsdiagramm mit Kreisen und Pfeilen übertragen: Bedroht & SchwächerAlsGegner Weglaufen Patrouille Sch w ls erA äch Ge Bed Stä roht rke & rAl sGe gne r er gn AllesIstSicher Angreifen Abbildung 2.5: Computerfigur im Zustandsdiagramm 24 2 Endliche Automaten Das Konzept der Zustände und ihrer Übergänge lässt sich noch in einem anderen Kontext verwenden und erweitern. Als Beispiel dienen uns die in früheren Zeiten verwendeten römischen Zahlen. Dort gab es gewisse Buchstaben, die einen bestimmten Wert hatten. So waren I=1, V=5, X=10, L=20 und daraus ließen sich dann andere Zahlen zusammenstellen ( XV=15, LX=60, XX=20, ... ). Üblicherweise beginnt man mit dem größten Wert und steigt dann zu kleineren Werten hinab ( daher auch XV und nicht VX ). Einzige Ausnahme bildeten die Varianten IV=4, IX=9, XL=40, XC=90, CM=900. Schränken wir uns auf die Buchstaben I und V ein, so können wir allein daraus jede Menge Kombinationen bilden aber natürlich sind die meisten von ihnen keine zulässigen, römischen Zahlen. Wie kann ein Computer beim Einlesen der Zahl entscheiden, ob es eine gültige römische Zahl ist oder nicht? Wir verwenden einen leeren Anfangszustand und verwenden dann das nächste Zeichen ( Zahl von links gelesen ), um in einen anderen Zustand zu gelangen. Immer dann, wenn wir eine gültige Zahl gefunden haben, markieren wir diesen Zustand als möglichen Endzustand. Würde das Einlesen dort enden, dann hätten wir eine gültige Zahl vorliegen. Die jeweiligen Ziffern I und V bilden also nach und nach eine Art Reiseroute durch die Zustände und wenn die Übergänge korrekt aufgebaut sind, kann ein Computer den Übergängen folgen und am Ende entscheiden, ob eine römische Zahl erreicht wurde oder nicht. Start V V V I I I I I I,V V I,V I,V Abbildung 2.6: Römische Zahlen erkennen Listen wir noch einmal auf, was in diesem Beispiel zu erkennen war : • Es gab eine endliche Anzahl an Zuständen, von denen einer der Startzustand war. • Ein Teil der Zustände war besonders als Endzustand gekennzeichnet. • Es gab verschiedene Zeichen ( hier : I und V ), die eingelesen wurden und je nach Zeichen einen Übergang zu einem anderen Zustand boten. Haben wir alle diese Bestandteile vorliegen, so spricht man auch von einem endlichen Automaten (EA). Definition. Ein endlicher Automat A ist durch folgende Bestandteile gegeben : · eine endliche Menge ⌃ an Zeichen ( das sogenannte Eingabealphabet ) · eine endliche Menge S an Zuständen · einen Startzustand s0 2 S · eine Menge F als Teilmenge von S als Endzustände · eine Funktion , die die Übergänge zwischen den Zuständen angibt. Ist man allgemein im aktuellen Zustand s und liest das Zeichen a, dann gibt es ja irgendeinen Nachfolgezustand s0 . Genau dies soll die Zustandsüberführungsfunktion liefern, d.h. mathematisch kurz : (s, a) = s0 25 2 Endliche Automaten Alle nötigen Mengen und die Funktion bilden den Automaten, d.h. A = (⌃, S, s0 , F, ). Beispiel. Betrachten wir ein Beispiel mit ⌃ = {a, b} , S = {s0 , s1 , s2 } , F = {s1 } und a,b entnehmen wir s2 a b Start s0 a s1 b . der folgenden Abbildung. Lassen wir diesen Automaten verschiedene Wörter(=Buchstabenfolgen aus a und b ) durchgehen, so führt uns bei einigen Folgen der letzte gelesene Buchstabe in den Endzustand, während andere Wörter dort nicht enden. So kommen wir mit a, ab, abb, abbb alle zum Endzustand, während Folgen wie b, aba, aabb, baba dort nicht hin gelangen. Definition. Liest ein Automat Wörter ( = Zeichenfolgen ) ein, so kann nach dem Lesen des letzten Zeichens ein Endzustand erreicht werden. Solche Wörter heißen akzeptierte Wörter. Die Menge aller akzeptierten Wörter heißt L(A) und wird auch Sprache des Automaten genannt. Mitunter spricht man daher von einem endlichen Automaten auch als endlichen Akzeptor. In unserem Beispiel zeigt ein Blick auf die Abbildung, dass alle akzeptieren Wörter mit a beginnen und dann nur noch b folgen darf. Daher ist L(A) = {a, ab, abb, abbb, abbbb, . . .} oder mit einer Abkürzung L(A) = {abn |n 2 N}. Dabei meint natürlich bn keine Multiplikation, sondern die n-fache Aneinanderreihung von Buchstaben ( mitunter auch Konkatenation genannt ). 2.2 Nichtdeterministische Automaten Die bisher definierten Automaten waren so konstruiert, dass sie beim Abarbeiten eines Zeichens immer genau einen einzigen festgelegten Übergang zu einem anderen Zustand vorfanden. Kennt man den aktuellen Zustand s und das nächste zu lesende Zeichen a, so liefert (s, a) eine eindeutige Antwort auf den Nachfolgezustand. Definition. Ein Automat A, der in jedem seiner Zustände bei jedem Zeichen einen eindeutigen Nachfolgezustand besitzt, wird deterministischer, endlicher Automat (DEA) genannt. Abweichend von dieser Definition kann man sich aber auch Automaten vorstellen, die nicht so eindeutig festgelegt sind. Die folgende Abbildung zeigt einen solchen Automaten. Im Zustand s0 gibt es zwei Übergänge mit dem Zeichen 1. Dazu kommt, dass im Zustand s1 überhaupt keine Übergänge mehr zu finden sind. Wie ist das zu verstehen? Vom Zustand s0 entstehen zwei Möglichkeiten, die der Automat beide verfolgen kann. Dadurch verlieren wir zwar die Eindeutigkeit des Weges aber gewinnen eben eine Vielzahl an Möglichkeiten. Ist der Automat im Zustand s1 , so bringt ihn jedes weitere Zeichen zu überhaupt keinem Zustand mehr, d.h. der Automat wird mit einem undefinierten Zustand beendet. Setzt man dem Automaten jetzt irgendein Wort vor und gelangt der Automat bei mindestens irgendeiner der vielen Möglichkeiten zu einem Endzustand, so akzeptiert er das Wort. Der Automat in der Abbildung akzeptiert somit alle möglichen Wörter, die irgendwie anfangen ( in sich Drehen bei s0 ) und dann aber auf jeden Fall mit 1 enden. 26 2 Endliche Automaten 0,1 Start 1 s0 s1 Abbildung 2.7: Ein erster, seltsamer Automat Definition. Ein Automat A, der beim Lesen eines Zeichens mindestens einen Zustand mit nicht eindeutigem Nachfolgezustand besitzt, wird nichtdeterministischer, endlicher Automat (NDEA) genannt. Gibt es beim Lesen eines Worts mindestens eine Abfolge von Übergängen, die in einem Endzustand enden, so spricht man davon, dass der Automat das Wort akzeptiert. Wie bei den bisherigen Automaten benennen wir mit L(A) als Menge aller akzeptierten Wörter die Sprache des Automaten. Beispiel a) Der folgende Automat akzeptiert das Wort 100. Beachte, dass du dazu nur mindestens einen Weg finden musst, der zum Endzustand s1 führt. Schauen wir uns dazu einige Möglichkeiten an : s0 ! s0 ! s0 ! s0 1 0 0 s0 ! s1 ! s1 ! s1 1 0 0 s0 ! s2 ! N ichts 1 0 : 0,1 Start 1 s0 nicht akzeptiert s1 1 0 1 s2 akzeptiert nicht akzeptiert Abbildung 2.8: Beispiel NDEA Die letzte Übergangsfolge zeigt einen Weg, der nach Lesen von 1 zwar noch zu s2 führt, dort dann aber ohne weitere Übergänge versandet. Falsch wäre die Sprechweise, dass der Automat manchmal 100 akzeptiert und manchmal nicht. b) Der abgebildete Automat überprüft, ob ein Wort aus den Buchstaben a...z auf ug endet ( so wie in Betrug, Unfug, Zug, genug, ... ). Vom Startzustand s0 gibt es für den Buchstaben u zwei Übergänge, daher liegt ein NDEA vor. Dennoch ist der Automat gut zu verstehen und schnell konstruiert. Ein gleichwertiger DEA, der auch alle Wörter auf “ug” erkennt, wäre umfangreicher und auf Anhieb weder so einfach zu finden noch zu lesen. Erkennen von “ug” mit NDEA a-z Start s0 u s1 g s2 Abbildung 2.9: Erkennung von “ug” mit NDEA Pattern Matching Nehmen wir an, dass mehrere Personen zusammen an einem langen Reportage über Fotografie schreiben. Die einzelnen Beiträge der Autoren werden in einer Textarbeitung zusammengesetzt. Da fällt einem Autor auf, dass die verschiedenen Beiträge sowohl die Schreibweise “Foto” als auch “Photo” aufweisen. Vom aktuellen Duden wird zwar die Schreibweise mit “F” empfohlen aber wirklich falsch ist die ältere Variante mit “Ph” auch nicht. Natürlich kann die Schreibweise dabei nicht nur am Anfang auftreten sondern auch mitten im Wort zu finden sein ( Beweisfoto, Hochzeitsfoto, Aktfotografie, ... ). Um den Text aber einheitlich zu gestalten, müsste man jetzt sämtliche Seiten durchforsten auf der Suche nach der alten Schreibweise. 27 2 Endliche Automaten Erkennen von “ug” mit DEA a-z u u Start s0 u s1 a-z u,g g s2 u a-z u Abbildung 2.10: Erkennung von “ug” mit DEA An dieser Stelle bieten aber die meisten Programme eine Suchfunktion, d.h. man kann die Textverarbeitung anweisen nach der Zeichenkette “pho” zu suchen1 . Diese Suche nach einer bestimmten Zeichenkette innerhalb von Wörtern wird auch “Pattern Matching” genannt und lässt sich wunderbar mit Automaten realisieren. Die Abbildung zeigt einen einfachen NDEA, der irgendwo die Zeichenkette “pho” sucht. Der Automat geht den vorhandenen Text Zeichen für Zeichen durch und bei jedem Endzustand hält er an und zeigt dem Benutzer an, dass er die gewünschte Zeichenkette gefunden hat. a..z Start s0 a..z p h s1 s2 o s3 Abbildung 2.11: Suche nach “pho” 2.3 Äquivalenz von Automaten Das letzte Beispiel mit dem Automaten, der die Zeichenkette “pho” suchen soll, zeigt noch einmal, wie schnell und einfach ein nichtdeterministischer Automat entworfen ist, der das gewünschte Verhalten zeigt. In der Praxis stellt sich aber dann irgendwann doch die Frage, woher der Automat beim ersten Lesen eines “p” denn weiß, ob er noch im Startzustand s0 bleiben soll oder schon zu s1 wechselt. Den nachfolgenden Buchstaben kennt der Automat in seiner beschränkten Sicht ja noch nicht und hellseherische Fähigkeiten oder zufälliges, richtiges Raten trauen wir ihm auch nicht zu. Das Beispiel verdeutlicht noch einmal den Unterschied zwischen den DEA und NDEA in Bezug auf die Übergänge. Beide Arten von Automaten benutzen ein Alphabet ⌃ , eine Menge S an Zuständen und gewisse Endzustände F als Teilmenge von S . Der große Unterschied besteht in den Übergängen bzw. im benutzten . Bei den deterministischen Automaten erwarten wir immer eine klare Antwort, was den Nachfolgezustand angeht. Daher ist dann auch eben ein eindeutige Funktion, d.h. (s, a) liefert eben im Zustand s und beim Zeichen a einen eindeutigen Nachfolgezustand. Bei einem NDEA gibt es ja oft beim Lesen eines Zeichens mehrere Möglichkeiten für den Nachfolgezustand. So betrachtet ist es nicht mehr passend bei nur einen genauen Zustand zu erwarten. Mathematisch wird die Übergangsfunktion dahingehend abgeändert, dass sie eine Menge an Nachfolgezuständen liefert. Beim Automaten der Abbildung 2.11liefert z.B. (s0 , ”p”) eben beide Möglichkeiten als Menge, d.h. (s0 , ”p”) = {s0 , s1 }. Andere Zustände haben überhaupt keine Nachfolger. So ist z.B. (s1 , ”b”) nicht 1 Nur “ph” liefert zu viele andere Wörter wie “Physik”, “Graph”, “Phrase”, “Philosophie”, ... . 28 2 Endliche Automaten Startzustand festgelegt. Wir wählen daher die leere Menge als Weg ins Nichts, d.h. (s1 , ”b”) = { }. Die folgende Tabelle zeigt alle Übergänge : a..z ohne p,h,o p h o s0 { s0 } { s0 ,s1} { s0 } { s0 } s1 { } { } { s2 } { } s2 { } { } { } { s3 } s3 { s3 } { s3 } { s3 } { s3 } Abbildung 2.12: Übergänge mit Hilfe von Mengen Wir halten fest : Bemerkung. Ein nichtdeterministischer, endlicher Automat A = (⌃, S, s0 , F, ) unterscheidet sich von einem deterministischen Automaten nur in der Übergangsfunktion . Diese ordnet einem Zustand und einem Zeichen eine Teilmenge an Zuständen zu. Vergleich der beiden Arten Der deterministische Automatentyp scheint eher einer schrittweisen Computerlogik zu entsprechen. Wenn es klare Nachfolgezustände gibt, dann springt der Automat klar definiert von einem Zustand zum nächsten. Der NDEA mit seinen vielen Möglichkeiten erscheint etwas allgemeiner, erlaubt einfachere Diagramme aber lässt noch offen, wie der Automat beim Abarbeiten von Zeichen Entscheidungen trifft. Daher muss die folgende Aussage schon etwas überraschen : Satz. Für jeden nichtdeterministischen Automaten A gibt es einen deterministischen Automaten B, der exakt die gleichen Wörter erkennt, d.h. L(A) = L(B). ( Automaten mit der gleichen Sprache werden auch äquivalent genannt. ) Beweis. Der Beweis des Satzes ist ein konstruktiver Beweis, d.h. man kann ein direktes Vorgehen angeben, wie man aus einem NDEA einen äquivalenten DEA erhält. Daher zeigen wir das Vorgehen an einem Beispiel und überzeugen uns davon, dass das gleiche Vorgehen bei jedem NDEA funktioniert. Der Trick beim Umwandeln besteht grob gesagt darin, die bei auftretenden Mengen zu einem Zustand zusammenzufassen und dann konsequent fortzufahren. Daher spricht man auch von der sogenannten Teilmengenkonstruktion. Ausgangspunkt sei der per Abbildung dargestellte Automat : s3 1 Start s0 0,1 s1 1 0 s2 0 Abbildung 2.13: Umzuwandelnder NDEA 29 2 Endliche Automaten Beim fertigen DEA wird es ja auch wieder verschiedene Zustände geben. Um sie nicht mit den gegebenen Zuständen s0 , s1 , ... zu verwechseln, benutzen wir z0 , z1 , z2 , ... für die neuen Zustände. Schritt 1 : Anfangszustand Wir setzen z0 = {s0 } als neuen Anfangszustand. Schritt 2 : Alle erreichbaren Nachfolgezustände suchen Vom bisherigen, einzigen Zustand z0 suchen wir alle irgendwie erreichbaren Folgezustände. Hier zeigt uns ein Blick auf die Abbildung, dass wir sowohl mit 0 als auch mit 1 nur zu s1 gelangen. Alle Folgezustände ( hier halt nur s1 ) bündeln wir in eine eigene Menge und verwenden diese Menge als nächsten Zustand z1 . Also ist z1 = {s1 } Schritt 3 : Wiederhole das Verfahren von Schritt2 Der Zustand z1 bietet verschiedene Möglichkeiten. So kann man mit 0 bei s1 bleiben oder mit 1 zu s2 wechseln oder als dritte Variante mit 1 zu s3 vorankommen. Wir unterscheiden je nach Zeichen. Es ist (z1 , 0) = {s1 } aber diese Menge hatten wir ja bereits mit z1 benannt. Andererseits ist (z1 , 1) = {s2 , s3 } und da diese Teilmenge noch nicht aufgetreten ist, geben wir ihr den neuen Namen z2 . Also : z2 = {s2 , s3 } Da wir noch einen neuen Zustand erhielten, setzen wir das Verfahren fort und betrachten, welche Mengen wir von z2 aus mit 0 und 1 erhalten. Dabei müssen wir alle Möglichkeiten ( d.h. von s2 und s3 aus ) beachten. (z2 , 0) = {s1 } = z1 , (z2 , 1) = { } Die leere Menge trat auch noch nicht auf. Sie erhält den neuen Zustandsnamen z4 und auch diese Menge müssen wir weiterverfolgen. Bei der leeren Menge fällt dies natürlich sehr leicht : (z3 , 0) = { } = z3 , (z3 , 1) = { } = z3 Jetzt liegen keine neuen Teilmengen mehr vor und das Verfahren ist abgeschlossen. Aber wo hat der DEA seine Endzustände? Ganz einfach, als Endzustände wählen wir alle die Zustände aus, die in ihrer Menge irgendeinen Endzustand vom Originalautomaten besitzen. Der einzige Endzustand im Ausgangsbild war ja s3 und dies taucht nur in z2 auf. Folglich ist F = {z2 }. Alles in allem erhalten wir als DEA : Start z0 0,1 1 z1 0 z2 1 z3 0,1 0 Abbildung 2.14: Fertige Umwandlung Mit diesem Verfahren wird also ganz allgemein einer Teilmenge von Zuständen immer wieder eine neue oder eine bereits bekannte Teilmenge zugeordnet. Diese Zuordnung ist aber eindeutig ( und daher deterministisch ) und da es bei n möglichen Zuständen nur 2n mögliche Teilmengen gibt, ist das Ergebnis auch auf jeden Fall ein endlicher Automat. 30 2 Endliche Automaten Standardisiertes Vorgehen Statt alle Schritte aufzuschreiben, können wir Zeit und Platz sparen indem wir mit einer Tabelle arbeiten. Dazu beginnen wir stets in der ersten Zeile mit z0 = {s0 }, betrachten nach und nach die möglichen Zeichen und erstellen die möglichen Folgezustände als Teilmenge. Neue Teilmengen ( grün mit neu gekennzeichnet ) erhalten einen neuen Namen und werden in den nächsten Zeilen weiterverfolgt. Bereits in der Tabelle vorhandene Zustände ( mit Haken X ) brauchen dann nicht erneut gelistet zu werden. Endzustände können schließlich per Unterstreichen gefunden werden. Neue Zustände 0 1 z0={ s0 } { s1 }=z1 neu { s1 }=z1 neu z1={ s1 } { s1 }=z1 { s2, s3}=z2 z2={ s2, s3} { s1 }=z1 { }=z4 z4={ } { }=z4 { }=z4 neu neu Abbildung 2.15: Umwandlung per Tabelle Beispiel. Wir stellen uns die Aufgabe einen Automaten mit dem Alphabet ⌃ = {a, b} zu erstellen, der genau die Wörter akzeptiert, die an der vorletzten Stelle ein a aufweisen. Ein NDEA ist schnell gefunden. a,b Start s0 a s1 a,b s2 Abbildung 2.16: NDEA, der a an vorletzter Stelle erkennt Wir wandeln ihn mit dem Teilmengenverfahren per Tabelle in einen DEA um: Neue Zustände a b z0={ s0 } { s0, s1}=z1 z1={ s0, s1} { s0,s1,s2 }=z2 { s0, s2}=z3 z2={ s0,s1,s2 } { s0,s1,s2 }=z2 { s0, s2}=z3 z3={ s0, s2} { s0, s1}=z1 { s0 }=z0 { s0 }=z0 Abbildung 2.17: Umwandlung per Tabelle Diesmal gibt es zwei Endzustände und fertig ist der DEA, der ebenfalls genau die Wörter mit a an der vorletzten Stelle erkennt. 31 2 Endliche Automaten a b Start z0 a a z1 b a b z2 b z3 Abbildung 2.18: DEA, der a an vorletzter Stelle erkennt Fazit Vergessen wir vor lauter Begeisterung über das Teilmengenverfahren nicht die eigentlich Aussage : Bemerkung. Die Konzepte der deterministischen, endlichen Automaten (DEA) und der nichtdeterministischen, endlichen Automaten (NDEA) erweisen sich als gleichwertig. Wir können jeweils einen Automaten der einen Art in einen Automaten der anderen Art umwandeln und finden somit immer zwei äquivalente Automaten verschiedenen Typs, die dennoch genau die gleichen Wörter akzeptieren. 2.4 Exkurs : Zelluläre Automaten Mit dem Aufkommen der ersten Mikroskope in der Mitte des 17. Jahrhunderts gelang zum ersten Mal ein Einblick in die Welt des Allerkleinsten. Ungesehene Details traten hervor und viele Dinge, die mit bloßem Auge nicht zu sehen waren, zeigten sich den Augen der Forscher. Der englische Forscher Robert Hooke brachte 1665 in einem Sammelband namens Micrographia großarAbbildung 2.19: Zeichnung von R. Hooke tige Zeichnungen heraus, in denen er seine Beobachtungen festhielt2 . Hooke war es auch der durch seine Beobachtungen erkannte, dass größere Lebewesen in ihrem Aufbau eine Art Grundbaustein - quasi einen Legostein der Natur - besitzen und sprach von den Zellen eines Lebewesens. Daran angelehnt entwickelte man im Computerbereich viele Jahre später die Idee größere Ansammlungen mit Hilfe von kleinen Zellen darzustellen. Die Zellen existieren dann in verschiedenen Zuständen und besitzen vorgegebene Regeln nach denen sie sich weiter entwickeln, d.h. nach denen sie ihren Zustand ändern ( damit wären wir beim Automaten ). Im Unterschied zu den bereits behandelten DEA und NDEA lesen die Zellen aber keine Zeichen für den Nachfolgezustand, sondern bestimmen den Nachfolgezustand entweder durch mathematische Berechnungen oder durch zufällige Wahl. Zusammenfassend spricht man bei den Zellen von einem zellulären Automaten. Ein moderner Klassiker : Game of Life Das Paradebeispiel für zelluläre Automaten ist das vom englischen Mathematiker John Conway 1970 erfundene “Game of Life”. Der Titel ist etwas irreführend, da hier nichts gespielt wird, sondern eher eine Art Simulation vom Leben und Sterben einer zweidimensionalen Zellkultur abläuft. 2 Das Buch ist heute Teil der Library of Medicine und lässt sich virtuell durchblättern. Link : https://ceb.nlm.nih.gov/proj/ttp/flash/hooke/hooke.html 32 2 Endliche Automaten Ausgangspunkt ist eine zweidimensionale Anordnung von quadratischen Zellen wie in einem Rechenheft. Für jede Zelle gibt es dabei nur die Zustände “lebend” und “tot. Die lebenden Zellen kann man dann farblich hervorheben. Durch vorgegebene Regeln können sich dann die Zustände aller Zellen gleichzeitig ändern und eine neue Anordnung entsteht. Diese wird auch als nachfolgende Generation bezeichnet. Conway fand heraus, dass interessante Muster entstehen, wenn man die Übergänge zwischen den Zuständen davon abhängig macht, wie viele Nachbarn eine Zelle besitzt. Durch die quadratische Anordnung hat jede Zelle acht Nachbarn, die ja selbst immer nur tot oder lebendig vorliegen können. Abbildung 2.20: Acht Nachbarn einer Zelle Folgende Regeln sind auf lebende Zellen anzuwenden : Regel 1 : Eine lebende Zelle mit weniger als zwei lebenden Nachbarn stirbt in der nächsten Generation an Einsamkeit. Regel 2 : Eine lebende Zelle mit genau 2 oder genau 3 Nachbarn bleibt lebendig. Regel 3 : Eine lebende Zelle mit mehr als 3 Nachbarn stirbt an Überbevölkerung in der nächsten Generation. Die folgende Abbildung zeigt die Anwendung dieser Regeln auf eine zufällige Anordnung von Zellen. Dabei sterben die mit “x” markierten Zellen in der nächsten Generation. Allmählich sterben die vorhandenen Zellen alle aus. x x x x x x x x x x x x Abbildung 2.21: Folgegenerationen bei lebenden Zellen Die bisherigen Regeln erzeugen keinerlei neue Zellen. Daher gibt es eine vierte und letzte Regel : Regel 4 : Bei einer toten Zelle entsteht bei genau 3 lebenden Nachbarn neues Leben. Damit sind die Regeln von Game of Life vollständig und wir können die Zustände und ihre möglichen Übergänge wie bei einem Automaten darstellen. Dabei sind an den Übergängen nur die Zahl der lebenden Nachbarn notiert3 . 3 Das Game of Life “liest” insofern doch das nächste Zeichen ( = Zahl von 0 bis 8 ) nur dass diese Zahl nicht aus einem eingegebenen Wort stammt, sondern durch die Zustände der Nachbarn berechnet wird. 33 2 Endliche Automaten 3 2,3 lebend 3 tot <2, >3 Abbildung 2.22: Zustände und Übergänge Die neue 4. Regel ändert den bisherigen Ablauf vollständig. Wir benutzen die gleiche Anfangsverteilung, erhalten aber diesmal auch neue Zellen ( durch einen Stern markiert ) und einen ganz anderen Verlauf : x * * x x * * x * * x x x * x x * * x x * * x x * x * Abbildung 2.23: Generationenfolge mit Geburten Bei Processing findet sich unter Beispiele/Topics/Cellular Automata auch ein schon fertiges Programm zum Game of Life. Dort wird ein zweidimensionales Array aus Integerzahlen verwendet. Die Zahlen 0 ( tot ) und 1 (lebend ) stellen die beiden Zustände dar. Werfen wir einen Blick in den Quelltext an die Stelle, an der die Regeln umgesetzt werden : // We’ve checked the neigbours: apply rules! if (cellsBuffer[x][y]==1) { // The cell is alive: kill it if necessary if (neighbours < 2 || neighbours > 3) { cells[x][y] = 0; // Die unless it has 2 or 3 neighbours } } else { // The cell is dead: make it live if necessary if (neighbours == 3 ) { cells[x][y] = 1; // Only if it has 3 neighbours } } // End of if In einem ersten if-Teil wird überprüft, ob die aktuelle Zelle cellsBuffer [x] [y] einen Wert von 1 aufweist, d.h. lebendig ist oder nicht ( else-Teil). Bei lebendigem Zustand wird weiter unterschieden ( zweites inneres if), ob die Zahl der Nachbarn kleiner als 2 oder größer als 3 ist und sollte dies der Fall sein, so wird die Zelle cells [x] [y] auf 0 gesetzt. War die untersuchte Zelle aber gar nicht lebendig (else-Teil), dann muss nur noch geprüft werden, ob es bei genau drei Nachbarn zu einer Geburt einer neuen, lebenden Zelle kommt und eine 1 in cells [x] [y] gesetzt werden muss. Beachte, dass nur bei Änderungen eines Zustands neue Werte geschrieben werden. Schaut man ein wenig genauer, erkennt man, dass in diesem Programm zwei verschiedene Arrays vorliegen, zum einen das Array cellsBuffer und zum anderen das Array cells. Warum gibt es zwei verschiedene Arrays? Nehmen wir an, dass es im Programm nur ein einziges Array für die Darstellung aller Zellen gäbe. Dann durchläuft der Rechner alle Zellen nacheinander und berechnet den Zustand in der Folgege- 34 2 Endliche Automaten neration. Schreibt er diesen Wert aber sofort in das einzige Array, dann hat diese Änderung schon Auswirkungen auf die Berechnungen für die nachfolgende Zelle. Das Ergebnis wird verfälscht. Die folgende Abbildung erklärt diesen Effekt genauer an einem Beispiel dreier, übereinander liegender Zellen. Nach korrekter Anwendung der Regeln ergeben sich drei waagerecht liegende Zellen in der nächsten Generation. A und E haben nur einen Nachbar und gehen ein, C hat zwei Nachbarn und überlebt. Bei B und D entstehen neue Zellen, da es drei Nachbarn gibt. x * * x Abbildung 2.24: Beispiel für drei Zellen Nehmen wir jetzt an, dass ein Rechner das Feld von links oben nach rechts unten Zeile für Zeile durchgeht. Kommt er zu A, so stellt er fest, dass diese Zelle nicht überlebt. Trägt er aber diesen Wert direkt in das Array ein und ändert den Wert der Zelle, dann finden wir bei B auf einmal nur noch zwei Zellen in der Nachbarschaft. Dadurch kommt es dann zu keiner Geburt. Gleiches gilt für D. Insgesamt verändert das sofortige Eintragen die korrekte Berechnung und wir erhalten ein ganz anderes Muster : A B C A D B E C D E Abbildung 2.25: Falsches Muster bei sofortigem Ändern Daher passiert bei der Processingversion von Game of Life ungefähr Folgendes : a) Das Feld cells wird verwendet, um den aktuellen Zustand der Zellen grafisch darzustellen. b) Alle Zustände werden dann als Kopie in cellsBuffer abgelegt. c) Mit Hilfe von cellsBuffer werden alle Folgezustände berechnet aber nicht dort, sondern in cells gespeichert. Dadurch bleiben alle Werte von cellsBuffer während der ganzen Berechnungen unverändert. d) Nach der letzten Zelle beginnt der Vorgang von vorne. Einige der Figuren bei Game of Life haben Besonderheiten, die in ihnen verliehenen Namen zum Ausdruck kommt. So gibt es Figuren, die sich nach einer gewissen Anzahl an Generationen wiederholen und damit einen geschlossenen Zyklus bilden. Die abgebildeten Figuren wirken ohne Ansehen der nächsten Generationen allerdings leblos, so dass eine der zahlreichen Seiten zu Conways Game of Life hier weiterhelfen kann. Zyklische zelluläre Automaten Ein anderer Typ von zellulärem Automat wurde Mitte der 1980er Jahre von David Griffeath entdeckt. Mit einer ähnlichen Zellanordnung wie bei Game of Life besitzen in diesem Fall die Zellen mehr als nur zwei Zustände. Jede Zelle beinhaltet eine Integerzahl von 0 bis n 1 , so dass insgesamt n Zustände möglich sind. Ähnlich wie bei einer Uhr sind diese Zahlen miteinander im Kreis verbunden4 , d.h. nach n 1 folgt wieder die 0. Wieder gilt es die Nachbarn einer Zelle zu betrachten, allerdings diesmal ohne 4 Mathematiker sprechen davon, dass man die Zahlen modulo n betrachtet, d.h. nur den ganzzahligen Rest bei Division durch n benutzt. 35 2 Endliche Automaten Abbildung 2.26: Periodische Figuren die diagonal benachbarten Zellen. Dadurch hat eine Zelle nur vier Nachbarn. Griffeath legte schließlich noch eine einzige Regel fest, wie eine Zelle ihren Zustand wechseln kann. Regel : Ist eine Zelle im Zustand k und ist mindestens einer der vier Nachbarn im Zustand k + 1 , so wechselt die Zelle in der Mitte auch in den Zustand k + 1. Die Abbildung zeigt einige Zellen für den Fall n = 4 . Dabei sind die Zustände 0, 1, 2, 3 jeweils mit verschiedenen Farben gekennzeichnet worden. 0 2 1 1 0 2 2 2 0 2 3 3 3 1 3 2 0 2 3 3 1 3 3 3 0 1 2 1 1 2 3 2 2 3 3 3 2 0 1 2 2 1 2 2 2 2 3 2 Abbildung 2.27: Beispiel Automat Griffeath Der Bereich der Zellen ist bei diesem Automaten - ähnlich wie bei Game of Life - natürlich begrenzt. Daher verwendet man oft den Kniff, dass man sich den linken Rand verbunden mit dem rechten Rand vorstellt. Ebenso lässt sich der obere Rand mit dem unteren verbinden. Das hat zudem den Vorteil, dass nun wirklich alle Zellen die gleiche Anzahl an Nachbarn haben. Der einzige kleine Nachteil ist, dass man beim Betrachten der Nachbarn bei den Randzellen eben auch an der gegenüberliegenden Seite suchen muss. Beherzigt man diese Regel, so verändert sich die bisherige Abbildung : 0 2 1 1 1 2 2 2 2 2 3 3 3 3 3 0 0 3 0 1 3 1 3 2 0 2 3 3 1 3 3 0 2 3 0 1 3 0 1 2 0 1 2 1 1 2 3 2 2 3 3 3 3 3 3 0 0 3 0 1 2 0 1 2 2 1 2 2 2 2 3 2 2 3 3 3 3 3 3 0 Ränder verbunden Abbildung 2.28: Automat Griffeath mit verbundenen Rändern Immer noch sehen die bisherigen Bilder nicht nach etwas Besonderem aus. Erst bei genügend vielen Pixeln und etwas Laufzeit ergeben sich durch die einfache Regel schöne Grafiken. Selbst aus einem anfänglich zufälligen Muster an Zuständen ergibt sich nach kurzer Zeit etwas Geordnetes. Zunächst bilden sich zusammenhängende Flächen einer einheitlichen Farbe an mehreren Stellen. Doch bei genügend vielen Pixeln kommt es dann irgendwann zu Spiralen, in denen sich die Zustände geordnet wiederholen. Die folgende Abbildung zeigt sowohl die anfängliche Zufallsverteilung ( rechts unten ), die einheitlichen Flächen ( Mitte rechts ) sowie die Spiralen ( Mitte oben und unten ). Sehr eindrucksvoll lassen sich alle diese Phasen in einem Video betrachten : https://www.youtube.com/watch?v=rXl01oAxGHg 36 2 Endliche Automaten Abbildung 2.29: Muster nach 400 Generationen aus einer zufälligen Verteilung 37 3 Sprachen 3.1 Formale Sprachen Im vorherigen Abschnitt über endliche Automaten ging es immer darum, ob ein Automat eine vorgegebene Zeichenkette akzeptiert oder nicht. Auch bei unserer eigenen Schriftsprache gibt es Wörter, die wir akzeptieren ( z.B. Katze ) und andere, deren Schreibweise wir ablehnen ( z.B. Kaddse ). Andere Wörter haben ähnlich klingende Laute, schreiben sich aber dann doch ganz anders. ( z.B. Kaiser und heiser). Wir als Menschen benutzen zum erkennen der richtigen Schreibweise keinen Automaten, sondern haben größtenteils im Laufe der ersten Lebensjahre viele Wörter auswendig gelernt. Eine menschliche Sprache zu lernen und zu beherrschen erfordert eine enorme Anzahl an Regeln. Daher klingen ja auch viele Computer-Übersetzungen auch immer noch unfreiwillig komisch1 . Daher betrachten wir in diesem Kapitel viel simplere Sprachen und suchen nach einfachen Aufbauregeln für Sprachen. Kann man Sprachen so aufbauen, dass sie auf purer Logik aufgebaut sind? Beginnen wir zunächst mit einfachen Wörtern, die sich wiederum aus einzelnen Zeichen aufbauen lassen : Definition. Eine endliche Menge ⌃ von Zeichen heißt Alphabet. Eine endliche Zeichenkette, die aus den Zeichen des Alphabets gebildet wird, heißt Wort. Die Menge aller Wörter wird mit ⌃⇤ bezeichnet. Einzelne Wörte bezeichnen wir mit w1 , w2 , . . .. Beispiel. a) Mit ⌃ = {a, . . . , z} sind informatik, schule aber auch gfsdcnjur Wörter. b) Mit ⌃ = {0, 1} ist 1001 ein Wort. Benutzt man verschiedene Regeln, so lassen sich aus Wörtern schnell eine ganze Sprache erzeugen2 . Regeln : • Jeder Buchstabe ist zugleich auch ein Wort. • Sind v, w Wörter ( also v, w 2 ⌃⇤ ), so ist auch die Aneinanderhängung ( auch Konkatenation genannt ) vw ein Wort. • Es gibt ein leeres Wort "der Länge Null und es gilt : " · w = w · " = w Sämtliche Wörter, die man aus dem gegebenen Alphabet bilden kann, sind ja in der Menge ⌃⇤ erfasst. Üblicherweise ist diese Menge unendlich groß, denn schon bei einer geringen Zahl an Buchstaben gibt es dennoch schon unendlich viele Wörter, die man daraus bilden kann. Beispiel. Mit ⌃ = {a, b} erhalten wir ⌃⇤ = {", a, b, aa, ab, ba, bb, aaa, aab, . . .}. Definition. Jede Teilmenge L ✓ ⌃⇤ heißt formale Sprache über ⌃. Formale Sprachen sind demnach einfach Mengen an Wörtern! 1 Einfach und schnell auszuprobieren mit dem Google Translator und der englischen Übersetzung von “Meine Nachbarin hat Kohlmeisen.”. 2 Beachte, dass mit Sprache nicht eine gesprochene Sprache eines Landes gemeint ist. 38 3 Sprachen 3.2 Grammatiken Eine Sprache L als Teilmenge von ⌃⇤ lässt für die Wahl von L noch sehr viele Möglichkeiten offen ( von L = {} bis L = ⌃⇤ ) . Formale Sprache klingt zwar als Fachbegriff toll, ist aber zu weitläufig, denn oft möchte man für eine Sprache eben nicht alle Möglichkeiten zulassen, sondern möchte, dass die Wörter der Sprache einen gewissen Aufbau oder eine gewisse Gemeinsamkeit aufweisen. Man benötigt eine Struktur bzw. Regeln für die Sprache. Beispiel. Vereinfacht betrachtet sind Emailadressen nach folgendem Schema aufgebaut : name @ domain1.domain2 . TopLevel . Dabei kann es bei der Angabe der Domain aber auch nur einen einzigen Teil geben. So sind [email protected] oder [email protected] erlaubte Emailadressen. Andererseits ist nicht jedes Wort, das man aus dem Alphatbet ⌃ = {., @, a, b, ..., z} bilden kann auch eine gültige Emailadresse. Es gibt ein vorgegebenes Schema und nur innerhalb des Schemas kann man dann noch Buchstaben bzw. Wörter einsetzen. Auch natürliche Sprachen besitzen ( in gewissen Grenzen ) eine solche Struktur. Ein bekanntes Beispiel ist in der englischen Sprache die Struktur S P O, d.h üblicherweise besitzt ein englischer Satz die Bestandteile Subjekt, Prädikat und Objekt in dieser Reihenfolge. So ist “James kicks the ball.” eben ein korrekter Satz während “Kicks James the ball.” eben falsches Englisch erzeugt. Das Problem sind in diesem Fall nicht die Wörter oder ihre Schreibweise sondern die Struktur des Satzes und dieser Teil einer Sprache wird durch die Grammatik geregelt. Auch bei einfacheren Computersprachen werden wir daher Grammatiken einführen, die wie eine Vorgabe für erlaubte Wörter sind. Definition. Eine Grammatik G ist eine Struktur für eine anzugebende Sprache und besteht aus folgenden Teilen : Eine endliche Menge T ( sogenannte Terminalsymbole ), die die Zeichen enthält, aus denen die Wörter gebildet werden. Eine endliche Menge N ( sogenannte Nichtterminalsymbole ), die aus Hilfszeichen bestehen Einer endlichen Menge P an Produktionen, d.h. Regeln zum Ersetzen der Zeichen aus N Einem speziellen Zeichen S 2 N , das sogenannte Startsymbol Zusammenfassend spricht man auch von einer Grammatik G = (T, N, P, S) Die Rolle der Produktionen soll noch klarer dargestellt werden : Produktionen können wir uns wie Ersetzungen vorstellen. Man gibt sie in der Form x ! y an. Dabei ist x ein Nichtterminalsymbol, d.h. ein unfertiges Zeichen, das noch ersetzt werden soll. Die Produktionsregel legt fest, welches y wir anstelle des x zu notieren haben. Dabei kann y ein Terminalsymbol sein oder eine Kombination aus Nichtterminal- und Terminalsymbol. Um aus einer gegebenen Grammatik zu entnehmen, welche Wörter sich an die Grammatik halten, muss man die Wörter aus der Grammatik ableiten. Dazu beginnt man mit dem speziellen Startsymbol S und folgt den angegebenen Produktionen. Solange noch ein Nichtterminalsymbol enthalten ist, muss dieses durch weitere Regeln ersetzt werden. Ein Beispiel verdeutlicht, was gemeint ist : Beispiel. a) Es sei T = {0, 1}, N = {S, A, B} und die Produktionen seien gegeben durch P : S ! 1A, A ! 0B, B ! 0 Wie sieht eine mögliche Ableitung aus? S =) 1A =) 10B =) 100 Da wir bei den Produktionen nirgendwo eine Wahl hatten, führt in diesem Beispiel jede Ableitung immer nur zum Wort 100. Es ist wesentlich interessanter, wenn wir weitere Produktionen ergänzen, die uns bei den Nichtterminalsymbolen eine Auswahl lassen. 39 3 Sprachen b) Wir ergänzen das Beispiel aus a) und wählen erneut T = {0, 1}, N = {S, A, B} jedoch erweitern wir die Produktionen durch P : S ! 1A, A ! 0B, A ! 1, B ! 1A, B ! 0 Treffen wir jetzt auf die Nichtterminale A oder B, so können wir wählen und erhalten verschiedene, mögliche Ableitungen : S =) 1A =) 10B =) 101A =) 1011 S =) 1A =) 11 S =) 1A =) 10B =) 101A =) 1010B =) 10100 c) Ein weiteres Beispiel zeigt, wie man eine Grammatik wählen kann, die lauter Palindrome erzeugt, d.h. Wörter die von links wie von rechts gelesen gleich sind. Es sei T = {a, b, c}, N = {S}. Als Produktionen wählen wir P : S ! aSa, S ! bSb, S ! cSc, S ! a , S ! b, S ! c, S ! " Um Platz zu sparen werden wir in Zukunft Produktionen, die mit dem gleichen Nichtterminalzeichen ( hier S ) anfangen, kürzer zusammenfassen : S ! aSa | bSb | cSc | a | b | c | " Mögliche Ableitungen sind dann : S =) aSa =) aba S =) bSb =) bbSbb =) bbcScbb =) bbcacbb S =) cSc =) cbSbc =) cbabc Möchte man die Produktionen einer Grammatik anschaulich darstellen, so bieten Syntaxdiagramme dazu eine Möglichkeit. Man listet für jedes Nichtterminalsymbol die möglichen Ersetzungen als grafische Wege auf. Beim letzten Beispiel der Palindrome ergibt sich : S a S a b S b c S c a b c Abbildung 3.1: Syntaxdiagramm Alle Wörter, die sich auf irgendeine Weise aus einer gegebenen Grammatik ableiten lassen, nennt man auch die von der Grammatik erzeugte Sprache L(G). 40 3 Sprachen 3.3 Reguläre Grammatiken Die bisherigen Beispiele für Grammatiken ordneten jeweils einem Nichtterminalzeichen eine oder mehrere Produktionen zu. Definition. Eine Grammatik G = (T, N, P, S) heißt kontextfrei, wenn in sämtlichen Produktionsregeln auf der linken Seite nur ein einziges Nichtterminalzeichen steht. Anders ausgedrückt : Jede Regel in P ist von der Form X ! · · · mit X 2 N . Die von einer solchen Grammatik erzeugte Sprache L(G) wird kontextfreie Sprache erzeugt. Der Name für diese Art von Grammatik ergibt sich daraus, dass man für jedes Nichtterminalzeichen eindeutige Ersetzungen vorfindet, d.h. der vorherige Teil des entstehenden Wortes spielt keinerlei Rolle. Die Ersetzung benötigt keinerlei Wissen oder Kontext von den vorherigen Regeln. Alle Beispiele in 3.2 waren kontextfreie Grammatiken. Beim jetzigen Stand der Kenntnisse über Sprachen ergibt sich somit folgendes Bild : Menge aller Sprachen * Menge der kontextfreien Sprachen Abbildung 3.2: Bisherige Unterteilung von Sprachen Schränkt man die Art wie die Regeln einer Grammatik auszusehen haben noch weiter ein, so gelangt man zu einer weiteren Untermenge an Sprachen : Definition. Eine G = (T, N, P, S) heißt regulär, wenn sie kontextfrei ist und bei jeder Produktionsregel auf der rechten Seite höchstens ein Terminalzeichen gefolgt von höchstens einem Nichtterminalzeichen steht. Anders formuliert : Jede Regel in P ist von der Form X ! a oder X ! Y oder X ! aY oder X ! ". Sprachen, die von einer solchen Grammatik erzeugt werden, heißen reguläre Sprachen. Mit einem Blick zurück auf die Beispiele in 3.2 lässt sich erkennen, dass es sich bei c) nicht um eine reguläre Grammatik handelt. Eine Produktionsregel wie S ! aSa besitzt eben schon zwei Terminalzeichen und ist dann schon nicht mehr regulär. So wie die Regeln aufgebaut sind, lässt sich erkennen, dass alle Wörter sehr regelmäßig von links nach rechts aufgebaut werden. Was ist so besonders an regulären Grammatiken? Bei einer regulären Grammatik sind die Produktionsregeln ja so aufgebaut, dass wir auf der linken Seite ein einzelnes, eindeutiges Nichtterminalzeichen ( z.B. X ) haben. Für dieses X gibt es dann ja nur die folgenden Möglichkeiten : • X ! a, d.h. das bestehende X wird direkt durch einen Buchstaben ersetzt und das Wort ist beendet. • X ! aY , d.h. wir ergänzen im Wort den Buchstaben a und fahren dann bei den Regeln von Y fort. 41 3 Sprachen • X ! Y , d.h. wir kommen vom Nichtterminal X direkt zu einem anderen Nichtterminal Y , für das dann andere Produktionsregeln gelten. Das zu bildende Wort erhält keinen weiteren Buchstaben und eigentlich könnte man die Regeln von Y auch direkt bei X ergänzen. Ein wenig abstrakter betrachtet beginnen wir beim Zustand X und kommen dann unter Schreiben eines Zeichens a zu einem weiteren Zustand Y oder beenden mit dem Schreiben von a das Wort. Diese X a Y a X Abbildung 3.3: Übergänge bei regulären Sprachen Abbildungen kommen uns doch arg bekannt vor vom Thema Endliche Automaten und in der Tat zeigen die nächsten beiden Sätze, dass Endliche Automaten und reguläre Grammatiken die gleiche Art von Sprachen beschreiben. Satz. (Grammatik!Automat ) Es sei G = (T, N, P, S) eine reguläre Grammatik und L(G) die zugehörige reguläre Sprache. Dann gilt : Es gibt einen endlichen Automaten A = (⌃, S, s0 , F, ) mit L(A) = L(G), d.h man kann bei einer regulären Grammatik immer schon einen Automaten erzeugen, der exakt die Wörter akzeptiert, die auch die Grammatik erzeugt. Beweis. Beim Beweisen vermeiden wir einen strengen, mathematischen Beweis und konzentrieren uns darauf, wie man aus einer gegebenen Grammatik den zugehörigen Automaten findet. 1. Als Menge aller Zustände wählen wir S = N , d.h. die Nichtterminalzeichen nehmen die Rolle der Zustände ein. 2. Als Startzustand s0 wählen wir das Startsymbol S . 3. Als Menge aller Endzustände wählen wir alle die Nichtterminalzeichen X , die eine Regel der Form X ! a oder X ! " aufweisen. Denn eine solche Regel bedeutet ja, dass die Grammatik das Wort beendet hat und unser zugehöriger Automat zwingend einen Endzustand aufweisen muss. 4. Für das Alphabet des Automaten kommen nur die Terminalzeichen in Frage, d.h. ⌃ = T . 5. Auch bei der nötigen Übergangsfunktion fällt eine Festlegung nicht schwer. Aus der Regel X ! aY entwerfen wir einen Übergang von X nach Y beim Lesen von a , d.h. (X, a) = Y . Für alle weiteren Regeln wie X ! a oder X ! " fehlt uns rechts vom Pfeil ein Zustand. Daher ergänzen wir bei der Menge der Zustände einen weiteren Endzustand E und können dann durch (X, a) = E bzw. (X, ") = E beide Regeln in Übergänge übersetzen. Das abstrakte Vorgehen im Beweis erschließt sich besser, wenn man es an einem konkreten Beispiel probiert. Beispiel. Wir kehren zum Beispiel b) im Abschnitt 3.2 zurück. Dort war eine Grammatik mit T = {0, 1}, N = {S, A, B} undP : S ! 1A, A ! 0B | 1, B ! 1A | 0 gegeben. Wie im Beweis beschrieben wählen wir ⌃ = T = {0, 1} sowie die Zustände s0 = S, s1 = A, s2 = B sowie einen Endzustand s3 = E. Die gegebenen Produktionen führen uns dann zu den noch nötigen Übergängen . Insgesamt haben wir damit die Grammatik in einen NDEA umgewandelt, der die gleiche Sprache besitzt. Start S 1 0 A B 1 1 0 E Abbildung 3.4: Ergebnis der Umwandlung 42 3 Sprachen Auch die umgekehrte Umwandlung vom Automat zur Grammatik ist möglich. Satz. (Automat!Grammatik ) Es sei A = (⌃, S, s0 , F, ) ein endlicher Automat und L(A) seine Sprache. Dann gilt : Es gibt eine reguläre Grammatik G = (T, N, P, S) mit L(G) = L(A), d.h. man kann genau die akzeptierten Wörter eines Automaten auch mit Hilfe einer regulären Grammatik erzeugen. Beweis. Im Wesentlichen ordnen wir wie beim vorherigen Satz wieder die Zustände den Nichtterminalen zu und verwenden als Terminale die Zeichen des Automaten. Aus jedem Übergang (sx , a) = sy können wir die Grammatikregel Sx ! aSy gewinnen. Ist im Automaten ein Endzustand SF erreicht, so ergänzen wir einfach eine Regel mit Hilfe des leeren Zeichens SF ! ". Dadurch endet das Wort der Grammatik auf jeden Fall und der Endzustand wurde erreicht. Alle weiteren Details entsprechen der Hinrichtung ( siehe erster Beweis ). Beispiel. Gegeben ist der Automat A = (⌃, S, s0 , F, ) mit ⌃ = {0, 1} wie in der nachstehenden 0,1 Start s0 1 s1 0 s2 0 s3 A B C D Abbildung. Offenbar liegt hier ein NDEA vor, der erkennt, ob eine Bitfolge mit 100 endet. Bei der Umwandlung legen wir S = s0 , A = s1 , B = s2 und C = s3 fest und übersetzen die Übergänge als Produktionen : S ! 0S | 1S | 1A, A ! 0B, B ! 0C C ! " Streng genommen benötigen wir das " am Ende nicht und könnten die Produktionsregel bei B vereinfachen zu B ! 0 . Ferner ist klar, dass N = {S, A, B, C} und T = ⌃ = {0, 1} zu wählen sind. Fazit Alles in allem zeigte dieser Abschnitt, dass reguläre Grammatiken und Endliche Automaten zwei gleich gelungene Konzepte sind, um Sprachen ( = akzeptierte bzw. erzeugte Wörter ) umzusetzen. Reguläre Sprachen sind die von Automaten akzeptierten Sprachen und umgekehrt. Menge aller Sprachen * Menge der kontextfreien Sprachen Menge der regulären Sprachen Abbildung 3.5: Bisherige Sprachen im Überblick 3.4 Reguläre Ausdrücke Nach den endlichen Automaten und den Grammatiken lernen wir noch einen dritten Weg kennen, die Wörter einer Sprache zu beschreiben. Diese Variante heißt reguläre Ausdrücke ( engl. regular expressions oder kurz regex ) und ist in vielen Computerprogrammen weit verbreitet. Reguläre Ausdrücke benötigen keine Zustände und Übergänge und auch keine Ableitungen aus Regeln sondern sind eher so 43 3 Sprachen wie Baupläne zum Erstellen von Wörtern. Es gibt einige wenige Grundregeln, aus denen man in einer rekursiven Form größere Wörter aufbaut. Ausgangspunkt ist wie bei den anderen beiden Verfahren zunächst wieder ein Alphabet ⌃ aus Zeichen. Dann orientiert man sich an den folgenden Regeln : a) Das leere Zeichen " und jeder einzelne Buchstabe a 2 ⌃ ist ein regulärer Ausdruck. b) Hängt man zwei reguläre Ausdrücke R, S aneinander ( Fachbegriff : Konkatenation ), so ist das Resultat RS wieder ein regulärer Ausdruck. c) Hat man zwei reguläre Ausdrücke R und S , dann ist (R | S) auch ein regulärer Ausdruck und meint, dass man sich an dieser Stelle für R oder S entscheiden soll. d) Ist R ein regulärer Ausdruck, so ist auch (R)⇤ ein regulärer Ausdruck und bedeutet, dass man R beliebig oft ( auch keinmal ) wiederholen kann. Diese Wiederholung eines Ausdrucks heißt Iteration oder auch Kleene’sche Hülle und wird oft - sofern es eindeutig ist - nur mit R⇤ abgekürzt. e) Ist R ein regulärer Ausdruck, so ist auch (R)+ bzw. R+ ein regulärer Ausdruck und meint eine beliebige Wiederholung von R aber mindestens einmal. Diese fünfte Regel ist optional, da man R+ auch schon durch R(R)⇤ erhalten könnte. Bislang wirken diese Regeln leicht abstrakt. Folgende Beispiele zeigen, wie man sie verwendet : Beispiel. a) Mit ⌃ = {a, b, c} ist R = (a)⇤ bc(a)⇤ ( verkürzt : R = a⇤ bca⇤ ) ein regulärer Ausdruck. Der Ausdruck als Bauplan sagt also, dass man damit Wörter erzeugen kann, die mit einer beliebigen Zahl von a beginnen ( auch kein a ist erlaubt ) , dann aber auf jeden Fall ein b gefolgt von einem c enthalten und zum Schluss des Worts wieder beliebige viele a besitzen. b) Bei gleichem Alphabet ist auch R = a (bc | cb) a ein regulärer Ausdruck. Allerdings gibt es nur zwei passende Wörter, nämlich abca und acba. c) Auch R = (ac)⇤ c⇤ (a | b) ist ein regulärer Ausdruck und in der zugehörigen Sprache L(R) sind zahlreiche Wörter enthalten. Es ist L(R) = {a, b, acca, acacacb, acccca, . . .} Bei der Angabe von regulären Ausdrücken möchte man einen möglichst einfachen Term erhalten. Viele Klammern verdeutlichen zwar, wie etwas gemeint ist, aber machen einen Term auch schwerer lesbar. Daher legt man fest, dass bei einem regulären Ausdruck die Reihenfolge Iteration vor Konkatenation vor Auswahl kommt3 . Daher bedeutet ab⇤ eben nicht die Wiederholung von ab sondern nur ein a gefolgt von einer Wiederholung von b . War es anders gemeint, so hätte man (ab)⇤ schreiben müssen. Umwandlung : Regulärer Ausdruck in einen Automaten Jetzt, da wir eine dritte Methode zum Erzeugen von Sprachen gefunden haben, stellt sich die Frage, wie sich die regulären Sprachen im Vergleich zu den Automaten oder den Grammatiken verhalten. Die ersten zwei Methoden hatten sich ja insofern als gleichwertig erwiesen als man jeden Automaten in eine Grammatik und umgekehrt umwandeln konnte. Wir werden versuchen zu jedem regulären Ausdruck einen endlichen Automaten zu finden. Da ja die regulären Ausdrücke rekursive aufeinander aufbauen, kann man auch beim zu entwerfenden Automaten versuchen erst einzelne Teile anzugehen und dann diese miteinander zu verbinden. Betrachten wir zunächst die Grundstrukturen : 3 Das erinnert ein wenig an die Rechenfolge Potenz vor Punkt vor Strich und daher gibt es auch Informatikbücher, die a⇤ , a · b und a + b verwenden. Dann wäre (a + b)⇤ · c gleichbedeutend mit (a | b)⇤ c. 44 3 Sprachen a) Jeder einzelne Buchstabe a des gegebenen Alphabets ⌃ war ein regulärer Ausdruck. Einen passenden Automaten zu finden ist ein Kinderspiel : a Abbildung 3.6: Einzelner Buchstabe b) Das Aneinanderhängen mehrerer Buchstaben ist ähnlich leicht, aber wie gehen wir vor, wenn zwei beliebige reguläre Ausdrücke R1 , R2 aneinandergehängt werden sollen? Nehmen wir an, dass beide Ausdrücke einzeln schon durch Automaten dargestellt wurden. Das Bild zeigt sie als Kasten mit Startzustand und Endzustand und weiteren Zuständen und Details, die durch ... abgekürzt wurden. Möchte man die Ausdrücke aneinanderhängen, so geht es darum die Kästen miteinander zu verbinden. Offenbar müssen wir lediglich den Endzustand von R1 zu einem gewöhnlichen Zustand umformen und von dort aus einen Übergang zum Startzustand von R2 hinzufügen. Bei diesem neuen Übergang im Gesamtautomat bietet sich das leere Zeichen "an. R1 Aneinanderhängen ( Konkatenation ) ... R1 R2 R2 ... ... ... Abbildung 3.7: Aneinanderhängen zweier Ausdrücke c) Sind zwei Ausdrücke R, S schon in Automaten umgewandelt, so können wir die Auswahl R | S einfach dadurch erzielen, dass wir vom Startzustand aus zwei "-Übergänge einbauen. Wir wandeln die Endzustände in gewöhnliche Zustände und ergänzen zwei Übergänge zu einem gemeinsamen Endzustand. Dadurch ist gewährleistet, dass das Ergebnis wieder eine Art Kasten mit genau einem Ein- und einem Ausgang ist. Auswahl R1 R2 R1 ... R2 ... ... ... Abbildung 3.8: Auswahl d) Die Iteration R⇤ eines beliebigen Ausdrucks ist einfach dadurch zu erreichen, dass vom bisherigen Endzustand ein Übergang eingebaut wird, der zurück zum Beginn des Automaten zeigt. Um auch die nullfache Wiederholung ( auch bei R⇤ möglich ) zu erreichen, gibt es noch einen Übergang direkt vom Beginn des Automaten zum Endzustand. Wieder erweisen sich "-Übergänge als hilfreiches Mittel. Lässt man diesen zweiten Übergang weg, so hat man einen Automaten für R+ gefunden. 45 3 Sprachen Iteration R* R R ... ... Abbildung 3.9: Iteration Beispiel. Gegeben sei der reguläre Ausdruck R = a+ (a | b)⇤ b(ab)⇤ . Die Abbildung zeigt zunächst Automaten für die einzelnen Bestandteile a+ , (a | b)⇤ , b und (ab)⇤ . Nach den oben besprochenen Regeln a+ (a|b)* a,b a b (ab)* a b b Abbildung 3.10: Einzelne Bestandteile des regulären Ausdrucks können wir diese miteinander verbinden ( siehe die roten, ergänzten Übergänge ) und erhalten einen endlichen Automaten, der exakt die Wörter des regulären Ausdrucks als Sprache besitzt. a+(a|b)*b(ab)* a a,b b a b Abbildung 3.11: Fertiger Automat Eine schnellere Methode zur Umwandlung Die eben beschriebene Methode basierte darauf einzelne Teile des Ausdrucks in Automaten umzuwandeln und dann diese einzelnen Teile geschickt zu verbinden. Diese Umwandlung hin zu einem Automaten lässt sich anders erreichen, indem man eine Folge von Automaten konstruiert, die Schritt für Schritt den regulären Ausdruck auflösen. Dabei startet man mit einem Startzustand und benutzt den kompletten Ausdruck R als Übergangsbeschriftung zum Endzustand. Start R Abbildung 3.12: Erster Schritt 46 3 Sprachen Dieser einzige Übergang wird jetzt nach den folgenden, grafisch dargestellten Regeln unterteilt : R R|S ersetzt durch: S optional RS ersetzt durch: R* ersetzt durch: R S R R+ R ersetzt durch : R Abbildung 3.13: Vorgehensweise Das gute an diesen Regeln ist, dass man sie auch umgekehrt nutzen kann, um von einem Automaten zu einem regulären Ausdruck zu kommen. Die folgende Abbildung zeigt, wie man aus einem Automaten die inneren Zustände entfernen kann, so dass nach und nach nur noch ein Start- und ein Endzustand übrig bleibt. Direkt zu Beginn wird ein weiterer Zustand durch "mit dem Startzustand verbunden, damit man später auch an dieser Stelle die Regeln benutzen kann. X si Y A B Z C sj D X | AY*B ersetzt durch: sk si Z | DY*C AY*C DY*B sk Zustand sj wird ersetzt Abbildung 3.14: Umwandlung Automat zu RegEx Beispiel. Gegeben sei der abgebildete Automat mit ⌃ = {a, b}, der alle Wörter erkennt, die als drittletztes Zeichen ein a besitzen. Die darunter gezeichneten Darstellungen zeigen, wie man schrittweise zum passenden regulären Ausdruck kommt. 47 3 Sprachen a,b gegeben Start a a,b a,b a a|b a|b a (a|b)(a|b) a,b Start a,b Start a|b Start a(a|b)(a|b) a,b Start Start Ausdruck : a(a|b)(a|b) (a|b)*a(a|b)(a|b) (a|b)*a(a|b)(a|b) Abbildung 3.15: Beispiel Automat zu RegEx 3.5 Arithmetische Ausdrücke Wann immer wir ein Programm in Processing schreiben, wird der Quelltext durchsucht, ob auch alle Zeilen so aufgebaut sind, wie es die Programmiersprache Java verlangt. Die gesamte Überprüfung eines Quelltexts hier nachzuvollziehen, führte zu weit. Interessierte finden weitere Informationen unter den Stichworten Lexer und Parser. Schrauben wir unsere Ansprüche daher etwas zurück und betrachten nur einen kleinen Ausschnitt einer Programmiersprache. Bei nahezu allen Programmiersprachen kann man mathematische Terme mit konkreten Zahlen aufstellen, die dann berechnet werden. So werden 4.2 ⇤ 3.1 + 9.22 oder (1.4 + 3.14)/2 problemlos ausgerechnet, während bei (1.3 + 2.3 ⇤ 1.4 zu Recht maniert wird, dass hier die schließende Klammer fehlt. Abbildung 3.16: Als falsch erkannter Term Um solche mathematischen Rechenterme ( auch arithmetische Ausdrücke genannt ) entweder zu akzeptieren oder eben als fehlerhaft abzulehnen, scheint es nahezulegen, eine der drei genannten Methoden ( Endliche Automaten, Reguläre Sprachen, Reguläre Ausdrücke ) zu verwenden um damit vorzugeben, wie ein korrekter Term auszusehen hat. Jedoch scheitern diese drei Konzepte bereits an einer viel simpleren Stelle : 48 3 Sprachen Klammern erkennen Jeder Schüler lernt, dass er für jede öffnende Klammer auch irgendwann eine schließende Klammer schreiben muss. Der besseren Lesbarkeit wegen, kürzen wir die öffnende und die schließende Klammer mit a = ( und b =) ab. Bleiben wir bei diesen beiden Zeichen, d.h. ⌃ = {a, b} so können wir sehr einfache Fälle von Klammerungen betrachten. Es sei L = {an bn | n > 0}, d.h. in L liegen alle Wörter, die zu Beginn eine gewisse Zahl an öffnenden Klammern und dann die gleiche Anzahl an schließenden Klammern besitzen. Geht man auf die Suche nach einem Automaten, der die Wörter in L erkennt, so kann man Schritt für Schritt anfangen. Start Start a a Start a b b a a b b b L={ , ab } a b b L={ , ab, a2b2 } b b L={ , ab, a2b2, a3b3 } Abbildung 3.17: Erkennen von an bb Die Abbildung zeigt aber, dass die Automaten bei jedem weiteren Wort von L immer mehr Zustände benötigen. Dann ließen sich mit diesem Aufbau an Automaten aber gar nicht alle Wörter in L mit einem endlichen Automaten finden. Andererseits : Vielleicht haben wir unseren Automaten nur zu naiv aufgebaut und könnten irgendwie doch einen anderen Automaten mit der Sprache L finden? Der folgende Satz und sein Beweis zeigen, dass dies schlicht unmöglich ist. Satz. Die Menge L = {an bn | n > 0} ist keine reguläre Sprache, d.h. es gibt keine regulären Grammatik, keinen endlichen Automaten und auch keinen regulären Ausdruck, der diese Sprache besitzt. Beweis. Der Beweis der Aussage erfolgt als sogenannter Widerspruchsbeweis. Wir nehmen an, dass es einen endlichen Automaten zur Sprache L gibt und erzielen daraus einen logischen Widerspruch. Wenn alle unsere Schritte korrekt waren, dann kann der Fehler letztlich nur in der falschen Annahme liegen. Gehen wir also als Annahme davon aus, dass es doch einen endlichen Automaten gibt, der exakt L als Sprache besitzt, d.h. L(A) = L. Die Anzahl der Zustände in diesem Automaten ist eine endliche Zahl, die wir mit n abkürzen. Jetzt wählen unter all den Wörtern der Sprache ein Wort ak bk mit k > n. Der Automat akzeptiert das Wort und liest dabei zunächst die ersten k mal den Buchstaben a . Da es aber nur n Zustände gibt und er mehr als n Zeichen verarbeitet, kommt es dabei zwangsläufig zu der Situation, dass er einen Zustand zweimal erreicht4 . Somit besitzt der Automat bereits in der ersten Hälfte des Worts ( bei den a’s ) eine Schleife. Damit haben wir dann aber die Möglichkeit, die Schleife ak bk Abbildung 3.18: Schleife beim Lesen des Worts Schleife ein oder mehrere Male zu durchlaufen und dennoch ein akzeptiertes Wort zu erhalten, in dem die Anzahl der a’s und b0 s nicht gleich ist. Ein solches Wort liegt aber nicht in L, d.h. L(A) 6= L. Dies ist ein Widerspruch zu unserer Annahme L(A) = L . Da alle Schritte sonst korrekt waren, liegt der Fehler in der Annahme, d.h. es gibt keinen solchen Automaten. Dass es keine reguläre Grammatik oder 4 Die allgemeine Idee dahinter heißt mitunter auch Schubfachprinzip ( pigeon hole principle ). Hat man n Fächer und soll in diese aber mehr als n Gegenstände legen, so gibt es mindestens ein Fach, das doppelt belegt ist. 49 3 Sprachen regulären Ausdruck gibt, folgt natürlich sofort, denn im letzten Abschnitt hatten wir ja alle drei als gleichwertige Konzepte benannt, die man auch ineinander umwandeln kann. Damit fängt das Auslesen von arithmetischen Ausdrücken schon schlecht an, denn wenn schon einfache Klammerungen nicht erkannt werden, wie sollen dann erst noch komplexere Terme akzeptiert werden? Allerdings zeigte der Satz ja nur, dass es keine reguläre Grammatik gibt. Es bleibt noch das kleine Schlupfloch, eine nicht reguläre Grammatik zu verwenden. Ein Blick zurück in den Abschnitt 3.2 zeigt, dass es ja noch die kontextfreien Sprachen gibt. Und genau mit solchen Sprachen lassen sich z.B. schon die Klammerungen erkennen. Beispiel. Die kontextfreie Grammatik G = (T, N, P, S) mit der Produktion S ! aSb | " besitzt als Sprache die Menge L(G) = {an bn | n > 0} . Genau diese Sprache konnten reguläre Sprachen wie gezeigt nicht erzeugen. Der allgemeinere Ansatz einer kontextfreien Grammatik trägt hier weiter. Durch solche Grammatiken lassen sich dann arithmetische Ausdrücke gut erkennen. Betrachten wir die Grammatik G = (T, N, P, S) mit T = {0, . . . , 9, ( , ) , +, , ⇤, /}, N = {S, Zif f er, Zahl} und den Produktionen : S Zahl ! Zahl | (S) | S + S | S ! ! Zif f er S | S ⇤ S | S/S Zif f er | Zif f erZahl 0|1|2|3|4|5|6|7|8|9 Hier erhalten wir schon einen ersten Eindruck, wie man ganze Zahlen, die vier Grundrechenarten und eine Beklammerung erreichen kann5 .Damit lassen sich dann schon konkrete Ausdrücke ableiten : S ) S ⇤ S ) S ⇤ (S) ) S ⇤ (S ) ... ) 13 ⇤ (105 5 20/4) S) ) S ⇤ (S Vorzeichen und Kommazahlen hinzuzufügen ist eine gute Übung. 50 S/S) ) Zahl ⇤ (Zahl Zahl/Zahl) 4 Aufbau eines Rechners In der modernen, westlichen Welt ist der Computer aus dem Berufs- und Privatleben kaum mehr wegzudenken. Ungefähre Schätzungen weisen auf ca. 1,5 Milliarden Computer und weitere Milliarden an Handys hin und belegen damit eindeutig, dass der Computer ein Teil des Alltags geworden ist. Allerdings folgt aus dem täglichen Benutzen der Rechner noch lange nicht ein umfassendes Verständnis wie genau ein Computer funktioniert und damit passt der Computer in eine lange Reihe technischer Geräte, deren genaue Funktionsweise im Alltag unbekannt bleibt (Auto, Mikrowelle, Fernseher, GPSNavi, ... ). In diesem Kapitel wird der Versuch unternommen, einige Details in der Arbeitsweise eines Computers besser zu verstehen. 4.1 Der Weg zum modernen Rechner Während man beim motorisierten Auto genau sagen kann, dass Carl Benz 1879 am Silvesterabend zum ersten Mal einen lauffähigen Motor fertigstellte und dann sieben Jahre später 1886 das zugehörige Auto samt Motor patentieren ließ, bleibt es bei der Entwicklung des Computers unklarer. Wer hat eigentlich wann und wo den ersten Computer erfunden? Der Begriff Computer meint - wörtlich übersetzt - einen Rechner und war noch im 17. Jahrhundert eine Bezeichnung für einen Menschen, dessen Aufgabe darin bestand gegen Geld nötige Rechnungen durchzuführen. Nun gut, das war wohl nicht mit Erfindung des Computers gemeint. Ein großer Schritt zum modernen Computer waren die Ideen und Überlegungen des englischen Akademikers Charles Babbage, der 1822 auf dem Papier erste Grundzüge einer Maschine entwickelte, die mit mechanischen Mitteln grundlegende Rechnungen durchführen sollte. Zeit seines Lebens entwickelte und verfeinerte er den Aufbau der Maschine immer weiter aber leider gelang es ihm in seinem Leben nicht diese Maschine vollständig zu bauen. Ihm zu Ehren wurde seine Maschine aber 2002 auf der Grundlage seiner Konstruktion aufgebaut. Sie ist im Science Museum in London ausgestellt und besteht aus 8000 Teilen, wiegt 5 Tonnen und hat eine Länge von über drei Meter1 . 1 Eine kleinere Version dieser Maschine wurde bereits 1991 fertig und im Science Museum in London aufgebaut 51 4 Aufbau eines Rechners Abbildung 4.1: Die Difference Engine von Charles Babbage Trotz dieses beeindruckenden Projekts von Babbage fehlt dieser Maschine noch etwas zum Computer. Sie ist rein mechanisch, zwar ein Glanzstück der Mechanik aber eben vollständig ohne elektrischen Strom. Würden wir kurbeln wollen, um unsere Computer zu verwenden? Der in der Bevölkerung nahezu unbekannte Entwickler des ersten Computers mit elektrischen Bauteilen war der Bauingenieur Konrad Zuse, der im Wohnzimmer seiner Eltern 1938 in Berlin Kreuzberg den Computer Z1 aufbaute. Abbildung 4.2: Zuses Z1 im Wohnzimmer der Eltern ( Foto : Deutsches Museum ) Im zweiten Weltkrieg entwickelten englische Ingenieure einen Computer, dessen Aufgabe darin bestand verschlüsselte Funksprüche zu entschlüsseln. Federführend war der Mathematik Max Newman und im Bereich der theoretischen Grundlagen der Mathematiker Alan Turing. Ihr Computer hieß Colossus und konnte bei den einkommenden Funksprüchen schon 5000 Zeichen pro Sekunde verarbeiten ( sofern sie auf vorgestanzten Lochkarten bereitlagen ). Damit gelang den Engländern innerhalb von Stunden die täglich wechselnden Grundeinstellungen beim Verschlüsselungsgerät Enigma zu finden und damit dann den Rest des Tages Funksprüche problemlos mitzulesen. Allerdings war der Colossus noch nicht frei programmierbar, d.h. er war eben nur für das Entschlüsseln gebaut und konnte - im Gegensatz zu heutigen Rechnern - nicht beliebige Programme auführen. 52 4 Aufbau eines Rechners Moderne Rechner Was findet sich denn nun in einem modernen Computer? Ein Blick auf die Abbildung zeigt verschiedene Teile, die alle zusammen auf dem sogenannten Mainboard angeordnet sind. Entscheidend ist der sogenannte Prozessor ( auch CPU genannt ), der auf den CPU-Sockel gesetzt wird. Ebenfalls sehr wichtig sind die blau gekennzeichneten RAM-Steckplätze. Dort werden Steckmodule eingesetzt, die dem Computer dann eine große Menge an möglichen Speicherplätzen bieten. Im vorderen Bereich finden wir die als Peripherie-Anschlüsse gekennzeichneten möglichen Steckverbindungen zu externer Hardware ( Tastatur, Maus, Bildschirm, Drucker, ...). Damit alle Teile miteinander verbunden sind und Daten austauschen können, gibt es ein Bus-System. Mit Hilfe von vorhandenen Leitungen kann dabei der Prozessor Daten aus dem Speicher holen oder dorthin bringen. Andere Leitungssysteme (=Bus-Systeme ) bringen z.B. Daten rasch zur Grafikkarte ( Northbridge ) oder Daten von / zur angeschlossenen Peripherie ( Southbridge ). Abbildung 4.3: Aktuelles Mainboard In typischen Werbeprospekten werden nur manche dieser Teile erwähnt ( Prozessor und Speicher ) und der Rest des Mainboards oft vernachlässigt. 4.2 Die von Neumann-Architektur Der eben beschriebene moderne Computeraufbau hat seinen Ursprung in den theoretischen Ideen des ungarischen Mathematikers John von Neumann. Dieser beschrieb im Jahr 1945 einen grundlegenden, theoretischen Aufbau, der ihm für einen frei programmierbaren Computer geeignet erschien. Man spricht von der von Neumann-Architektur bzw. dem von Neumann-Rechner und meint damit einen Urahn aller Computer im Geiste. Der von Neumann-Rechner ist ein universeller Rechner, der verschiedene Aufgaben übernehmen kann und dazu bestimmte Programme als von außen zugeführte Bearbeitungsschritte benötigt. Er lässt sich in folgende Funktionseinheiten unterteilen: a) Ein Steuerwerk, das den Ablauf koordiniert. 53 4 Aufbau eines Rechners b) Ein Rechenwerk, das die nötigen Berechnungen durchführt. c) Ein Speicher, der aus einzelnen Speicherzellen besteht. In diesen Zellen lassen sich ( in binärer Form ) Zahlen ablegen. Der Speicher hat für jede Zelle eine fortlaufende Nummerierung. Mit Hilfe der Nummer, kann man auf den Speicher zugreifen, d.h. ihm Zahlen entnehmen oder im Speicher Zahlen ablegen. d) Ein Eingabewerk, das Eingaben erlaubt und ein Ausgabewerk, das Ausgaben möglich macht. Beide fassen wir als Peripherie ( d.h. irgendwelche angeschlossenen Geräte ) zusammen. Diese Einheiten sind durch Leitungen ( sogenanntes Bussystem ) verbunden, so dass Daten ausgetauscht werden können. Speicher 1 2 Steuerwerk 3 4 Bus 5 6 7 Rechenwerk 8 9 10 11 Ein-&Ausgabe (Peripherie) Abbildung 4.4: Aufbau von Neumann-Rechner Eine Besonderheit beim von Neumann’schen Aufbau liegt darin, dass das ablaufende Programm und die beim Ablauf benötigten Daten alle im gleichen Speicher liegen. Greift man also wahllos den Wert einer Speicherstelle heraus, so lässt sich an Hand der Zahl selbst gar nicht entscheiden, ob man nun mitten in einem Programm oder bei den Daten gelandet ist. Wie arbeitet der von Neumann-Rechner? Soll in unserem Rechner ein Programm ablaufen, so muss dieses Programm zunächst in den Speicher gelegt werden. Man kann das Programm dazu in den Speicher laden ( von einem Hilfsgerät wie Festplatte, CD, ... ) oder bei kleinen Programmen direkt eingeben. Beachte, dass mit Programm hier keinerlei Quelltext gemeint ist, sondern einfach eine Abfolge von Zahlen, die nacheinander im Speicher liegen. Die Abbildung zeigt einen Ausschnitt eines Windows-Programms namens HelloWorld.exe, das nur den Text HelloWorld ausgibt. Um das Programm im Speicher dann korrekt zu starten, muss der Rechner den Start des Programms natürlich noch kennen. Dazu gibt es eine einzelne Speicherzelle namens Program Counter (PC 54 4 Aufbau eines Rechners Abbildung 4.5: Ausschnitt des Programms HelloWorld.exe oder auch Befehlszähler), der jeweils auf die Speicherzelle weist, in der der nächste Befehl steht. Zu Beginn zeigt der Befehlszähler auf den Anfang des Programms. Wird das Programm gestartet, so beginnt jetzt ein sich stetig wiederholender Prozess, der immer die gleichen Schritte ausführt, bis das Programm beendet ist. a) Der Inhalt der Speicherzelle, auf die der Befehlszähler weist, wird ausgelesen und in eine eigene Speicherzelle namens Befehlsregister ( Instruction Register oder IR ) geschrieben. Dieser erste Schritt heißt Fetch. b) Der Befehlszähler wird um 1 erhöht, so dass er auf die nächste Speicherzelle und damit die nächste Stelle im Programm zeigt. Man spricht von Increment-Schritt. c) Im Befehlsregister steht zwar jetzt eine Zahl, die ausgelesen wurde, aber was bedeutet diese Zahl für den Prozessor? Dies wird nur klar, wenn die Zahl so ausgelesen wird, dass für den Prozessor klar ist, was zu tun ist. Dieser Schritt heißt Decode und macht klar, welcher Befehl abgearbeitet werden muss. Dabei meint Befehl etwas, was der Hersteller des Steuerwerks schon hardwaremäßig fest eingebaut hat. Welche Befehle ein Prozessor verarbeiten kann, hängt also davon ab, wie der Prozessor aufgebaut ist. Ein konkretes Beispiel wird später klarer machen, was hier gemeint ist. d) Manche Befehle benötigen noch weitere Angaben. So gibt es etwa einen Befehl zum Auslesen einer Speicherzelle aber dann muss der Prozessor eben auch wissen, welche Zelle gemeint ist. Solche zusätzlichen Argumente sind auch nur Zahlen, die nach und nach aus dem Speicher ausgelesen werden. Dieser vierte Schritt heißt Fetch Operands. e) Ist dann klar, welcher Befehl gemeint ist und sind auch alle nötigen Zusatzdaten eingelesen, so kann der Befehl vom Prozessor umgesetzt werden. Es kommt zum Execute-Schritt. Wird beim Abarbeiten ein Ergebnis irgendwo in den Speicher geschrieben ( Store ) , so wird in manchen Darstellungen dies als eigener Schritt gesehen. Wir sehen ihn hier nur als Spezialfall vom Execute. Nach diesem fünften Schritt wiederholt sich der Ablauf, d.h. die Zelle, auf die der Befehlszähler zeigt, wird wieder ausgelesen, dekodiert, usw. Das Programm endet erst dann, wenn im Programm ein StopBefehl abgearbeitet wird. 4.3 Ein Modellcomputer Nach diesen allgemeinen Überlegungen zum Aufbau und zum Ablauf bei einem von Neumann-Rechner wird es Zeit ein konkretes Modell eines solchen Rechners zu betrachten. Dazu müssen wir noch ein paar zusätzliche Details ergänzen. Bei unserem Modellrechner gibt es besondere Speicherstellen, die Register genannt werden. In ihnen werden besondere Werte gespeichert, die der Rechner beim Ausführen des Programms benötigt. Ein solches Register wurde oben schon erwähnt, das Befehlsregister. In diesem Register lässt sich nach Bedarf ablesen, in welcher Speicherstelle der nächste Programmschritt zu finden ist. 55 4 Aufbau eines Rechners START FETCH S OP EXEC UT E REMENT INC DE CO DE FET CH Abbildung 4.6: Zyklus des Rechners Ein weiteres wichtiges Register stellt der sogenannte Akkumulator dar. Es ist eine Art Zwischenspeicher für das Rechenwerk. Werden also Zahlen addiert und subtrahiert, so erfolgen diese Berechnungen im Rechenwerk mit dem im Akku gespeicherten Wert. Unser Rechner ist in dieser Hinsicht sehr primitiv, da er nur einen einzigen solchen Speicher aufweist. Moderne Computer haben statt eines Akkumulators mehrere Speicher im Rechenwerk, die man zusammenfassend auch Datenregister nennt. Erste Befehle rund um den Akkumulator Damit der Akkumulator überhaupt einen Sinn hat, muss es möglich sein, seinen Inhalt zu einer Speicherzelle zu bringen. Auch der umgekehrte Weg, d.h. den Wert einer Speicherzelle in den Akkumulator auslesen ist nötig. Ferner existieren stets Befehle, die es erlauben mit dem Akkuinhalt Berechnungen anzustellen. Eine erste Übersicht zeigt die folgende Abbildung. Dabei ist zu beachten, dass die Buchstabenkürzel keine Befehle wie in einer Programmiersprache sind, sondern lediglich eine bessere Veranschaulichung von Zahlen sind, die der Prozessor als Befehl interpretiert ( Decode-Phase, s.o. ) : LDA x Lädt den Inhalt von Speicher x in den Akku. STA x Speichert den Inhalt des Akkus im Speicher x. ADD x Addiert den Inhalt von Speicher x zum Akku. SUB x Subtrahiert den Inhalt von Speicher x vom Akku. load accu store accu add to accu subtract from accu INC Vergrößert den Akkuinhalt um 1. DEC Vermindert den Akkuinhalt um 1. NEG Ändert das Vorzeichen des Inhalts im Akku. increment accu decrement accu negate accu Abbildung 4.7: Erste Befehle Sprungbefehle Üblicherweise werden die Befehle in der Reihenfolge abgearbeitet, in der sie im Speicher liegen. Möchte man davon abweichen, so kann man Sprungbefehle verwenden. Der direkte Sprung erfolgt mit “JMP x” ( jump ) wobei x die nötige Nummer des Speichers ist, in der der nächste Befehl steht. Kniffliger sind die bedingten Sprungbefehle, d.h. es kommt zu einem Sprung im Programm nur wenn 56 4 Aufbau eines Rechners eine gewisse Bedingung erfüllt ist. Dazu wird der Wert des Akkus mit der Bedingung verglichen und falls die Bedingung erfüllt ist, wird zu einem anderen Teil des Programms gesprungen. Sollte die Bedingung nicht erfüllt sein, wird einfach mit dem nächsten, folgenden Befehl fortgefahren. Die Abbildung zeigt die möglichen Sprungbefehle : JMP x Springt zum Speicher x und setzt das Programm dort fort. JMS x Springt zu x, falls der Akkuinhalt < 0 ist. JPL x Springt zu x, falls der Akkuinhalt > 0 ist. JZE x Springt zu x, falls der Akkuinhalt = 0 ist. JNM x Springt zu x, falls der Akkuinhalt nicht < 0 ist. ( also Akku 0 ) Springt zu x, falls der Akkuinhalt nicht > 0 ist. ( also Akku 0 ) Springt zu x, falls der Akkuinhalt nicht = 0 ist. ( also Akku 0 ) jump jump if minus jump if plus jump if zero jump if not minus JNP x jump if not plus JNZ x jump if not zero Abbildung 4.8: Sprungbefehle Restliche Befehle Abgerundet wird die Liste an Befehlen durch Möglichkeiten zur Ein- und Ausgabe sowie zum Festsetzen von Werten, die bei Programmstart schon bereitgestellt sind. Schließlich gibt es noch ein sehr nützliches Kommando namens “END”, damit das Programm eben nicht bis in alle Ewigkeit läuft. INM x Liest eine Zahl ein und speichert sie beim Speicher x. OUT x Gibt den Inhalt des Speichers x aus. DEF x Ist kein Befehl, sondern besagt lediglich, dass im Speicher hier die Zahl x vorliegt. input to memory output define END end Beendet das laufende Programm. Abbildung 4.9: Restliche Befehle Ein erstes Beispiel Betrachte das folgende Programm : 0 JMP 4 1 DEF 10 2 DEF 20 3 DEF 0 4 LDA 1 5 ADD 2 6 STA 3 7 OUT 3 8 END Das Programm beginnt mit einem Sprung zur Zeile 4. Dadurch werden die Speicherzellen 1,2,3 übersprungen. In ihnen stehen keine Anweisungen, sondern ihre Inhalte sind für die kommenden Berechnungen reserviert. Bei Programmstart enthält Zelle 1 die Zahl 10, Zelle 2 die 20 und Zelle 3 eine Null. 57 4 Aufbau eines Rechners Das eigentliche Programm beginnt also ab Zeile 4 und startet damit, dass der Wert von Zelle 1 ( d.h. die 10 ) in den Akku kopiert wird ( LDA). Anschließend wird der Wert der 2. Zelle zum Akku addiert. Beachte hier den Unterschied, dass nicht 2 addiert wird ( 10 + 2 = 12 ), sondern der Inhalt von Zelle 2 ( also 10 + 20 = 30 ). Das Ergebnis wird in Zelle 3 gespeichert ( STA ) und ausgegeben ( OUT) . Das anschließende END stoppt die Ausführung. Allgemein addiert also das Programm die Zahlen in den Zellen 1 und 2 und speichert das Ergebnis in Zelle 3. Man kann sich den Ablauf des Programms auch dadurch klarmachen, dass man die momentanen Werte des Akkus und der verschiedenen Speicherzellen protokolliert und dadurch den Überblick behält, was gerade wo steht. Akku LDA 1 ADD 2 STA 3 – 10 30 Speicher 1 Speicher 2 Speicher 3 10 20 0 30 Ausgabe OUT 3 Abbildung 4.10: Schrittweises Vorgehen 4.4 Von der Hochsprache zur Maschinensprache Direktes Programmieren in der Maschinensprache, d.h. direktes Eingeben einer langen Zahlenkette, die das Programm bildet, ist ein aussichtsloses Unterfangen. Zunächst wären ja mal alle vorhandenen Befehle des Steuerwerks auswendig zu lernen und selbst wenn das noch gelingen sollte, so führt ein kleinster Tippfehler bei einer Zahl zu einem Befehl, den man nie im Programm haben wollte. Daher gibt es fast niemanden, der Programme direkt in Maschinensprache schreibt. Eine erste Erleichterung ist da schon die Idee, die auch im letzten Abschnitt benutzt wurde, die Befehle mit Hilfe kleiner Buchstabenkürzel ( LDA, STA, ... ) abzukürzen. Dadurch lassen sich dann einfache Programme schon recht gut lesen und nachvollziehen. Man spricht von einer Assembler-Sprache. Dennoch gibt es auch hierbei noch einige Nachteile : • Die Befehle in der Assemblersprache beziehen sich immer auf einen bestimmten Prozessortyp. Heißt es bei einem Prozessor LDA 1 um einen Wert vom Speicher 1 in den Akku zu laden, so besitz ein anderer Prozessor den Befehl MOVE 1,d0 und lädt damit den Speicherwert 1 in das Datenregister d0 ( anderer Name für Akku bei diesem Prozessor ). Daher muss man den vorhandenen Prozessor und seine Befehle immer erst genau studieren. Ein fertiges Assemblerprogramm passt somit streng genommen nur für einen einzigen Prozessor. • Bisher haben wir kleine Assemblerprogramme ( vgl. Kapitel zum Modellcomputer ) mit einem Texteditor geschrieben. Haben wir eine Zeile vergessen, so führte das nachträgliche Einfügen dazu, dass alle Zeilennummern umständlich von Hand geändert werden mussten. • Schon relativ leichte Abläufe erfordern in Assembler oft eine Vielzahl an Zeilen. Unser Beispiel in 4.3 addierte zwei Zahlen und benötigte dafür immerhin schon 8 Zeilen. In unseren bisherigen Programmiersprachen war dies mit einer Zeile ( etwa z = x + y; ) zu erreichen. Nicht unerwähnt soll aber bleiben, dass man mit Assembler die schnellstmöglichen Programme schreiben kann. Die Befehle sind eben alle direkte Hardwarebefehle. Gerade bei älteren Prozessoren, die im 58 4 Aufbau eines Rechners Vergleich zu heute eher langsam laufen, war es üblich Assembler zu verwenden, um eine schnelle Ausführung von Programmen zu erreichen. Bis 1990 wurden daher nahezu sämtliche Spiele in Assembler geschrieben. Die höheren Programmiersprachen Mit Beginn der 1950er Jahre begannen die ersten Versuche von der direkten Programmierung in Assembler wegzukommen und in höheren Sprachen zu schreiben. Von diesen höheren Sprachen gibt es große Anzahl aber allen gemeinsam ist die Eigenschaft, dass in ihnen Befehle auftreten, die ein Prozessor eben nicht mehr direkt umsetzen kann. Wie können solche Programme dann noch überhaupt auf einem Rechner laufen? Schreiben wir ein simples Programm in Processing, so müssen wir dazu die vorgegebenen Strukturen der Programmiersprache Java verwenden. Diese Sprache fiel freilich nicht vom Himmel, sondern wurde - anders als natürliche Sprachen - in den Jahren 1991/1992 von einem Team der Firma Sun erfunden. Nehmen wir als Beispiel die uns bereits bekannte for-Schleife : for ( int i=1; i<=20; i++) { ...// Code der Schleife } Abbildung 4.11: Beispiel einer for-Schleife Soll diese Schleife jemals ausgeführt werden, so muss letztlich ein Programm in Maschinensprache erstellt werden, dass genau den vorgesehen Ablauf der Schleife durchführt. Diese Übersetzung von einem Java-Quelltext hin zu einem fertigen Maschinenspracheprogramm wird von einem sogenannten Compiler geleistet. Dies ist also selbst eine von jemandem geschriebene Software, deren Aufgabe es ist, aus einem korrekten Quelltext einer höheren Programmiersprache ein Maschinenspracheprogramm zu erzeugen. höhere Programmiersprache for ( int i=1; i<=10; i++){ Maschinensprache A1 44 C2 7A 39 ... Abbildung 4.12: Arbeit eines Compilers Das Beispiel der for-Schleife könnte z.B. folgendermaßen in Maschinensprache umgewandelt werden: 59 4 Aufbau eines Rechners 00 JMP 3 01 DEF 1 02 DEF 10 //Startwert 1 //Endwert 10 03 04 05 06 // Bedingung Schleife prüfen // Schleife fertig? 50 51 52 53 LDA SUB JPL ... ... LDA INC STA JMP 1 2 54 // Code der Schleife 1 1 4 // Zelle 1 erhöhen // und nächster Durchgang 54 ... Abbildung 4.13: Schleife in Maschinensprache Der Variablen i in der Java-Version entspricht hier die Speicherzelle 01 und der letzte Wert der Schleife wird in der Zelle 02 gespeichert. Subtrahiert man diese Werte voneinander ( z.B. zu Beginn 1 10 = 9 ) , so erreicht man zunächst einen negativen Wert. Daher findet kein Sprung bei JPL 54 statt, so dass die Schleife ein erstes Mal durchlaufen wird. Die Zeilen 50-53 erhöhen dann die Zahl in Zelle 01 ( unser i ) und springen zurück zur Zeile 4, d.h. zur Überprüfung, ob die Schleife schon beendet werden soll oder nicht. Erst dann, wenn die Zelle 01 einen Wert von 11 hat, ergibt die Subtraktion einen positiven Wert. Der Sprung mit JPL hinter die Schleife findet statt und damit ist die Schleife beendet2 . Auch kompliziertere Teile der höheren Programmiersprache ( if-else, arrays, komplexe Datentypen, ... ) müssen letztlich vom Compiler immer erst in Maschinensprache übertragen werden. Da diese Übersetzungen ja auch nach einem gewissen Schema durchlaufen werden, kann es sein, dass der fertige Maschinencode nicht optimal ist, d.h. man hätte ihn in direkter Assemblerprogrammierung wohl kürzer und effektiver schreiben können. Somit sind kompilierte Programme eben nicht immer so schnell wie bei direkter Programmierung in Assembler. Bei einer Kompilierung ist das Endergebnis ein fertiges Programm in Maschinensprache. Wenn diese Datei gespeichert ist, dann benötigt man den Quelltext nicht mehr, um das Programm ablaufen zu lassen3 . Es gibt einen zweiten Ansatz, bei dem der Quelltext vorhanden bleibt, und erst beim Programmstart zur Laufzeit in Maschinensprache übersetzt wird. Bei dieser Variante des Übersetzens spricht man von einem Interpreter. Die bekanntesten Beispiele für dieses Vorgehen sind die Sprachen Python und JavaScript ( das trotz des Namens nichts mit Java zu tun hat! ). So handelt es sich bei Dateien mit der Endung .py nur um Textdateien, die dann erst durch den Aufruf python Name.py in Maschinensprache umgewandelt und gestartet werden. 2 Die wilden Sprünge wie hier mit JPL sind typisch für Maschinensprache, aber machen den Code gerade bei vielen Sprüngen unübersichtlich. Daher sind bei höheren Programmiersprachen Sprünge meist verpönt, obwohl nach wie vor fast alle diese Sprachen einen goTo Befehl zum Springen haben. 3 Die bekannten .exe-Dateien in Windows lassen eben keinen Rückschluss mehr auf den Quelltext zu. 60 4 Aufbau eines Rechners ( more gibt den Inhalt der Textdatei Test.py aus, während python den Interpreter startet.) Abbildung 4.14: Beispiel Python-Programm JavaScript ist ebenfalls eine Interpretersprache, die von vielen Browsern verwendet wird. Dazu hat jeder Browser eine sogenannte JavaScript-Engine, die fertige Text-Dateien mit der Endung .js bei Bedarf umwandeln und starten kann. 61 5 Kryptologie 5.1 Einige klassische Verfahren Wann immer Menschen miteinander schriftlich kommunizieren, gab es immer Nachrichten, deren Inhalt nur dem Schreiber und dem Empfänger bekannt sein sollten. Liesbesbriefe, Geheimnisse oder Schatzkarten sind ihrem Wesen nach eben nicht für den Rest der Welt bestimmt und wer kann schon sagen durch welche Hände ein Schriftstück geht bevor es den Empfänger erreicht? Daher ist es nicht verwunderlich, dass Techniken entwickelt wurden, die den Inhalt einer Nachricht verschlüsseln und damit den Blicken von neugierigen Mitlesenden entziehen. Eine Variante besteht darin, die wichtige Nachricht vor den Augen der Öffentlichkeit zu verstecken. Ob die bei Kindern beliebte Zaubertinte, das Schreiben mit Zitronensaft oder die im Schuhabsatz geschmuggelte Nachricht, im Kern geht es immer darum eine Art von Versteckspiel zu betreiben. Diese Techniken fasst man unter dem Begriff Steganographie zusammen1 . Beim Versenden von Nachrichten ( egal ob nun per Brief, Funk oder auf andere nichtschriftliche Art ) besteht nicht immer die Chance etwas zu verstecken. Ein Funker sendet nun einmal Funksignale, die von jeder anderen Funkstation aufgefangen werden können. Ein anderer Weg wird mit der hier betrachteten Kryptographie eingeschlagen. Man ist sich dort immer der Möglichkeit bewusst, dass eine Nachricht von Unbefugten abgefangen und mitgelesen wird und versucht ein System der Verschlüsselung zu wählen, so dass der Unbefugte mit der abgefangenen Nachricht nichts anfangen kann. Einige bekannte, historische Beispiele geben einen Einblick in die Verschlüsselungskunst. Steganographie ( Nachrichten verstecken ) Geheime Nachrichten Substitution ( Ersetzung ) Kryptographie ( Nachrichten verschlüsseln ) Code (Wörter ersetzen) Chiffre ( Buchstaben ersetzen ) Transposition ( Vertauschung ) Abbildung 5.1: Möglichkeiten in der Kryptologie Bevor wir uns einzelne Beispiele ansehen, legen wir ein paar allgemeine Sprechweisen fest : Definition. Der unverschlüsselte Text wird als Klartext bezeichnet und der verschlüsselte als Geheimtext. Jeder dieser beiden Texte besteht aus gewissen Zeichen, die Alphabete genannt werden. 1 Aktuelle Beispiele wären das Verstecken einer Nachricht als winzige Pixel in einer Bilddatei oder das Einfügen von nahezu unsichtbaren Wasserzeichen in Videodateien. 62 5 Kryptologie Das gesamte Verfahren um vom Klartext zum Geheimtext zu kommen heißt Verschlüsselungssystem. Die meisten Verfahren verwenden zum Übergang zwischen Klar- und Geheimtext einen sogenannten Schlüssel, so dass man auch von verschlüsseln und entschlüsseln spricht. Substitutionsverfahren Bei dieser Gruppe von Verfahren bleibt jeder Buchstabe des Alphabets beim Verschlüsseln an seinem Platz aber wird durch ein Zeichen des Geheimalphabets ersetzt. a) Cäsarcode : Bei diesem Code wird das Alphabet des Klartextes verschoben, so dass jeder Buchstabe durch einen anderen Buchstaben im verschobenen Alphabet ersetzt wird. Als Schlüssel müssen Sender und Empfänger nur vorher austauschen, mit welchem Buchstaben das verschobene Alphabet beginnt. Klartext: Geheimtext: ABCDEFGHIJKLMNOPQRSTUVWXYZ FGHIJKLMNOPQRSTUVWXYZABCDE EIN BEISPIEL JNS GJNXUNJQ Schlüssel : 1. Buchstabe des Geheimalphabets Abbildung 5.2: Cäsarcode b) Monoalphabetische Substitution : Sender und Empfänger vereinbaren vorab ein geheimes Wort, mit dem das Geheimalphabet beginnt ( Wiederholungen von Buchstaben im Wort werden übergangen ). Der Rest wird dann alphabetisch aufgefüllt. Klartext: Geheimtext: ABCDEFGHIJKLMNOPQRSTUVWXYZ KATZENLOBCDFGHIJMPQRSUVWXY ICH GEBE AUF BTO LEAE KSN Schlüssel : Wort ( hier KATZENKLO ) Abbildung 5.3: Erweiterter Cäsarcode c) Freimaurer-Code : Gerade in Jugendbüchern findet man oft Geheimschriften, die durch ungewöhnliche Zeichen auffallen. Der Freimaurer-Code verwendet nach einem festen System ein Geheimalphabet aus Strecken und Punkten. Wieder wird jedes Zeichen des Klartextalphabets durch genau ein Zeichen des Geheimalphabets ersetzt. Man spricht bei den Beispielen a), b) und c) auch von einer monoalphabetischen Substitution. A B C J . K. .L D E F M. N. .O . G H I P. Q . R T S U V X. Klartext: Geheimtext: W . .. Y Z E S G E H T L O S Schlüssel : Freimaurer-Code Abbildung 5.4: Freimaurer-Code d) Vigenère-Code : Im 16. Jahrhundert entwickelte französisches Diplomat Blaise de Vigenère ein neues System, das ihm und seinen Zeitgenossen lange Zeit als absolut sicher galt ( le chiffre indéchiffrable). Der große Fehler der monoalphabetischen Substitution liegt darin, dass ein Buchstabe des Klartextes immer wieder genau zum selben Zeichen des Geheimalphabets verschlüsselt wird. Dadurch sind derartig verschlüsselte Texte anfällig für einfache statistische Untersuchungen. Man zähle die Häufigkeiten der einzelnen Zeichen und kommt man sehr oft mit der Annahme 63 5 Kryptologie weiter, dass bei einem deutschen Text der Buchstabe E am häufigsten erscheint. Vigenères System vermeidet dies, indem die zu verwendenden Geheimalphabete rotieren. Buchstabe für Buchstabe wechselt man zu einem anderen Alphabet, sodass der gleiche Buchstabe zu verschiedenen Geheimzeichen führen kann. Diese Art von Substitution wird polyalphabetisch genannt. Schlüssel : Wort ( hier IGEL ) ABCDEFGHIJKLMNOPQRSTUVWXYZ IJKLMNOPQRSTUVWXYZABCDEFGHt GHIJKLMNOPQRSTUVWXYZABCDEF EFGHIJKLMNOPQRSTUVWXYZABCD LMNOPQRSTUVWXYZABCDEFGHIJK Klartext: Geheimtext: TREFFEN IN AACHEN BXIQNKR TV GENPKR Abbildung 5.5: Vigenère-Code Transpositionsverfahren Bei einem Transpositionsverfahren bleibt jeder Buchstabe wie er ist aber die Position innerhalb der Nachricht wird geändert. Die Buchstaben werden nach einem festgelegten System durcheinandergewirbelt. a) Matrix/Spaltensystem : Als Schlüssel wählt man ein Wort, dessen Länge eine Anzahl von Spalten vorgibt. Man trägt den Klartext von links nach rechts waagerecht ein. Anschließend sortiert das Schlüsselwort und alle darunter stehenden Spalten alphabetisch. Der Geheimtext ergibt sich daraus, dass man die Buchstaben anschließend Spalte für Spalte von oben abliest2 . Klartext: WIR TREFFEN UNS UM ACHT BEIM ALTEN BAHNHOF Schlüssel : KLAVIER K W F U E N F L I F M I B A R E A M A V T N C A H I R U H L N E E N T T H A R E A M A R F S B E O E E N T T H I R U H L N K W F U E N F L I F M I B R F S B E O V T N C A H Geheimtext: REAMAENTTHRUHLNWFUENFIFMIBFSBEOTNCAH Abbildung 5.6: Spaltencode b) ADFGX-Code : noch ergänzen 5.2 Das One-Time-Pad Beim Vigenère-Code springt man von Buchstabe zu Buchstabe zu einem anderen Geheimalphabet. Nach dem Ende des Schlüsselworts wiederholt sich dieser Vorgang und genau darin liegt eine Möglichkeit, das System zu knacken. Gut drei Jahrhunderte nach Vigenères Erfindung gelang es Charles Babbage diese Wiederholungen auszunutzen um jeden Vigenère-verschlüsselten Text auch ohne Schlüssel zu entziffern. 2 Prinzipiell kann man natürlich auch den Klartext in Spalten eintragen, dann vertauschen und den Geheimtext zeilenweise ablesen. Es muss nur vorab Einigkeit darüber herrschen, wie verschlüsselt wird. 64 5 Kryptologie Mit wenigen Änderungen am Vigenère-System kommt man aber zu einer Verschlüsselung, die dann jedem Angriff standhält. • Der Schlüssel ist genau so lang wie der Klartext. • Der Schlüssel besteht aus rein zufällig gewählten Zeichen. ( keine Wörter, kein System, alle Zeichen haben eine gleiche Häufigkeit. ) • Der Schlüssel wird nur ein einziges Mal verwendet. Mit diesen Änderungen spricht man vom sogenannten One-Time-Pad-Verfahren. Beim One-Time-Pad ist jeder Schlüssel gleich wahrscheinlich und als Folge kann man dem Geheimtext selbst auch keine Informationen mehr zum Schlüssel entnehmen. Aus einem Klartext kann im Prinzip jeder mögliche Geheimtext entstehen und auch umgekehrt kann es zu einem Geheimtext jeden möglichen Klartext geben. Ein Beispiel soll dies verdeutlichen : Jemand möchte den Satz “Ich liebe dich.” versenden. Dieser Text enthält 12 Zeichen von A-Z und daher wird auch ein Schlüssel der Länge 12 benötigt. Die Abbildung zeigt zwei mögliche, zufällig gewählte Schlüssel und die sich ergebenden Geheimtexte. Die hier gezeigten zwei möglichen Schlüssel Klartext: ICHLIEBEDICH Schlüssel : CXPQVSBCBNFW Geheimtext: KZWBDWCGEVHD Klartext: Schlüssel : Geheimtext: ICHLIEBEDICH VZTWNEIGSPLW DBAHVIJKVXND Abbildung 5.7: OneTimePad Verschlüsselung sind völlig willkürlich und erzeugen jeweils einen komplett unterschiedlichen Geheimtext. Spielen wir im Geiste alle möglichen Schlüssel durch und das sind hier ja alle möglichen Kombinationen aus 12 Zeichen, so erhalten wir auch alle möglichen Geheimtexte. Fängt jetzt ein Angreifer den Geheimtext ab und kennt den Schlüssel nicht, so könnte er ja rein theoretisch bei 12 Buchstaben alle möglichen Schlüssel ausprobieren ( sogenannter brute-force-Angriff ). Allerdings wird ihm das alles nichts bringen, denn auch umgekehrt finden man zu jedem Geheimtext alle möglichen Klartexte. Alle möglichen Schlüssel erzeugen die unterschiedlichsten Buchstabensalate Geheimtext: KZWBDWCGEVHD Geheimtext: KZWBDWCGEVHD Geheimtext: KZWBDWCGEVHD Schlüssel : CXPQVSBCBNFW Klartext: ICHLIEBEDICH Schlüssel : CXPUDEKCBNFW Klartext: ICHHASSEDICH Schlüssel : YVPKMCQHKJZM Klartext: MEHRRUMZUMIR Abbildung 5.8: OneTimePad Entschlüsselung beim Entschlüsseln und ohne irgendeine Kenntnis über den Schlüssel bleibt eben unklar, welcher der möglichen Klartexte ( Ich liebe dich, Ich hasse dich, Mehr Rum zu mir) der gemeinte ist. Lässt sich nichts über den Schlüssel herausfinden, so führt kein Weg zum Klartext. Bei der vorhandenen Sicherheit des Verfahrens stellt sich sofort die Frage : Warum benutzt man dann nicht für jede Form von Verschlüsselung das OneTime-Pad? Das Problem der Schlüsselverteilung Gehen wir mal davon aus, dass wir mit ein paar Bekannten einen geheimen Spionagering in Europa aufbauen möchten. Insgesamt hätten wir n = 100 Personen gefunden, die sich über ganz Europa verteilt hätten und anschließend geheime Informationen über die einzelnen Länder miteinander austauschen. Wenn in unserem Agentennetz jeder mit jedem kommunizieren kann, so müssen wir alle Kombination von Sender und Empfänger berücksichtigen. Kann jeder senden, haben wir n Möglichkeiten beim Sender 65 5 Kryptologie und anschließend n 1 Möglichkeiten beim Empfänger. Das führt insgesamt auf eine Anzahl von n(n 1) Varianten3 . In unserem Fall wären das 100 · 99 = 9900 Möglichkeiten. Schicken unsere Agenten jetzt täglich über jeden möglichen Kanal nur eine Nachricht, so kommen wir schon auf 9900 Nachrichten pro Tag oder 297000 im Monat. Also brauchen wir jeden Monat 297000 zufällige Passwörter, die ja auch erst noch erzeugt werden müssen und - viel schlimmer - an alle Agenten verteilt werden müssen. Diese Schlüssel müssen streng geheim gehalten werden, daher sollten zum Austausch der Schlüssel nur wenige Personen involviert sein. Andererseits können wenige Personen schlecht viele Schlüssel verteilen. Und sollten wir irgendwann auf die Idee kommen, dass wir größeren Bedarf an Agenten haben und jetzt 200 Agenten benötigen, so steigt die Anzahl leider quadratisch an. Wir brauchen 39800 Schlüssel pro Tag oder 1194000 pro Monat. Ein wahrer Alptraum, der dazu führt, dass man das OneTime-Pad nur für sehr spezielle Nachrichtenkanäle verwendet4 Das Prinzip von Kerckhoff Das Problem der Schlüsselverteilung könnte man ja auch so lösen, dass man gar keine Schlüssel verwendet und stattdessen eine Maschine baut, die nur speziel Eingeweihte kennen. Der abhörende Gegner kennt die Maschine nicht, besitzt sie auch nicht und wenn man nur alle Sicherheitsanstrengungen unternimmt, dann wird er vielleicht ja auch niemals erfahren, dass es eine neue Verschlüsselungsmaschine gibt. Diese Denkweise wird im Englischen mit “security by obscurity” umschrieben und meint eine Vorgehensweise, die Sicherheit ( security ) dadurch erzeugt, dass sie den Gegner im Unklaren ( obscurity ) über das Verfahren lässt. Im Alltag benutzen manche Menschen genau ein solches System, in dem sie z.B. für den Haustürschlüssel innerhalb einen Platz vereinbaren, an dem für Notfälle ein Ersatz liegt. Leider ist es dann doch allzu oft der Platz unter der Fußmatte und so ist eben die geplante Unklarheit nicht immer gegeben. Auch wenn natürlich etwas Unklarheit gegenüber Unbeteiligten oder Gegnern nicht schaden kann, so sollte man sich nicht allzu sehr darauf verlassen, dass der Gegner unwissend bleibt. Die Geschichte der Spionage lehrt, dass es irgendwann immer jemanden mit einem Motiv ( Geld, Ideologie, ... ) gibt, der Geheimnisse verrät. Daher sollte man immer schon von dem Fall ausgehen, dass ein Gegner die Methode einer Verschlüsselung kennt und das System dennoch dank eines Schlüssels sicher bleibt. In der Kryptologie wird dies als Prinzip von Kerckhoff bezeichnet : Die Sicherheit eines Verschlüsselungssystems beruht nur auf der Geheimhaltung des Schlüssels aber nicht auf der Geheimhaltung des Verfahrens. (Prinzip von Kerckhoff) Treffend fasste dies auch der amerikanische Informatiker Claude Shannon folgendem Satz zusammen: “The enemy knows the system.” ( Claude Shannon ) 5.3 Rechnen mit Resten Diese Art der Verschlüsselung heißt RSA-Verschlüsselung ( nach den Anfangsbuchstaben der Nachnamen ) und soll hier nachvollzogen werden. Das Verfahren basiert auf einem Teilgebiet der Mathematik namens Zahlentheorie, bei dem es um Eigenschaften von ganzen Zahlen geht. Primzahlen, Quadratzahlen, Teiler und ähnliche Fachbegriffe stammen alle aus diesem Gebiet. 3 Beachte dabei, dass wir hier unterscheiden zwischen A ! B ( A sendet an B) und B ! A (B sendet an A). Ohne diese Unterscheidung sind es nur halb so viele, d.h. n(n 1)/2. 4 Der Legende nach für die Kommunikation zwischen dem Weißen Haus und dem Kreml. 66 5 Kryptologie Modulo-Rechnungen Wichtig wird im weiteren Verlauf das Rechnen mit Resten sein, d.h. ähnlich wie in der Grundschule werden wir danach fragen, welcher Rest bei einer Zahl bleibt, wenn wir sie durch eine andere teilen. Schöner ausgedrückt spricht man von der modularen Arithmetik und wählt sich eine feste Zahl m, den sogenannten Modul. Alle möglichen, ganzen Zahlen lassen sich dann durch m dividieren und auf ihren Rest hin überprüfen. Wählen wir z.B. m = 4, so können wir alle anderen Zahlen danach einteilen, welche Reste sie beim Dividieren durch 4 erzeugen. Egal mit welcher Zahl wir auch beginnen, es ergibt sich immer einer der Reste 0, 1, 2 oder 3. Diese vier Reste bilden Restklassen und jede Zahl lässt sich einer der Restklassen zuordnen. Modul m=4 Rest 0 0 4 12 16 40 100 -8 92 Rest 1 5 17 81 4005 Rest 2 6 1 -3 -2 34 1002 26 2 Rest 3 3 7 -1 15 123 Abbildung 5.9: Restklassen modulo 4 Definition. Für m 2 N+ als Modul lässt sich jede Zahl z 2 Z einer der Restklassen 0, 1, 2, . . . , m 1 zuordnen, je nachdem welcher Rest bei der Division durch m entsteht. Um die Zugehörigkeit von z zur Restklasse k anzugeben, verwendet man die Schreibweise z⌘k (mod m) und verwendet die Sprechweise “z ist kongruent k modulo m “. Beispiel. a) Offenbar gilt ( einfaches Nachrechnen ) : 25 ⌘ 4 (mod 7) und 25 ⌘ 1 (mod 8) . b) Betrachtet man die Reihe der Quadratzahlen modulo 4, so zeigt sich folgender Anfang : 1 ⌘ 1 (mod 4) 4 ⌘ 0 (mod 4) 9 ⌘ 1 (mod 4) 16 ⌘ 0 (mod 4) 25 ⌘ 1 (mod 4) Es sieht also stark danach aus, dass die Quadratzahlen modulo 4 abwechselnd nur die Reste 0 und 1 liefern aber niemals einen Rest 2 oder 3. Das lässt sich einfach erklären durch folgende Überlegung : Quadriert man eine gerade Zahl etwa 2n , so erhält man (2n)2 = 4n2 und damit ein Vielfaches von 4, d.h. beim Teilen durch 4 bleibt kein Rest. Das Quadrat einer ungeraden Zahl ( 2n + 1 ) ergibt hingegen : (2n + 1)2 = 4n2 + 4n + 1. Die ersten beiden Terme sind Vielfache von 4. Die zusätzliche 1 sorgt dann dafür, dass beim Dividieren durch 4 ein Rest von 1 entsteht. Also : (2n)2 = 4n2 ⌘ 0 (mod 4) (2n + 1)2 = 4n2 + 4n + 1 ⌘ 1 (mod 4) 67 5 Kryptologie Eine große Besonderheit beim Rechnen mit Resten besteht darin, dass man große Zahlen schon vorab auf ihre Restklasse reduzieren kann und man dann nur mit dem ( meist vom Wert her viel kleineren ) Rest rechnen muss5 . Rechnen wir auch dazu ein Beispiel : Beispiel. Welche Zahl gehört auf die rechte Seite bei 7709 · 73 ⌘ ? (mod 7) Langer Weg mit den großen Zahlen : Produkt ausrechnen 7709 · 73 = 562757 und jetzt nach dem Rest suchen : 562757 : 7 = 80393 R6. Also ist 7709 · 73 ⌘ 6 (mod 7) Kurzer Weg : vorher reduzieren : 7709 ⌘ 2 (mod 7) und 73 ⌘ 3 (mod 7) und dann ist 7709 · 73 ⌘ 2 · 3 = 6 (mod 7) Auf diese Weise lassen sich auch sehr große Rechnungen durchführen, die selbst einen gewöhnlichen Taschenrechner überfordern. Beispiel. Mit welcher Ziffer endet die Zahl 3100 ? Die Endziffer einer Zahl erhält man dann, wenn man die Zahl modulo 10 betrachtet, denn dann fragt liefert die letzte Einerstelle ja genau einen Rest von 0 bis 9. Unser Taschenrechner erlaubt es aber nicht 3100 auszurechnen. Daher nähern wir uns trickreich dieser Potenz. Es ist 34 = 81 ⌘ 1 (mod 10) und daraus folgt dann trickreich : 3100 = (34 )25 ⌘ 125 = 1 (mod 10) Die Zahl hat die Endziffer 1. Beim letzten Beispiel hat es also sehr geholfen, dass wir die Basis beim Potenzieren geschickt durch einen vorher berechneten Rest ersetzen konnten. Fassen wir diese Ersetzungen mal zusammen. Satz. Es seien a, b 2 Z beliebige ganze Zahlen und m ein Modul. Ferner seien r1 , r2 die zugehörigen Restklassen von a und b in Bezug auf m , d.h. a ⌘ r1 (mod m) und b ⌘ r2 (mod m). Dann gilt : 1) a ± b ⌘ r1 ± r2 (mod m) 2) a · b ⌘ r1 · r2 (mod m) 3) an ⌘ r1n (mod m) für n 2 N. Beim Rechnen mit Resten lassen sich also alle Summanden ( Regel 1) , Faktoren ( Regel 2) und die Basis beim Potenzieren ( Regel 3) durch den zugehörigen Rest ersetzen. Beispiel. a) Es ist 512 + 308 ⌘ 2 + 2 = 4 ⌘ 1 (mod 3). b) 27 · 35 ⌘ 5 · 2 = 10 (mod 11) c) 205 ⌘ 25 = 32 ⌘ 2 (mod 6) 5.4 Lineare Kongruenzen Dadurch, dass wir mit Resten rechnen, wurden viele schwierige Rechnungen ( 3100 u.a. ) wesentlich leichter. Allerdings tauchen dafür Probleme an Stellen auf, an die man zunächst nicht denkt. 5 Dies folgt natürlich nicht einfach dadurch, dass wir es gerne so hätten, sondern muss erst bewiesen werden. 68 5 Kryptologie Die Gleichung 3x = 1 kann jeder Schüler lösen, der schon Brüche kennt. Mit nur einem Umformungs1 schritt gelangt man zu x = . Die entsprechende Gleichung 3 · x ⌘ 1 (mod 19) sieht zwar ähnlich 3 einfach aus, aber ist deutlich schwieriger zu lösen. Eine Division im Bereich der ganzen Zahlen führt ja eben viel zu oft zu Brüchen und damit aus den ganzen Zahlen heraus. Zunächst bleibt also wenig übrig, als alle möglichen Restklassen von 0 bis 18 zu probieren und dadurch auf die Lösung x = 13 zu kommen. Natürlich sind dann auch alle anderen ganzen Zahlen, die zur Restklasse 13 gehören, Lösungen, d.h. x = 32 = 13 + 19 oder x = 6 = 13 19 erfüllen ebenso die Kongruenzgleichung. Wir vereinbaren, dass wir in Zukunft beim Modul m nur die Lösungen innerhalb der Restklassen von 0 bis m 1 angeben. Zusammengefasst : 3 · x ⌘ 1 (mod 19) () x ⌘ 13 (mod 19) Definition. Eine Gleichung der Art a · x ⌘ b (mod m) mit a, b 2 Z und m 2 N als Modul heißt lineare Kongruenz. Eine Lösung der Gleichung ist eine der möglichen Restklassen 0, 1, 2, . . . , m 1 , die die lineare Kongruenz erfüllt. Beispiel. Beachte, dass lineare Kongruenzen nicht immer eine Lösung besitzen oder auch mehrere Lösungen haben können. Die folgenden Beispiele zeigen dies : a) Die Kongruenz 5 · x ⌘ 2 (mod 7) hat die Lösung x = 6. b) Die Kongruenz 2 · x ⌘ 3 (mod 4) hat gar keine Lösung, denn die linke Seite erzeugt nur gerade Zahlen, die modulo 4 nur die Reste 0 oder 2 erzeugen. c) Die Kongruenz 4 · x ⌘ 6 (mod 10) hat die Lösungen x = 4 sowie x = 9. Systematisches Lösen linearer Kongruenzen Damit wir in Zukunft nicht alle Lösungen der linearen Kongruenzen durch stures Raten bestimmen müssen, suchen wir nach einem generellen Verfahren zum Auffinden der Lösungen ( sofern sie existieren! ). Etwas unerwartet erweist sich dabei ein Hilfsmittel als geeignet, das zuletzt in Klasse 6 bei ganzen Zahlen verwendet wurde : der größte, gemeinsame Teiler ( ggT ) zweier Zahlen. Definition. Für zwei positive Zahlen a, b 2 N sei ggT (a, b) der größte, gemeinsame Teiler. Mitunter schreibt man verkürzend auch nur (a, b) für diese Zahl. Während man bei kleinen Zahlen durch ein wenig Kopfrechnen schnell den ggT bestimmen kann, wird es bei großen Zahlen sehr unübersichtlich. Was ist z.B. ggT (4848, 6000) ? Für solche Fälle gibt es ein sehr altes Verfahren namens Euklidischer Algorithmus, bei dem so lange die Division mit Rest durchgeführt wird bis die Division aufgeht. Dann ist der letzte vorherige Rest gleich dem gesuchten ggT. 6000 = 1 · 4848 + 1152 4848 = 4 · 1152 + 240 1152 = 4 · 240 + 192 240 = 1 · 192 + 48 192 = 4 · 48 Also ist hier ggT (4848, 6000) = 48. Der Beweis, dass dieses Verfahren in jedem Falle den ggT erzeugt, lassen wir in diesem Skript aus. Satz. Der größte, gemeinsame Teiler ggT (a, b) zweier ganzer, positiver Zahlen a, b lässt sich mit Hilfe der wiederholten Division mit Rest ( = Euklidischer Algorithmus ) berechnen. 69 5 Kryptologie Die vielen Divisionszeilen beim Euklidischen Algorithmus können noch weiter verwendet werden, indem man sie von unten nach oben erneut durchgeht. Beginnen wir mit der vorletzten Zeile und stellen nach dem ggT um, so erhalten wir : 240 = 1 · 192 + 48 =) ggT = 48 = 240 1 · 192 Jede weitere, darüber stehende Zeile nutzen wir jetzt, um dadurch einen Faktor umzuschreiben. In diesem Fall sagt uns die Zeile darüber : 192 = 1152 4 · 240 und setzen dies in die Zeile für den ggT ein : ggT = 48 = 240 1 · (1152 4 · 240) = 5 · 240 1 · 1152 Dies setzen wir fort und verwenden die zweite Zeile, um die 240 zu ersetzen : ggT = 48 = 5 · (4848 4 · 1152) 1 · 1152 = 5 · 4848 21 · 1152 Ein letztes Mal ersetzen wir mit der ersten Zeile die 1152 : ggT = 48 = 5 · 4848 21 · (6000 1 · 4848) = 26 · 4848 21 · 6000 Somit hätten wir jetzt eine Möglichkeit gefunden, den ggT (4848, 6000) direkt mit Hilfe der beiden Zahlen darzustellen : 48 = 26 · 4848 21 · 6000. Das Ergebnis ähnelt ein wenig der Darstellung einer Linearkombination von Vektoren. Das hier exemplarisch durchgeführte Zurückrechnen und die Darstellung des ggT nennt man auch den erweiterten Euklidischen Algorithmus. Satz. Der größte, gemeinsame Teiler ggT (a, b) zweier ganzer, positiver Zahlen a, b lässt sich in der Form ggT = n1 · a + n2 · b mit n1 , n2 2 Z schreiben, d.h. man kann den ggT als Linearkombination der gegebenen Zahlen darstellen. Zu einer solchen Schreibweise gelangt man mit Hilfe des erweiterten Euklidischen Algorithmus. Vereinfachung des Verfahrens Das jetzige Vorgehen beim erweiterten Euklidischen Algorithmus ist doch sehr lang und mühselig. Daher betrachten wir kurz eine Methode, die beides bietet. Sie bestimmt gleichzeitig den ggT und die zugehörige Linearkombination. Dazu betrachten wir als Beispiel die Zahlen a = 116 und b = 84 . Bei diesem Verfahren schreiben wir jede Zahl als Linearkombination von a und b und starten mit den gegebenen Zahlen : 116 = 1 · 116 + 0 · 84 84 = 0 · 116 + 1 · 84 Genau wie beim üblichen Euklidischen Verfahren untersuchen wir, wie oft die 84 in die 116 passt ( einmal ) und subtrahieren daher links einmal die 84 . Auf der rechten Seite rechnen wir direkt mit den Vielfachen von 116 und 84 : 116 = 1 · 116 + 0 · 84 84 = 0 · 116 + 1 · 84 32 = 1 · 116 1 · 84 Auf gleiche Weise fahren wir fort und müssen jetzt zweimal die 32 von 84 subtrahieren : 84 = 0 · 116 + 1 · 84 32 = 1 · 116 20 = 1 · 84 2 · 116 + 3 · 84 70 5 Kryptologie Dies setzen wir solange fort, bis links beim Subtrahieren kein Rest bleibt. Dann haben wir den ggT gefunden : 32 = 1 · 116 1 · 84 12 = 3 · 116 4 · 84 4 = 8 · 116 11 · 84 20 = 8 = 2 · 116 + 3 · 84 5 · 116 + 7 · 84 Zusammenhang zu linearen Kongruenzen Bevor wir jetzt zu sehr begeistert vom ggT und seiner Darstellung sind, sollten wir doch klären, was das alles mit dem Lösen von linearen Kongruenzen zu tun hat. Nehmen wir als Beispiel die Kongruenz 84 · x ⌘ 48 (mod 116). Darin finden wir die Zahlen 84 und 116 wie im letzten Beispiel wieder. Die Darstellung des ggT ergab ja : 4 = 8 · 116 11 · 84 Der Trick besteht jetzt darin, diese Zeile modulo 116 zu lesen. Dann fallen alle Vielfachen von 116 weg und es ergibt sich : 4 ⌘ 11 · 84 (mod 116) bzw. mit vertauschten Positionen : 84 · ( 11) ⌘ 4 (mod 116) Vergleichen wir mit der zu lösenden Kongruenz, so erkennen wir, dass auf der rechten Seite noch keine 48 steht, sondern eine 4. Da die 48 aber ein Vielfaches von 4 ist, können wir einfach beide Seite mit 12 multiplizieren : 84 · ( 11) · 12 ⌘ 48 (mod 116) 84 · ( 132) ⌘ 48 (mod 116) Die Zahl 132 ist noch etwas irritierend aber natürlich können wir jede Zahl durch Addition/Subtraktion auf die üblichen Restklassen von 0 bis 115 zurückführen. Somit könnte die Zeile auch heißen ( Beachte : 132 ⌘ 100 ) 84 · 100 ⌘ 48 (mod 116) Und damit haben wir die Lösung x = 100 von der zu lösenden Kongruenz gefunden. Gibt es weitere Lösungen? Zunächst könnten wir auf jeden Fall 116 nach Belieben addieren oder subtrahieren und erhalten weitere Zahlen als Lösungen. Allerdings gibt man üblicherweise eben nur die Lösungen in den Restklassen von 0 bis 115 an und da bleiben wir dann ja bei x = 100 stehen. Bleibt es bei dieser Lösung? Addieren wir geschickt zu unserer gefundenen Lösung, so finden wir weitere. Was wäre z.B. mit x = 100 + 29 = 129 ⌘ 13 ? Es ist 84 · 13 = 1092 ⌘ 48 (mod 116) und damit ist auch x = 13 eine Lösung. Weiteres Addieren von 29 ergibt die weiteren Lösungen x = 13 + 29 = 42 und x = 42 + 29 = 71 . Nochmaliges Addieren führt dann zurück zu unserer ersten Lösung x = 100. Der folgende Satz verallgemeinert und fasst zusammen : 71 5 Kryptologie Satz. Es seien a, b zwei positive ganze Zahlen und m 2 N der bekannte Modul. Dann gilt : 1. Die lineare Kongruenz a · x ⌘ b (mod m) besitzt genau dann mindestens eine Lösung, wenn ggT (a, m) ein Teiler von b ist. m m 2. Ist x0 eine Lösung der Kongruenz, so sind x0 + ggT , x0 + 2 ggT , . . . weitere Lösungen. Die Anzahl der Lösungen ist gleich dem ggT . Beweis. Der Beweis dieses Satzes findet sich im Anhang A3. Damit sind wir ab jetzt in der Lage uns bei jeder Kongruenz zu möglichen Lösungen zu äußern. Einen wichtigen Sonderfall des obigen Satzes notieren wir separat: Satz. Es sei m 2 N der bekannte Modul und a eine positive ganze Zahl, die zu m teilerfremd ist. Dann gilt : Die lineare Kongruenz a · x ⌘ 1 (mod m) hat eine einzige Lösung. Beweis. Diese Aussage folgt direkt mit dem vorher notierten Satz mit b = 1. Sind nämlich a und m teilerfremd, so ist ggT (a, m) = 1. Dieser ggT teilt auch b = 1 und damit gibt es eine Lösung x0 . Nach dem zweiten Teil des Satzes ergeben sich die weiteren Lösungen x0 + m, x0 + 2m, . . .und reduziert modulo m bleibt es daher bei der einzigen Lösung x0 . Definition. Wenn a und m teilerfremd sind, so nennt man die einzige Lösung von a · x ⌘ 1 (mod m) auch das Inverse von a . Damit ist gemeint, dass die Zahl x0 die Multiplikation mit a wieder aufhebt. Dies erinnert an 5 und 15 bei der klassischen Multiplikation ohne Rest. Beispiel. Zu a = 4 ist a0 = 5 das Inverse von a modulo 19 , denn a · a0 = 4 · 5 = 20 ⌘ 1 (mod 19). Dass in diesem Beispiel a0 = 5 eine aufhebende Wirkung hat, zeigt folgende Rechnung : 3 · 4 · 5 = 60 ⌘ 3 (mod 19). Die anschließende 5 hebt die Multiplikation mit 4 zuvor wieder auf. Mit einer leicht anderen Schreibweise kann man dies verdeutlichen : 3 · 4 · 5 = 3 · (4 · 5) ⌘ 3 · 1 = 3 (mod 19) Beispiel. a) Zu a = 4 ist a0 = 3 das Inverse von a modulo 11 , denn a · a0 = 4 · 3 = 12 ⌘ 1 (mod 11). Das Inverse zu einer Zahl a hängt also nicht nur von a ab, sondern auch vom Modul m. b) Zu a = 4 gibt es modulo 20 kein Inverses, denn bei 4 · x ⌘ 1 (mod 20) sind 4 und 20 nicht teilerfremd. 5.5 Einwegfunktionen In der üblichen Schulanalysis gibt es die klassische Exponentialfunktion ( z.B. f (x) = ex ) und den zugehörigen Logarithmus ln(x) als Umkehrfunktion. Wie bei jeder Funktion kann man bei bekannten x in die Funktion einsetzen und den Funktionswert berechnen (z.B. f (5) = e5 = 148, 41). Ist der Funktionswert bekannt, so hilft die Umkehrfunktion um das zugehörige x zu bestimmen ( z.B. f (x) = 200, d.h. ex = 200 und dank Umkehrfunktion ist x = ln 200 = 5, 298 ). Wir übertragen die Funktion in den Bereich des modularen Rechnens. Statt der irrationalen Zahl e müssen wir dann als Basis eine ganze Zahl verwenden. Es sei also f (x) = 2x (mod 53) Bei bekannten x ist das Berechnen des Funktionswerts weiterhin leicht möglich : f (5) = 25 = 32 (mod 53) f (8) = 28 = 256 ⌘ 44 (mod 53) f (20) = 220 = 28 · 28 · 24 ⌘ 44 · 44 · 16 = 1936 · 16 ⌘ 28 · 16 = 448 ⌘ 24 (mod 53) 72 5 Kryptologie Versuchen wir aber von einem gegebenen Funktionswert zurück zu x zu kommen, so ist diese Aufgabe deutlich schwerer, da wir keinerlei Logarithmus beim Rechnen mit Resten haben. =) x = ? f (x) = 40 Prinzipiell könnte man ja alle Restklassen von 0 bis 52 einsetzen und kann dann jedem x das f (x) zuordnen. Die obigen drei Beispiele führen dann auf die Punkte (5 | 32), (8 | 44) und (20 | 24). Vertauscht man die beiden Koordinaten, so erhält man jeweils einen Punkt auf dem Graphen der Umkehrfunktion, d.h. auf dem Graphen vom Zweierlogarithmus. Führt man das für alle Restklassen durch, kommt allerdings die böse Überraschung. Es entsteht ein heillos chaotischer Graph, der mit dem bekannten Verlauf von log2 (x) nichts zu tun hat. Dieser Logarithmus wird diskreter Logarithmus genannt. 55 Diskreter Logarithmus zu 2x (mod 53) 50 45 40 35 30 25 20 15 10 5 0 0 5 10 15 20 25 30 35 40 45 50 55 x Abbildung 5.10: Diskreter Logarithmus zu 2x (mod 53) Halten wir fest : Bemerkung. Die Gleichung ax ⌘ b (mod m) ist im Allgemeinen mit keiner einfachen Methode nach x aufzulösen. Eine direkte Umkehrfunktion wie der Logarithmus ist im Bereich des modularen Rechnens nicht zu finden. Man spricht auch vom Problem des diskreten Logarithmus. Funktionen wie f (x) = 2x (mod 53), die in einer Richtung ( x bekannt, f (x) berechnen ) sehr leicht zu handhaben sind, aber in umgekehrter Richtung ( f (x) bekannt, x berechnen ) ungeahnte Schwierigkeiten aufwerfen, heißen Einwegfunktionen. Das Verhalten erinnert an eine Falltür, durch die man leicht fallen kann, dann aber umgekehrt nicht mehr aus der Falle herauskommt. Daher spricht man auch von einer Trapdoor-Funktion. x ax ( mod m) Abbildung 5.11: Einwegfunktionen/Trapdoor 73 5 Kryptologie Alice und Bob ( und Mr.X ) Betrachten wir ganz allgemein zwei Personen ( Alice und Bob ), die miteinander Nachrichten austauschen. Leider gibt es aber eine dritte Person - hier Mr.X genannt - die so neugierig ist, dass sie alle ausgetauschten Nachrichten mitlesen möchte. Prinzipiell trauen wir Mr.X zu, dass er alle gesendeten Nachrichten lesen kann und er auch von technischen Hürden nicht abgehalten wird. Mr.X Alice Bob Abbildung 5.12: Alice, Bob und Mr.X Verwenden Alice und Bob keinerlei Verschlüsselung, so hat Mr.X natürlich leichtes Spiel. Entscheiden sich beide dazu ein Verschlüsselungsverfahren ( z.B. das OneTimePad) einzusetzen, so besteht das weiter oben schon erwähnte Problem, dass sie irgendwie die nötigen Schlüssel austauschen müssen. Über ihre unsichere Leitung sollten sie den Schlüssel besser nicht senden, denn sonst kann Mr.X ihn ja auch mitlesen. Zumindest dachte man so jahrhundertelang bis die Mathematiker Whitfield Diffie und Martin Hellman 1976 zeigten, dass man mit den in diesem Abschnitt erwähnten Einwegfunktionen ein Schema finden kann, bei dem Alice und Bob auch bei einer überwachten Leitung einen Schlüssel austauschen können ohne dass Mr.X ihn kennt. a) Beide einigen sich auf eine Einwegfunktion. Wir verwenden hier die oben schon betrachtete Funktion f (x) = 2x (mod 53) auch wenn sie für praktische Zwecke eine zu kleine Basis und Modul aufweist. Ihre Wahl können sie auch über die unsichere Leitung mitteilen, d.h. wir können davon ausgehen, dass Mr.X die Einwegfunktion bekannt ist. b) Alice wählt eine nur ihr bekannte Zahl A, die sie Bob nicht mitteilt. Ebenso wählt Bob eine geheime Zahl B . c) Alice berechnet mit ihrer Zahl ↵ = f (A) = 2A (mod 53) und auch Bob setzt seine Zahl in die Einwegfunktion ein und berechnet = f (B) = 2B (mod 53). d) Alice verschickt die Zahl ↵ und Bob die Zahl . A B e) Zum Schluss berechnet Alice einfach A = 2B = 2BA (mod 53) und Bob ↵B = 2A = AB 2 (mod 53) und siehe da : Beide erhalten die gleiche Zahl 2AB (mod 53), die sie ab jetzt als Schlüssel verwenden können. 74 5 Kryptologie Mr.X Bob Alice f(x)=2x (mod 53) geheime Zahl A geheime Zahl B berechnet α=f(A) berechnet β=f(B) β α berechnet βA (mod 53) berechnet αB (mod 53) Abbildung 5.13: Austausch nach Diffie-Hellman Aber kann denn Mr.X nicht auch auf den Schlüssel kommen? Immerhin hat er doch erfahren, um welche Funktion es ging und hat auch ↵ und abfangen können. Dazu müsste er aber ↵ = 2A (mod 53) bzw. = 2B (mod 53) nach A bzw. B auflösen und genau dieses Auflösen war ja so viel ungemein schwerer als die Rechnungen von Alice und Bob ( Problem des diskreten Logarithmus ). Beispiel. Betrachten wir als Beispiel nochmal die Funktion f (x) = 2x (mod 53). Angenommen, Alice wählt A = 14 und B = 30 . Wie lautet der Schlüssel? Alice berechnet ↵ = f (14) = 214 = 16384 ⌘ 7 (mod 53) und verschickt ↵ = 7 an Bob. Dieser berechnet seinerseits : = f (30) = 230 = 214 · 214 · 22 ⌘ 7 · 7 · 4 = 37 (mod 53) und sendet = 37 an Alice. Mr.X kann ↵, abfangen, kann aber eben nicht einfach zurückrechnen, welches die Zahlen A und B waren6 . Nach dem Empfang von ↵, rechnen beide weiter. Bei Alice ergibt sich A = 3714 ⌘ 16 (mod 53) und bei Bob ↵B = 730 ⌘ 16 (mod 53) und wie vorher allgemein gezeigt, kommen beide auf die gemeinsame Zahl 16 . 5.6 Asymmetrische Verschlüsselungsverfahren Das geschilderte Diffie-Hellman-Protokoll war in den 70er Jahren des 20. Jahrhunderts nur der Auftakt zu umwälzenden Ideen in der Kryptologie. Schlüssel konnten jetzt auch über unsichere Leitungen ausgetauscht werden. Aber es kam noch besser : 1977 fanden Roland Rivest, Adi Shamir und Leonard Adleman eine Verschlüsselungsmethode ( RSA-Verfahren genannt ), bei der überhaupt keine gemeinsamen Schlüssel mehr ausgetauscht werden mussten. 6 Natürlich könnte er bei diesen kleinen Zahlen einfach alle Restklassen von 0 bis 52 in 2x einsetzen und überprüfen, wann 7 bzw. 37 als Ergebnis sich zeigen. Bei größeren Zahlen dauert dieses Probieren schlicht zu lang. 75 5 Kryptologie Symmetrische Verfahren Üblicherweise ging man jahrhundertelang so vor, dass Sender und Empfänger den gleichen geheimen Schlüssel kennen. Dieser dient zum Entschlüsseln und gleichzeitig auch zum Verschlüsseln. Solche Verfahren werden daher auch symmetrisch genannt und dem Kerkhoff’schen Prinzip folgend liegt die Sicherheit des Verschlüsselns im Geheimhalten des Schlüssels. Gibt es mehr als zwei kommunizierende Partner ( Alice, Bob und neuerdings auch Charlie ), so müssen alle untereinander erst immer geheime Schlüssel vereinbaren und genau darin lag ja das Problem des Schlüsselaustausches. Alice Geheimer Schlüssel Bob Geheimer Schlüssel Geheimer Schlüssel Charlie Abbildung 5.14: Symmetrische Kommunikation Asymmetrische Verfahren Einen ganz anderen Ansatz bieten Verfahren, bei denen jeder Teilnehmer einen zweiteiligen Schlüssel besitzt. Der eine Teil ist sein privater Schlüssel, den nur er kennt und der auch niemals herausgegeben wird. Der zweite Teil des Schlüssels besteht aus einem öffentlichen Schlüssel, der jedermann zugängig ist und wie in einer Art Telefonbuch der Allgemeinheit bekanntgegeben wird. Somit hat jeder, der dieses Verschlüsselungssystem benutzt ein Paar an Schlüssen. Als Ganzes gesehen gibt es somit privat öffentlich Abbildung 5.15: Schlüsselpaar einen öffentlichen Schlüsselring, bei dem jeder seinen öffentlichen Schlüssel bekannt macht und einen privaten Schlüssel, der für alle anderen immer unsichtbar bleibt. Man spricht hier auch von public key-Kryptographie. 76 5 Kryptologie Alice Bob pr iv öf fen tli ch öffe n tl ich t priv a at ch ntli öffe Charlie at priv Abbildung 5.16: Asymmetrisches Verfahren Wie funktioniert so ein System in der Praxis? Nehmen wir an, dass Charlie eine geheime Nachricht an Alice senden möchte. Er sucht dann den öffentlichen Schlüssel von Alice und verschlüsselt damit seine Nachricht. Das Besondere besteht darin, dass nur der geheime Schlüssel von Alice die Nachricht entschlüsseln kann. Sogar Charlie selbst hätte jetzt keine Möglichkeit mehr zum Entschlüsseln. Daher heißen solche Systeme eben asymmetrisch, denn zum Ver- und Entschlüsseln werden zwei unterschiedliche Schlüssel gebraucht. Wichtig ist noch, dass es keinerlei Möglichkeit geben darf, aus dem öffentlichen Schlüssel irgendwie auf den privaten Schlüssel zu kommen, denn mit dem privaten Schlüssel kann man alles entschlüsseln, was der Person gesendet wird. Das RSA-Verfahren - Vorbereitungen Alle Überlegungen zu den public key-Verfahren stammten noch von Whitfield und Diffie aber hatten einen kleinen Schönheitsfehler : Sie funktionierten nur in der Theorie. In der Praxis fanden beide keine Möglichkeit wie sie ihre Ideen mit den Schlüsselpaaren realisieren sollten. Dies gelang nach kurzer Zeit den Mathematikern Rivest, Shamir und Adleman, deren Nachnamen dann Namensgeber für das von ihnen entwickelte RSA-Verfahren war. Um ihr Verfahren zu verstehen, brauchen wir im Wesentlichen zwei Grundlagen : • das Rechnen mit Resten ( insbesondere das Berechnen von Inversen ), siehe Abschnitt 5.3 • die Euler’sche Phi-Funktion, die eine Aussage über das Potenzieren beim Modulo-Rechnen trifft Legen wir los : Definition. Für eine Zahl n 2 N⇤ gibt '(n) die Anzahl aller positiven Zahlen n an, die zu n teilerfremd sind. ( Teilerfremd sind zwei Zahlen, wenn sie außer der Zahl 1 keine weiteren, gemeinsamen Teiler besitzen.). Die Zuordnung n 7! '(n) wird Euler’sche Phi-Funktion genannt. Beispiel. a) Es ist '(10) = 4 , denn nur die vier Zahlen 1, 3, 7, 9 sind teilerfremd zu 10 . b) Es ist '(6) = 2, '(3) = 2, '(15) = 8, '(17) = 16 Für die Phi-Funktion konnte bereits Euler viele Eigenschaften herausfinden von denen wir aber nur die erwähnen, die im RSA-Verfahren wichtig sind. 77 5 Kryptologie Satz. Für die Euler’sche Phi-Funktion gilt : 1. Ist p eine Primzahl, so ist '(p) = p 1. 2. Sind p, q zwei Primzahlen, so ist '(p · q) = '(p) · '(q) = (p 1) · (q 1). Beweis. Siehe Anhang. Vermutlich ist noch unklar, wieso plötzlich die Phi-Funktion auftritt und warum sie so wichtig ist. Das liegt vor allem an einem Satz, den Euler auch schon bewiesen hat : Satz. Sind a, m 2 N zwei teilerfremde Zahlen, so ist a'(m) ⌘ 1 (mod m). Beweis. Siehe Anhang. Machen wir uns diesen Satz an einem Beispiel klar: Beispiel. Es sind a = 7 und m = 10 zwei teilerfremde Zahlen. Es ist '(m) = '(10) = 4 und dann gilt laut Satz : 74 ⌘ 1 (mod 10) ohne, dass wir es nachrechnen müssten. Trotzdem gerechnet : 74 = 2401 ⌘ 1 (mod 10). Mit Hilfe dieses Satzes kann man oft große Potenzen schnell berechnen : Beispiel. Es sind a = 120 und m = 11 · 19 = 209 wieder zwei teilerfremde Zahlen. Dann ist '(209) = '(11 · 19) = 10 · 18 = 180 und mit dem Euler’schen Satz ergibt sich : 120180 ⌘ 1 (mod 209). Da 120180 eine 375-stellige Zahl ist, verzichten wir hier großzügig auf das Nachrechnen. Das RSA-Verfahren Nach diesen etwas trockenen Regeln zur Phi-Funktion können wir jetzt beschreiben, wie es funktioniert. Jeder Teilnehmer benötigt einen geheimen und einen öffentlichen Schlüssel. Wie geht also Alice vor, wenn sie am RSA-Verfahren teilnehmen möchte? a) Sie wählt zwei verschiedene Primzahlen p, q und berechnet n = p · q. b) Sie berechnet '(n) = '(p · q) = (p 1) · (q 1). c) Jetzt wählt sie eine beliebige Zahl e , die zu '(n) teilerfremd ist. d) Sie berechnet das Inverse von e modulo '(n), d.h. eine Zahl d mit der Eigenschaft : e · d ⌘ 1 (mod '(n)). e) Die Zahlen p, q, '(n) werden nicht mehr benötigt und vernichtet. Der geheime Schlüssel ist die Zahl d aus Schritt 4. Der öffentliche Schlüssel ist das Zahlenpaar e, n und kann von ihr allen mitgeteilt werden. Beispiel. Wir durchlaufen die einzelnen Schritte mit kleinen, übersichtlichen Zahlen : a) Wir wählen p = 11, q = 13 und erhalten n = 143. b) Es ist '(143) = '(11 · 13) = 10 · 12 = 120. c) Beim dritten Schritt entscheiden wir uns für e = 77 . d) Hier suchen wir nach einer Zahl d mit 77 · d ⌘ 1 (mod 120), also das Inverse zu 77 modulo 120. Im Abschnitt 5.4 haben wir gelernt, dass es ein solches Inverses gibt, denn 77 und 120 sind teilerfremd. ( Das erklärt nebenbei auch, warum die Zahl e teilerfremd zu '(n) sein muss. Mit ein wenig Gerechne erhalten wir : d = 53 e) Wir veröffentlichen e = 77, n = 143 und bewahren d = 53 an einem guten Versteck bei uns auf. 78 5 Kryptologie So weit, so gut, aber was fängt jetzt jemand mit unseren veröffentlichten Zahlen e = 77, n = 143 an? Nehmen wir an, dass uns unsere Bank die PIN 6983 für eine neue Bankkarte schicken will. Dazu geht sie folgendermaßen vor : a) Das von uns veröffentlichte Paar e, n wird herausgesucht. b) Die zu sendende Zahl 6983 wird der Reihe nach in Zahlen n zerlegt. Hier bietet sich die Aufteilung in 69 und 83 an. c) Die Bank berechnet nun c1 = 69e = 6977 ⌘ 75 (mod n) und schickt c1 an uns. Für die zweite Zahl der Reihe wird entsprechend gerechnet c2 = 83e = 8377 ⌘ 96 (mod n) und c2 gesendet. Man erkennt, dass beide öffentlichen Zahlen für das Berechnen benutzt werden. Nachricht an Alice Alice Klartext : 6983 Aufspalten in Zahlen pr i d= vat 53 n e= n= 77, 14 3 69, 83 c1=6977=75 (mod 143) c2=8377=96 (mod 143) c1,c2 schicken Abbildung 5.17: Nachricht an Alice senden Nach kurzer Zeit trudeln also bei Alice die Zahlen 75 und 96 ein. Wie entschlüsselt sie diese Zahlen/Nachricht? Jetzt schlägt die Stunde des privaten Schlüssels. Niemand außer ihr kennt ja d = 53 und das einzige, was sie tun muss ist ihrerseits wieder etwas zu rechnen. Sie potenziert die erhaltenen Zahlen mit d modulo nund kommt auf 7553 ⌘ 69 (mod 143) 9653 ⌘ 83 (mod 143) Durch Aneinanderhängen der einzelnen Abschnitte hat sie die zugeschickte PIN 6983 erfolgreich entschlüsselt. Das Verfahren formulieren wir nochmal als Satz. Satz. (RSA-Verfahren) Es sei (e, n) ein öffentliches Schlüsselpaar und k eine Zahl n als Klartext. Ferner sei c ⌘ k e (mod n) die empfangene Nachricht. Dann gilt : cd ⌘ k (mod n), d.h. das Potenzieren der empfangenen Nachricht mit d führt zurück zum Klartext. Beweis. Siehe Anhang. Das beschriebene RSA-Verfahren ist derart mathematisch aufgebaut, dass man - so scheint es - nur Zahlen hin und herschicken kann. Was ist mit üblichen Texten aus Buchstaben? 79 5 Kryptologie Entschlüsseln Alice erhalten : 75, 96 pr i d= vat 53 m1=75 =69 (mod 143) 53 e= n= 77, 14 3 m2=9653=83 (mod 143) Klartext war: 6983 Abbildung 5.18: Entschlüsseln der Nachricht Zum Versenden von Buchstaben müssen die Zeichen zunächst alle in Zahlen umgewandelt werden. Beim Verwenden eines Computers passiert dies sowieso ( Erinnerung : ASCII-Tabelle ), so dass es keinerlei Probleme bereitet, Texte in Zahlen umzuwandeln und diese dann in entsprechende Einzelzahlen zu zerlegen. Die Sicherheit des Verfahrens Gehen wir nochmal die zwei Schritte durch, die nötig waren, um eine Nachricht vom Sender zum Empfänger zu bringen. Erst musste man mit dem öffentlichen Schlüssel verschlüsseln und die erhaltenen Zahlen versenden. Anschließend wendet Alice ihren privaten Schlüssel darauf an. Betrachte dazu nochmal die Abbildungen 5.17 und 5.18. Ein mithörender Mr.X kann natürlich auch die für Alice bestimmten Zahlen abfangen und den öffentlichen Schlüssel kennt er sowieso. Sollte das nicht reichen, um auf den Klartext zu kommen? Verschlüsseln Mr.X liest die für Alice bestimmten Zahlen 75 und 96 mit. Er weiß auch, dass diese Zahlen durch das Potenzieren von a77 (mod 143) mit irgendwelchen a entstanden sind. Wieder stoßen wir auf das Problem des diskreten Logarithmus, d.h. nach dem heutigen Stand der Kenntnisse kann Mr.X nicht von 75 und 96 auf die Originalzahlen in vernünftiger Zeit zurückrechnen. Entschlüsseln Wenn es Mr.X gelingt auf den geheimen Schlüssel d zu kommen, ist natürlich der Rest ein Kinderspiel. Kann er es mit Hilfe der öffentlichen Zahlen e, n schaffen? Die Zahl d war ja das Inverse von e modulo '(n) und wurde so von Alice berechnet. Auch ein Mr.X kann auf das Inverse d kommen, wenn er '(n) kennt und das wiederum gelingt ihm, wenn er die zwei Primzahlen ,aus denen n besteht, finden kann. Das klingt ja gar nicht so aussichtslos und in der Tat kommt man von n = 143 schnell auf die Faktorisierung n = 143 = 11 · 13. Von da aus lässt sich auf d kommen. Natürlich ist unsere Zahl n hier viel zu klein gewählt, so dass wir es Mr.X zu leicht gemacht haben. Fassen wir zusammen : Die Sicherheit des RSA-Verfahrens ist sofort dahin, wenn jemand von der Zahl n schnell auf die Primfaktoren p und q zurückrechnen kann. Wählt man die Zahl n nur groß genug ( z.B. 300 Stellen ), so gibt es nach heutigem Stand der Kenntnisse nur Verfahren, die alle Trillionen von Jahren brauchen, um n in Primzahlen zu zerlegen. Sollte jedoch morgen irgendwo auf der Welt ein Mathematiker ein neues Verfahren zum Faktorisieren von Primzahlen in vernünftiger Zeit finden, so sind sämtliche RSA-gesichterten Geheimnisse nichts mehr wert. 80 5 Kryptologie 5.7 Signaturen Das in 5.6 besprochene RSA-Verfahren bietet eine weitere Möglichkeit, an die man zunächst nicht denkt. Bei einer Kommunikation über weite Distanzen kann man sich ja nie ganz sicher sein, ob eine erhaltene Nachricht tatsächlich authentisch ist oder ob jemand nur vorgibt eine andere Person zu sein. Angenommen, wir erhalten eine Nachricht, in der ein alter Freund uns um Hilfe bittet und dringend Geld benötigt. Unabhängig davon, ob die Nachricht uns verschlüsselt oder im Klartext erreicht hat, besteht für uns als Empfänger die Frage, ob wir dem Absender trauen können. Es gibt somit bei der Kommunikation noch weitere Probleme als nur das Mitlesen von Unbefugten zu verbieten. Insgesamt gibt es drei wichtige Fragestellungen: • Vertraulichkeit : Wenn mich eine Nachricht erreicht, kann ich sicher sein, dass nur ich sie lesen kann? Ist sie sicher vor Unbefugten geschützt? Diese Frage wurde schon beim RSA-Verfahren ( aber auch beim OneTimePad ) schon positiv beantwortet. • Authentizität : Wenn mich eine Nachricht erreicht ( egal ob verschlüsselt oder unverschlüsselt ), kann ich dann sicher sein, dass sie auch wirklich vom Absender stammt? Könnte sich nicht jemand mit einer falschen Identität ausgestattet haben? Hier geht es also nicht um den Inhalt der Nachricht sondern um das Vertrauen in den Absender. Mr.X als Alice Bob, schick mir 5000 €. Bob ? Abbildung 5.19: Authentischer Absender? • Integrität : Selbst bei korrektem und irgendwie bestätigtem Absender besteht noch die Möglichkeit, dass der Inhalt der Nachricht verändert wurde. Wie kann man sich also sicher sein, dass ein Inhalt auch korrekt beim Empfänger ankommt. Mr.X Alice Bob, ich liebe dich. Bob Bob, ich hasse dich. ? Abbildung 5.20: Nachricht unverändert? Bisher hatten wir ja nur geklärt, dass das RSA-Verfahren eine Nachricht sehr sicher verschlüsseln kann, d.h. die Vertraulichkeit ist auf jeden Fall gegeben. Aber auch für die beiden anderen Probleme bietet das RSA-Verfahren eine Lösung. Signieren von Dokumenten Im Alltag benötigen wichtige Dokumente ( Verträge, Zeugnisse, ärztliche Verschreibungen... ) Unterschriften von einer Person, damit man glaubwürdig nachweisen kann, dass es sich um ein echtes Dokument handelt. Die Unterschrift signiert das Dokument und bietet eine Möglichkeit die Echtheit 81 5 Kryptologie nachzuweisen. Sollten Zweifel an einem Dokument bestehen, so lässt sich eine Unterschrift ein weiteres Mal anfertigen und mit der Unterschrift im Dokument vergleichen. Sogenannte Graphologen sind Spezialisten für das Erkennen von Handschriften. Beim RSA-Verfahren findet die Kommunikation aber über weite Strecken und eventuell rein elektronisch ab. Um einer Person zu trauen, von der man nur eine Email erhalten hat, braucht man eine Art digitale Unterschrift. Der Ansatz besteht darin, den privaten Schlüssel d zu benutzen. Nehmen wir an, dass Alice ihren Namen mit dem privaten Schlüssel d verschlüsselt und diesen verschlüsselten Text an eine gewöhnliche Nachricht ( egal ob verschlüsselt oder unverschlüsselt ) anhängt. Mathematisch hat sie also ihren Namen in eine passende Zahl z umgewandelt ( evtl. auch mehrere Zahlen ) und dann z d (mod n) als Anhang verwendet. Aber wer soll denn diese Nachricht entschlüsseln? Zum Entschlüsseln brauchen wir das Gegenstück zu d , d.h. den öffentlichen Schlüssel und der ist jedem bekannt. Die Nachricht z d kann von einer beliebigen Person durch Potenzieren mit e wieder entschlüsselt werden und es erscheint Alices Name. Da ja nur Alice ihren privaten Schlüssel kennt, konnte die Originalnachricht auch wirklich nur von ihr stammen. Damit gibt es eine Möglichkeit, dass Alice ihre Identität bestätigen kann und das Problem der Authentizität ist (scheinbar) gelöst. Integrität Mr.X schlägt zurück. Er hat erkannt, dass Alice es immer schaffen kann, sich mit einer Signatur digital auszuweisen. Andererseits : Wenn Alice ihren Namen einmal verschlüsselt und verschickt hat, dann kann Mr.X diese Datei abfangen. Entweder kann er jetzt selber unter Alice Namen schreiben oder er ändert den Text einer von Alice verschickten Nachricht und setzt wieder ihren Namen darunter. Ein anderes Beispiel aus dem Computeralltag zeigt, dass man nicht nur beim Schreiben von Nachrichten auf das Problem stößt, ob eine Nachricht noch unverändert ist oder nicht : Nehmen wir an, dass wir X eine bestimmte Software zum Entpacken von ZIP-Dateien benötigen. Wir suchen nach einem passenden Programm und begeben uns per Browser zum Hersteller der Software. Die Seite sieht seriös aus ( Authentizität per Auge? ) und auch der Download startet schnell und gut. Bei der Installation klicken wir fünfmal auf “Ja” und haben die Software rasch installiert. Was wir nicht ahnen konnten : Ein Fiesling ( Mr.X schlägt zurück ) hat in der Nacht auf dem Download-Server der Firma die offizielle Version durch eine von ihm geänderte Variante ausgetauscht. Diese haben wir heruntergeladen und jetzt leider diverse Schnüffel-Software mitinstalliert. Vielleicht hat diese alle unsere Tastatureingaben mitgeschrieben und verschickt. Dann könnten alle unsere Passwörter, PIN-Nummern bei Online-Banking und vieles andere hinüber sein. Allgemein stellt sich die Frage : Ist eine Nachricht / Datei bei Bob noch die, die Alice verschicken wollte? Hashwerte Ein Ansatz, um Integrität einer Nachricht oder einer Datei zu erhalten, besteht darin, für eine Datei eine eindeutige Buchstaben/Zahlenfolge zu bestimmen. Diese Folge wird Prüfsumme, Checksum oder auch Hashwert genannt und hat möglichst folgende Eigenschaften. • Der Hashwert einer Datei ist eindeutig, d.h. zu einer Datei erzeugt das Hashverfahren immer den gleichen Hashwert. • Umgekehrt sollten zwei verschiedene Dateien auch zwei verschiedene Hashwerte erzeugen. Schon kleinste Änderungen führen zu einem anderen Hashwert. ( Entsteht bei unterschiedlichen Dateien dennoch der gleiche Hashwert, so spricht man von einer Hash-Kollision. ) • Das Berechnen des Hashwerts nimmt wenig Zeit in Anspruch. • Der Hashwert ist deutlich kürzer als die eigentliche Datei. 82 5 Kryptologie Ein bekanntes Verfahren ( neben vielen anderen ) ist MD5 ( Message Digest ) und wurde 1991 von Roland Rivest ( ja, genau der aus dem RSA-Verfahren ) entworfen7 . Zu einer Datei oder einem Text erzeugt das Verfahren eine 128-Bitfolge. Diese wird in 32x4 Bit unterteilt und die 4 Bits ( 0000 bis 1111 ) durch hexadezimale Zahlen dargestellt. Die Abbildung zeigt, dass schon geringste Änderungen zu einem drastisch anderen Hashwert führen. Ein Hersteller von Software geht folgendermaßen vor : Text MD5-Hash MD5 in Aktion 8dfb81100da240537405359bcb047b22 MD5 in Action 370774df9b870908c155267bdeed2019 Im Garten ist ein Hirsch. 9bb4572bef475375289f910244ce4fb9 Im Garten ist ein Harsch. c75f61271695a9f1537e92768d9fc35e Abbildung 5.21: MD5-Hashwerte a) Erstelle für die fertige Installationsdatei einen Hashwert. b) Verschlüssele diesen Hashwert mit dem privaten Schlüssel. c) Sende den verschlüsselten Hashwert bzw. stelle ihn öffentlich zur Verfügung. Bob als Empfänger hat dann den verschlüsselten Hashwert und kann ihn mit dem öffentlichen Schlüssel von Alice entschlüsseln. Heraus kommt der ursprüngliche Hashwert. Diesen kann er dann mit dem Hashwert seiner erhaltenen Datei vergleichen. Wenn er auf die gleichen Hashwerte kommt, dann war die Datei wirklich von Alice ( sonst hätte das Entschlüsseln nicht geklappt ) und die Datei war unverändert ( sonst wären die Hashwerte unterschiedlich ). 7 Seit dem Jahr 2004 gilt es nicht mehr als sicher, da chinesische Forscher eine Möglichkeit gefunden haben, wie sie gezielt zwei verschiedene Dateien erzeugen können, mit dem gleichen Hashwert. 83 Anhang ( noch unfertig ) A1 : Das Quicksort-Verfahren Im Laufe der Zeit haben sich sehr viele Ideen zum Sortieren von Daten gebildet. Ein Klassiker des Sortierens ist das QuicksortVerfahren, das hier betrachtet und zumindest im Ansatz auch analysiert wird. Quicksort wurde 1962 vom britischen Informatiker Charles Antony Hoare erfunden und verwendet ein rekursives Verfahren zum Sortieren von Daten. Die vorhandene Menge an unsortierten Elementen wird dabei in kleinere Teilbereiche zerlegt, die wieder in Teilbereiche zerlegt werden, usw. Quicksort gehört damit zu den Algorithmen vom Typ „Divide and Conquer“, d.h. ein Problem wird in kleinere Teilprobleme zerlegt ( Divide ), die dann einzeln gelöst werden ( Conquer ) und aus allen zusammen erhält man die Gesamtlösung. Abbildung 5.22: Charles Hoare Die grobe Idee des Verfahrens besteht darin, sich zunächst ein beliebiges Element der Liste zu wählen (sehr oft einfach das Element in der Mitte, aber die Wahl ist vom Prinzip her egal). Dies nennt man das Vergleichselement. Der nun folgende Ablauf hat zum Ziel die restlichen Bestandteile der Liste so zu verteilen, dass links vom Vergleichselement nur kleinere Elemente und rechts vom Vergleichselement nur größere Elemente zu finden sind. Dadurch ist die Liste nicht zwangsläufig sortiert aber zumindest vorsortiert. Betrachten wir das Verfahren schrittweise an einem grafischen Beispiel, bei dem wir einfach Balken verwenden, die der Größe nach sortiert werden sollen : a) Ausgangspunkt ist eine Anzahl von unsortierten Elementen. Von diesen wählt man ein Vergleichselement (hier das mittlere Element) und setzt zwei Markierungen (L und R) an den linken und rechten Rand. In diesem Fall ist dann VElement = 8 . b) Da links vom VElement nur kleinere Elemente stehen sollen, verschiebt man die linke Marke weiter nach rechts bis man ein Element findet, das größer ist als das VElement. Dies steht dann gewissermaßen „auf der falschen Seite“. Auf die gleiche Weise wird die rechte Marke nach links geschoben, bis man ein Element findet, das kleiner als das VElement ist. (in diesem Fall muss R gar nicht verschoben werden, da schon das Element ganz rechts kleiner als das VElement ist) c) Auf beiden Seiten hat man an den Stellen L und R zwei Elemente gefunden, die „auf der falschen Seite“ stehen. Diese beiden Elemente werden direkt miteinander vertauscht. d) Nach dem Tausch können die beiden Markierungen L und R weiter bewegt werden, da ja dann die beiden Elemente an den bisherigen Positionen auf jeden Fall richtig positioniert sind. e) Wie in Schritt 2 wird die linke Marke weiterbewegt bis man eine Zahl anschließend die rechte Marke bis man eine Zahl VElement findet. 84 VElement findet und 5 Kryptologie f) Nach einem weiteren Tausch und anschließendem Weiterbewegen von L und R kommt man zu der Situation, dass sich die beiden Markierungen L und R aneinander vorbeibewegt haben. Damit ist das Feld vorsortiert ( links vom VElement nur die kleineren, rechts die größeren Zahlen ) aber noch nicht komplett sortiert. g) Jetzt kann man das Verfahren rekursiv auf die beiden erzeugten Teilbereiche erneut anwenden, d.h. zuerst auf die Zahlen links vom VElement und anschließend auf die Zahlen rechts vom VElement. A2 : Lindenmayer-Systeme Die in Kapitel vorgestellten Grammatiken erlaubten es nach vorgegebenen Regeln ( Produktionen ) Wörter abzuleiten. Diese Vorgehensweise nutzte der ungarische Biologe Aristid Lindenmayer in einem ganz anderen Kontext. Sein Ziel war es eine theoretische Grundlage für Entwicklung von Lebewesen zu finden. So zeigen Rotalgen eine Art von Wachstum, bei dem Seitenstränge entstehen, in denen wieder neue Stränge wachsen. Dadurch besitzt die Rotalge eine Vielzahl an Verzweigungen mit kleiner werden Verästelungen. Abbildung 5.23: Rotalgen Lindenmayer entschied sich das Wachstum der Alge mit Hilfe einer Grammatik zu beschreiben. a b c b b b b e b b d g f e d a b b h g d b b f f e e d d Abbildung 5.24: Erste Schritte beim Wachstum Lindenmayers Ideen ( oft L-Systeme genannt ) erinnern stark an die Definition der Grammatiken in Kapitel 3 : Definition. Ein L-System ist eine endliche Menge an Zeichen, einer endlichen Menge an Produktionen, einer Startregel ( Axiom genannt ). 85 5 Kryptologie Der polnische Informatiker Prezemyslaw Prusinkiewicz erweiterte die Ideen Lindenmayers und benutzte die abstrakten Grammatikregeln als konkrete Anweisungen zum Zeichnen. A3 : Einzelne Beweise von zahlentheoretischen Sätzen Im Kapitel 5 wurden zur Hinführung des RSA-Systems verschiedene mathematische Sätze erwähnt ohne dass sie durch einen Beweis begründet wurden. Dieser Anhang zitiert noch einmal die Sätze und beweist sie auch. Satz. Es seien a, b zwei positive ganze Zahlen und m 2 N der bekannte Modul. Dann gilt : 1. Die lineare Kongruenz a · x ⌘ b (mod m) besitzt genau dann mindestens eine Lösung, wenn ggT (a, m) ein Teiler von b ist. m m 2. Ist x0 eine Lösung der Kongruenz, so sind x0 + ggT , x0 + 2 ggT , . . . weitere Lösungen. Die Anzahl der Lösungen ist gleich dem ggT . Beweis. Der Beweis basiert im Wesentlichen auf der Darstellung des ggT mit Hilfe des erweiterten Euklidischen Algorithmus. Nennen wir also d = ggT (a, m) und beweisen die beiden Richtungen. Richtung 1 : Hat die lineare Kongruenz die Lösung x0 , so ist a · x0 ⌘ b (mod m), d.h. es existiert ein k 2 N mit ax0 = b + km. Daraus folgt b = ax0 km . Die Zahl d als ggT teilt sowohl a als auch m und ist damit auch ein Teiler von ax0 km = b. Richtung 2 : Ist d ein Teiler von b, so ist b = r · d. Wie bekannt lässt sich d auch mit Hilfe von a und m darstellen. Es sei d = n1 a + n2 m mit geeigneten Zahlen n1 , n2 . Multiplizieren wir die Zeile mit r , so folgt : b = r · d = r · (n1 a + n2 m) = rn1 a + rn2 m und daraus folgt : b ⌘ rn1 a (mod m) und x0 = rn1 ist eine Lösung der Kongruenz. Fall 1. () (rn1 )a ⌘ b (mod m) Wenn die lineare Kongruenz Satz. (RSA-Verfahren) Es sei (e, n) ein öffentliches Schlüsselpaar und k eine Zahl n als Klartext. Ferner sei c ⌘ k e (mod n) die empfangene Nachricht. Dann gilt : cd ⌘ k (mod n), d.h. das Potenzieren der empfangenen Nachricht mit d führt zurück zum Klartext. Beweis. Da ja e und d Inverse bezüglich '(n) sind, gilt e · d ⌘ 1 (mod '(n)). Anders ausgedrückt heißt diese Zeile ja, dass e · d = 1 + x · '(n) ist, d.h. ein Vielfaches von '(n) plus 1. Dann ergibt sich : ⇣ ⌘x cd = (k e )d = k e·d = k 1+x·'(n) = k · k x·'(n) = k · k '(n) Nach dem Satz von Euler über die Phi-Funktion ist aber k 86 Index E Endlosschleifen, 19 G Game of Life, 32 H Halteproblem, 21 K Komplexität eines Algorithmus, 12 O Ordnung O, 12 P Problem des Handlungsreisenden, 17 Q Quicksort-Verfahren, 84 T Traveling Salesman Problem, 17 Z Zyklische zelluläre Automaten, 35 87