informatik grundkurs teil B skript

Werbung
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
Herunterladen