pdf - 1572k

Transcription

pdf - 1572k
Einführung in die Informatik
Benjamin Gufler
Erstellt mit LATEX
II
Inhaltsverzeichnis
I
1
1 Information, ihre Repräsentation und Verarbeitung
1.1 Information in der Informatik . . . . . . . . . . . . . . . . . . . . . .
1.2 Modellbildung in der Informatik . . . . . . . . . . . . . . . . . . . .
1.3 Informationsrepräsentationssysteme . . . . . . . . . . . . . . . . . .
1.4 Aussagen als Beispiel für Informationen . . . . . . . . . . . . . . . .
1.4.1 Boolesche Terme . . . . . . . . . . . . . . . . . . . . . . . . .
1.4.2 Boolesche Algebra der Wahrheitswerte . . . . . . . . . . . . .
1.4.3 Interpretation boolescher Terme . . . . . . . . . . . . . . . .
1.4.4 Gesetze der booleschen Algebra . . . . . . . . . . . . . . . . .
1.4.5 Anwendung der Gesetze der booleschen Algebra . . . . . . .
1.5 Information und Repräsentation: Normalform . . . . . . . . . . . . .
1.5.1 Umformung von Repräsentation als Informationsverarbeitung
1.5.2 Zeichenfolgen: Wörter über einem Alphabet . . . . . . . . . .
3
3
3
4
5
5
6
7
8
9
10
10
10
2 Algorithmen und Rechenstrukturen
2.1 Der Begriff Algorithmus . . . . . . . . . . . . . . . . . . . . .
2.1.1 Textersetzungsalgorithmen . . . . . . . . . . . . . . .
2.1.2 Deterministische Textersetzung . . . . . . . . . . . . .
2.1.3 Durch Textersetzungssysteme induzierte Abbildungen
2.2 Rechenstrukturen . . . . . . . . . . . . . . . . . . . . . . . . .
2.2.1 Funktionen und Mengen als Rechenstrukturen . . . .
2.2.2 Signaturen . . . . . . . . . . . . . . . . . . . . . . . .
2.2.3 Grundterme . . . . . . . . . . . . . . . . . . . . . . . .
2.2.4 Terme mit Identifikatoren . . . . . . . . . . . . . . . .
2.3 Algorithmen als Termersetzungssysteme . . . . . . . . . . . .
2.3.1 Termersetzung . . . . . . . . . . . . . . . . . . . . . .
2.3.2 Termersetzungssysteme . . . . . . . . . . . . . . . . .
2.3.3 Korrektheit von TES . . . . . . . . . . . . . . . . . . .
2.4 Aussagenlogik und Prädikatenlogik . . . . . . . . . . . . . . .
2.4.1 Aussagenlogik . . . . . . . . . . . . . . . . . . . . . . .
2.4.2 Prädikatenlogik . . . . . . . . . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
13
13
14
15
16
16
16
18
19
20
22
22
22
23
24
25
26
3 Programmiersprachen
3.1 Syntax - BNF . . . . . . . . . . . . . . . . .
3.1.1 BNF . . . . . . . . . . . . . . . . . .
3.1.2 Syntaxdiagramme . . . . . . . . . .
3.1.3 Kontextbedingungen . . . . . . . . .
3.2 Semantik . . . . . . . . . . . . . . . . . . .
3.3 Implementierung von Programmiersprachen
3.4 Methodik der Programmierung . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
29
29
30
32
32
32
33
33
III
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
IV
INHALTSVERZEICHNIS
4 Applikative / funktionale Sprachen
4.1 Rein applikative Sprachen . . . . . . . . . . . . . . . . . . . . . . . .
4.1.1 Syntax von Ausdrücken . . . . . . . . . . . . . . . . . . . . .
4.1.2 Bedeutung von Ausdrücken . . . . . . . . . . . . . . . . . . .
4.1.3 Konstanten und Identifikatoren . . . . . . . . . . . . . . . . .
4.1.4 Bedingte Ausdrücke . . . . . . . . . . . . . . . . . . . . . . .
4.1.5 Funktionsapplikation . . . . . . . . . . . . . . . . . . . . . . .
4.1.6 Funktionsabstraktion . . . . . . . . . . . . . . . . . . . . . . .
4.2 Deklarationen in applikativen / funktionalen Sprachen . . . . . . . .
4.2.1 Elementdeklaration . . . . . . . . . . . . . . . . . . . . . . . .
4.2.2 Funktionsdeklaration . . . . . . . . . . . . . . . . . . . . . . .
4.3 Rekursive Funktionsdeklarationen . . . . . . . . . . . . . . . . . . . .
4.3.1 Induktive Deutung rekursiver Funktionsdeklarationen . . . .
4.3.2 Deutung rekursiv deklarierter Funktionen durch kleinste Fixpunkte . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4.4 Rekursionsformen . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4.4.1 Lineare Rekursion . . . . . . . . . . . . . . . . . . . . . . . .
4.4.2 Repetitive Rekursion . . . . . . . . . . . . . . . . . . . . . . .
4.4.3 Kaskadenartige Rekursion . . . . . . . . . . . . . . . . . . . .
4.4.4 Vernestete Rekursion . . . . . . . . . . . . . . . . . . . . . . .
4.4.5 Verschränkte Rekursion . . . . . . . . . . . . . . . . . . . . .
4.5 Techniken applikativer Programmierung . . . . . . . . . . . . . . . .
4.5.1 Spezifikation der Aufgabenstellung . . . . . . . . . . . . . . .
4.5.2 Strukturierung und Entwurf . . . . . . . . . . . . . . . . . . .
4.5.3 Hinweise zur Strukturierung der Aufgabenstellung und der
Lösung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4.5.4 Rekursion und Spezifikation . . . . . . . . . . . . . . . . . . .
4.5.5 Parameterunterdrückung . . . . . . . . . . . . . . . . . . . .
4.5.6 Effizienz . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4.5.7 Dokumentation . . . . . . . . . . . . . . . . . . . . . . . . . .
4.5.8 Test / Integration von Programmen . . . . . . . . . . . . . .
4.6 Korrektheitsbeweise . . . . . . . . . . . . . . . . . . . . . . . . . . .
4.6.1 Induktion und Rekursion . . . . . . . . . . . . . . . . . . . .
4.6.2 Terminierungsbeweise . . . . . . . . . . . . . . . . . . . . . .
35
35
35
36
38
38
40
40
43
43
44
44
47
5 Zuweisungsorientierte Programmierung
5.1 Anweisungen . . . . . . . . . . . . . . . . . . . . . . . . .
5.1.1 Syntax . . . . . . . . . . . . . . . . . . . . . . . . .
5.1.2 Programmvariable und Zuweisungen . . . . . . . .
5.1.3 Zustände . . . . . . . . . . . . . . . . . . . . . . .
5.1.4 Funktionale Bedeutung von Anweisungen . . . . .
5.1.5 Operationelle Bedeutung von Anweisungen . . . .
5.2 Einfache Anweisungen . . . . . . . . . . . . . . . . . . . .
5.3 Zusammengesetzte Anweisungen . . . . . . . . . . . . . .
5.3.1 Sequentielle Komposition . . . . . . . . . . . . . .
5.3.2 Bedingte Anweisung . . . . . . . . . . . . . . . . .
5.3.3 Wiederholungsanweisung . . . . . . . . . . . . . .
5.4 Variablendeklarationen und Blöcke . . . . . . . . . . . . .
5.5 Prozeduren . . . . . . . . . . . . . . . . . . . . . . . . . .
5.5.1 Prozedurdeklaration . . . . . . . . . . . . . . . . .
5.5.2 Prozeduraufruf . . . . . . . . . . . . . . . . . . . .
5.5.3 Globale Programmvariable in Prozeduren . . . . .
5.5.4 Rekursion . . . . . . . . . . . . . . . . . . . . . . .
5.6 Block und Abschnitt, Bindung: Gültigkeit / Lebensdauer
63
63
63
64
64
64
64
65
65
65
66
67
68
69
69
70
71
71
72
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
49
49
49
50
50
51
52
52
52
54
55
56
56
56
58
58
58
59
60
INHALTSVERZEICHNIS
5.7
V
Programmtechniken für Zuweisungen . . . . . . . . . . . . . . . . . .
73
6 Sortendeklarationen
6.1 Deklaration von Sorten . . . . . . . . .
6.1.1 Skalare durch Enumeration . .
6.1.2 Direktes Produkt / Tupelsorten
6.1.3 Direkte Summe / Varianten . .
6.1.4 Teilbereichsorten . . . . . . . .
6.2 Felder . . . . . . . . . . . . . . . . . .
6.2.1 Einstufige Felder . . . . . . . .
6.2.2 Felder und selektives Ändern .
6.2.3 Mehrstufige Felder . . . . . . .
6.3 Endliche Mengen . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
77
77
77
78
79
80
80
81
81
82
83
7 Sprünge und Referenzen
7.1 Kontrollfluss . . . . . . . . . .
7.1.1 Marken und Sprünge . .
7.1.2 Kontrollflussdiagramme
7.2 Referenzen und Zeiger . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
85
85
85
86
86
8 Rekursive Sorten
8.1 Sequenzartige Rechenstrukturen . . . . . . . . . . . . . . . .
8.1.1 Rechenstruktur der Sequenzen . . . . . . . . . . . . .
8.1.2 Stack . . . . . . . . . . . . . . . . . . . . . . . . . . .
8.1.3 Warteschlangen (Queue) . . . . . . . . . . . . . . . . .
8.1.4 Dateien . . . . . . . . . . . . . . . . . . . . . . . . . .
8.2 Baumartige Rechenstrukturen . . . . . . . . . . . . . . . . . .
8.3 Rekursive Sortenvereinbarungen . . . . . . . . . . . . . . . . .
8.3.1 Verwendung von Rekursion für Sortenvereinbarungen
8.4 Geflechte . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
8.4.1 Einfache Geflechte . . . . . . . . . . . . . . . . . . . .
8.4.2 Geflechte über rekursiv vereinbarten Sorten . . . . . .
8.4.3 Verkettete Listen . . . . . . . . . . . . . . . . . . . . .
8.4.4 Zweifach verkettete Listen . . . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
89
89
89
89
91
91
91
95
96
96
97
97
98
98
9 Objektorientiertes Programmieren
9.1 Klassen und Objekte . . . . . . . . . . . . . .
9.1.1 Klassen . . . . . . . . . . . . . . . . .
9.1.2 Objekte . . . . . . . . . . . . . . . . .
9.2 Vererbung . . . . . . . . . . . . . . . . . . . .
9.2.1 Vererbungsbeziehung . . . . . . . . . .
9.2.2 Bemerkungen zur Objektorientierung .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
101
101
102
103
104
104
106
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
II Rechnerstruktur, Hardware, Maschinennahe Programmierung
107
1 Codierung / Informationstheorie
1.1 Codes / Codierung . . . . . . . . . . .
1.1.1 Binärcodes einheitlicher Länge
1.1.2 Codes variabler Länge . . . . .
1.1.3 Serien-/ Parallelwortcodierung
1.1.4 Codebäume . . . . . . . . . . .
1.2 Codes und Entscheidungsinformation .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
111
111
111
112
113
113
114
VI
INHALTSVERZEICHNIS
1.3
Sicherung von Nachrichtenübertragung . . . . . . . . . . . . . . . . . 115
1.3.1 Codesicherung . . . . . . . . . . . . . . . . . . . . . . . . . . 115
1.3.2 Übertragungssicherheit . . . . . . . . . . . . . . . . . . . . . . 116
2 Binäre Schaltnetze und Schaltwerke
2.1 Boolesche Algebra / Boolesche Funktionen . . . . . . . .
2.1.1 Boolesche Funktionen . . . . . . . . . . . . . . .
2.1.2 Partielle Ordnung auf BF . . . . . . . . . . . . .
2.2 Normalformen boolescher Funktionen . . . . . . . . . . .
2.2.1 Das boolesche Normalformtheorem . . . . . . . .
2.2.2 Vereinfachte Normalformen (DNF) . . . . . . . .
2.3 Schaltnetze . . . . . . . . . . . . . . . . . . . . . . . . .
2.3.1 Schaltfunktionen und Schaltnetze . . . . . . . . .
2.3.2 Darstellung von Schaltnetzen . . . . . . . . . . .
2.3.3 Halbaddierer . . . . . . . . . . . . . . . . . . . .
2.3.4 Arithmetische Schaltnetze . . . . . . . . . . . . .
2.3.5 Zahldarstellung . . . . . . . . . . . . . . . . . . .
2.3.6 Weitere arithmetische Operationen . . . . . . . .
2.3.7 Schaltnetze zur Übertragung von Information . .
2.4 Schaltwerke . . . . . . . . . . . . . . . . . . . . . . . . .
2.4.1 Schaltwerksfunktionen . . . . . . . . . . . . . . .
2.4.2 Schaltfunktionen als Schaltwerksfunktionen . . .
2.4.3 Schaltwerke . . . . . . . . . . . . . . . . . . . . .
2.4.4 Schaltwerksfunktionen und endliche Automaten .
2.4.5 Schaltwerke zum Speichern: Verzögerungsnetze .
2.4.6 Klassen von Schaltwerken . . . . . . . . . . . . .
2.4.7 Komposition von Schaltwerken und Schaltnetzen
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
117
117
117
119
119
119
120
121
121
122
124
126
127
130
130
131
131
132
132
132
133
134
135
3 Aufbau von Rechenanlagen
3.1 Strukturierter Aufbau von Rechenanlagen . . . . . .
3.1.1 Der Rechnerkern . . . . . . . . . . . . . . . .
3.1.2 Speichereinheit . . . . . . . . . . . . . . . . .
3.1.3 E/A . . . . . . . . . . . . . . . . . . . . . . .
3.1.4 Befehle und Daten auf Maschinenebene . . .
3.1.5 Operandenspezifikation und Adressrechnung .
3.1.6 Der Befehlszyklus . . . . . . . . . . . . . . .
3.2 Hardwarekomponenten . . . . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
137
137
139
139
139
139
141
143
144
4 Maschinennahe Programmierung
4.1 Maschinennahe Programmiersprachen . . . . . . . . .
4.1.1 Binärwörter als Befehle . . . . . . . . . . . . .
4.1.2 Der Befehlsvorrat der MI . . . . . . . . . . . .
4.1.3 Einfache Maschinenprogramme . . . . . . . . .
4.1.4 Assemblersprachen . . . . . . . . . . . . . . . .
4.1.5 Ein- und Mehradressform . . . . . . . . . . . .
4.1.6 Unterprogrammtechniken . . . . . . . . . . . .
4.2 Adressiertechniken und Speicherverwaltung . . . . . .
4.2.1 Konstante . . . . . . . . . . . . . . . . . . . . .
4.2.2 Operandenversorgung über Register . . . . . .
4.2.3 Absolute Adressierung . . . . . . . . . . . . . .
4.2.4 Relative Adressierung . . . . . . . . . . . . . .
4.2.5 Indizierung, Zugriff auf Felder . . . . . . . . . .
4.2.6 Symbolische Adressierung . . . . . . . . . . . .
4.2.7 Geflechtsstrukturen und indirekte Adressierung
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
145
145
146
146
147
149
150
151
153
153
154
154
154
154
155
156
INHALTSVERZEICHNIS
4.3
III
4.2.8 Speicherverwaltung . . . . . . . . . . . . . . . . . .
4.2.9 Stackverwaltung von blockstrukturierten Sprachen
Techniken maschinennaher Programmierung . . . . . . . .
4.3.1 Auswertung von Ausdrücken / Termen . . . . . . .
4.3.2 Maschinennahe Realisierung von Ablaufstrukturen
VII
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
Systeme und systemnahe Programmierung
158
158
160
161
162
165
1 Prozesse, Interaktion, Koordination in verteilten Systemen
1.1 Prozesse . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.1.1 Aktionsstrukturen als Prozesse . . . . . . . . . . . . . . . .
1.1.2 Strukturierung von Prozessen . . . . . . . . . . . . . . . . .
1.1.3 Sequentielle Prozesse und Spuren . . . . . . . . . . . . . . .
1.1.4 Zerlegen von Prozessen in Teilprozesse . . . . . . . . . . . .
1.1.5 Aktionen als Zustandsübergänge . . . . . . . . . . . . . . .
1.2 Systembeschreibungen durch Mengen von Prozessen . . . . . . . .
1.2.1 Petri-Netze . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.2.2 Prozessalgebra . . . . . . . . . . . . . . . . . . . . . . . . .
1.2.3 Synchronisation und Koordination von Agenten . . . . . . .
1.2.4 Beschreibung von Prozessen mit Prädikaten . . . . . . . . .
1.3 Programmiersprachen zur Beschreibung. . . . . . . . . . . . . . . . .
1.3.1 Parallelität in anweisungsorientierten Programmiersprachen
1.3.2 Kommunikation durch Nachrichtenaustausch . . . . . . . .
1.3.3 Nachrichtenaustausch über gemeinsame Variablen . . . . .
1.3.4 Sprachmittel zum Erzeugen paralleler Abläufe . . . . . . . .
1.3.5 Nebenläufigkeit in Java . . . . . . . . . . . . . . . . . . . .
Thread-Erzeugung in Java . . . . . . . . . . . . . . . . . . .
Synchronisation in Java . . . . . . . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
169
169
169
171
172
173
174
176
177
181
183
184
186
186
188
193
198
201
202
202
2 Betriebssysteme und systemnahe Programmierung
2.1 Grundlegende Betriebssystem – Aspekte . . . . . . . . . .
2.1.1 Aufgaben eines Betriebssystems . . . . . . . . . . .
2.1.2 Betriebsarten . . . . . . . . . . . . . . . . . . . . .
2.1.3 Ein einfaches Betriebssystem für den Stapelbetrieb
2.1.4 Ein einfaches Betriebssystem für Multiplexbetrieb
2.2 Benutzerrelevante Aspekte eines Betriebssystems . . . . .
2.2.1 Kommandosprache des Betriebssystems . . . . . .
2.2.2 Benutzerverwaltung . . . . . . . . . . . . . . . . .
2.2.3 Zugriff auf Rechenleistung . . . . . . . . . . . . . .
2.2.4 Dateiorganisation und Verwaltung . . . . . . . . .
2.2.5 Übertragungsdienste . . . . . . . . . . . . . . . . .
Das Client – Server – Modell . . . . . . . . . . . .
Middleware . . . . . . . . . . . . . . . . . . . . . .
2.2.6 Zuverlässigkeit und Schutzaspekte . . . . . . . . .
2.3 Betriebsmittelverteilung . . . . . . . . . . . . . . . . . . .
2.3.1 Prozessorvergabe . . . . . . . . . . . . . . . . . . .
2.3.2 Hauptspeicherverwaltung . . . . . . . . . . . . . .
2.3.3 Betriebsmittelvergabe im Mehrprogrammbetrieb .
2.3.4 Zuteilung der E/A – Geräte . . . . . . . . . . . . .
2.4 Techniken der Systemprogrammierung . . . . . . . . . . .
2.4.1 Unterbrechungskonzept . . . . . . . . . . . . . . .
2.4.2 Koordination und Synchronisation . . . . . . . . .
2.4.3 Segmentierung . . . . . . . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
205
206
206
209
210
212
214
215
215
215
215
217
218
218
220
220
220
221
222
222
223
223
225
227
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
VIII
2.5
INHALTSVERZEICHNIS
2.4.4 Seitenaustauschverfahren . . . . . . . . . . . . .
2.4.5 Verschiebbarkeit von Programmen . . . . . . . .
2.4.6 Simultane Benutzbarkeit von Unterprogrammen
2.4.7 Steuerung von E/A – Geräten . . . . . . . . . . .
2.4.8 Kommunikationsdienste . . . . . . . . . . . . . .
Betriebssystem – Strukturen . . . . . . . . . . . . . . . .
3 Interpretation und Übersetzung
3.1 Lexikalische Analyse . . . . . .
3.2 Zerteilen von Programmen . . .
3.3 Kontextbedingungen . . . . . .
3.4 Semantische Behandlung . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
228
228
229
230
232
235
von Programmiersprachen
. . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
239
241
242
243
243
IV
245
1 Formale Sprachen
1.1 Relation, Graphen . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.1.1 Zweistellige Relationen . . . . . . . . . . . . . . . . . . . . . .
1.1.2 Wege in Graphen, Hüllenbildung . . . . . . . . . . . . . . . .
1.2 Grammatiken . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.2.1 Reduktive und generative Grammatiken . . . . . . . . . . . .
1.2.2 Die Sprachhierarchie nach Chomsky . . . . . . . . . . . . . .
1.2.3 Strukturelle Äquivalenz von Ableitungen . . . . . . . . . . . .
1.2.4 Sackgassen, unendliche Ableitungen . . . . . . . . . . . . . .
1.3 Chomsky – 3 – Sprachen, endliche Automaten, reguläre Ausdrücke .
1.3.1 Reguläre Ausdrücke . . . . . . . . . . . . . . . . . . . . . . .
1.3.2 Endliche Automaten . . . . . . . . . . . . . . . . . . . . . . .
1.3.3 Äquivalenz der Darstellungsformen . . . . . . . . . . . . . . .
1.3.4 Äquivalenz von regulären Ausdrücken, endlichen Automaten
und Chomsky – 3 – Grammatiken . . . . . . . . . . . . . . .
1.3.5 Minimale Automaten . . . . . . . . . . . . . . . . . . . . . .
1.4 Kontextfreie Sprachen und Kellerautomaten . . . . . . . . . . . . . .
1.4.1 BNF – Notation . . . . . . . . . . . . . . . . . . . . . . . . .
1.4.2 Kellerautomaten . . . . . . . . . . . . . . . . . . . . . . . . .
1.4.3 Äquivalenz von Kellerautomaten und kontextfreien Sprachen
1.4.4 Greibach – Normalform . . . . . . . . . . . . . . . . . . . . .
1.4.5 LR(k) – Sprachen . . . . . . . . . . . . . . . . . . . . . . . .
1.4.6 LL(k) – Grammatiken . . . . . . . . . . . . . . . . . . . . . .
1.4.7 Rekursiver Abstieg . . . . . . . . . . . . . . . . . . . . . . . .
1.4.8 Das Pumping – Lemma für kontextfreie Sprachen . . . . . . .
1.5 Kontextsensitive Grammatiken . . . . . . . . . . . . . . . . . . . . .
249
249
249
251
252
253
254
255
256
258
258
259
260
2 Berechenbarkeit
2.1 Hypothetische Maschinen . . . . . . . . . . . . . . . . . . . . . . . .
2.1.1 Turing – Maschinen . . . . . . . . . . . . . . . . . . . . . . .
2.1.2 Registermaschinen . . . . . . . . . . . . . . . . . . . . . . . .
2.2 Rekursive Funktionen . . . . . . . . . . . . . . . . . . . . . . . . . .
2.2.1 Primitiv rekursive Funktionen . . . . . . . . . . . . . . . . . .
2.2.2 µ – rekursive Funktionen . . . . . . . . . . . . . . . . . . . .
2.2.3 Allgemeine Bemerkungen zur Rekursion . . . . . . . . . . . .
2.3 Äquivalenz der Berechenbarkeitsbegriffe . . . . . . . . . . . . . . . .
2.3.1 Äquivalenz von µ – Berechenbarkeit und Turing – Berechenbarkeit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
275
275
276
279
280
280
282
284
284
260
265
266
266
267
268
269
270
272
272
273
274
284
INHALTSVERZEICHNIS
IX
2.3.2
2.4
Äquivalenz von Registermaschinen- und Turing – Berechenbarkeit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2.3.3 Churchs These . . . . . . . . . . . . . . . . . . . . . . . . . .
Entscheidbarkeit . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2.4.1 Nicht berechenbare Funktionen . . . . . . . . . . . . . . . . .
2.4.2 (Nicht) entscheidbare Prädikate . . . . . . . . . . . . . . . . .
2.4.3 Rekursion und rekursiv aufzählbare Mengen . . . . . . . . . .
286
286
286
286
287
288
3 Komplexitätstheorie
3.1 Komplexitätsmaße . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.1.1 Zeitkomplexität . . . . . . . . . . . . . . . . . . . . . . . . . .
3.1.2 Bandkomplexität . . . . . . . . . . . . . . . . . . . . . . . . .
3.1.3 Zeit- und Bandkomplexitätsklassen . . . . . . . . . . . . . . .
3.1.4 Polynomiale und nichtdeterministisch polynomiale Zeitkomplexität . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.1.5 Nichtdeterminismus in Algorithmen — Backtracking . . . . .
3.2 N P – Vollständigkeit . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.2.1 Das Erfüllungsproblem . . . . . . . . . . . . . . . . . . . . . .
3.2.2 N P – vollständige Probleme . . . . . . . . . . . . . . . . . .
3.3 Effiziente Algorithmen für N P – vollständige Probleme . . . . . . .
3.3.1 Geschicktes Durchlaufen von Baumstrukturen . . . . . . . . .
3.3.2 Alpha / Beta – Suche . . . . . . . . . . . . . . . . . . . . . .
3.3.3 Dynamisches Programmieren . . . . . . . . . . . . . . . . . .
3.3.4 Greedy – Algorithmen . . . . . . . . . . . . . . . . . . . . . .
291
291
291
292
293
4 Effiziente Algorithmen und Datenstrukturen
4.1 Diskussion ausgewählter Algorithmen . . . . .
4.1.1 Komplexität von Sortierverfahren . . . .
4.1.2 Wege in Graphen . . . . . . . . . . . . .
4.2 Bäume . . . . . . . . . . . . . . . . . . . . . . .
4.2.1 Geordnete, orientierte, sortierte Bäume
4.2.2 Darstellung von Bäumen durch Felder .
4.2.3 AVL – Bäume . . . . . . . . . . . . . .
4.2.4 B – Bäume . . . . . . . . . . . . . . . .
4.3 Effiziente Speicherung großer Datenmengen . .
4.3.1 Rechenstruktur der Mengen (mit Zugriff
4.3.2 Mengendarstellung durch AVL – Bäume
4.3.3 Streuspeicherverfahren . . . . . . . . . .
307
307
307
308
309
309
309
310
310
310
311
311
314
. . .
. . .
. . .
. . .
. . .
. . .
. . .
. . .
. . .
über
. . .
. . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
Schlüssel)
. . . . . .
. . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
294
295
297
297
298
299
300
301
303
304
X
INHALTSVERZEICHNIS
Teil I
1
Kapitel 1
Information, ihre
Repräsentation und
Verarbeitung
Informatik: Wissenschaft, Technik und Anwendung der Modellierung, maschinellen Verarbeitung, Speicherung, Übertragung und Darstellung von Information
und informationsverarbeitenden Prozessen.
Hardware: Alle Geräte der Informationsverarbeitung und Kommunikationstechnik.
Software: System von Programmen und Daten zur Bearbeitung einer bestimmten
Fragestellung oder Aufgabe.
Programm: In einer formalen Sprache abgefasste Verarbeitungsvorschrift.
Wir unterscheiden im Umgang mit Information:
• Repräsentation (äußere Form der Darstellung)
• abstrakte Deutung: Information
• Bezug zur Realität
• Richtigkeit
1.1
Information in der Informatik
Zentrale Fragen:
• schematische Darstellung von Information (Informationsrepräsentationssysteme), Daten- und Objektstrukturen sowie deren Bezüge untereinander
• Regeln und Vorschriften zur Verarbeitung von Informationen in der Gestalt
von Daten und Repräsentationen
1.2
Modellbildung in der Informatik
Modell: Vereinfachte Abbildung eines Ausschnitts der Wirklichkeit mit Mitteln der
Informatik
Modellbildung: Wahl eines Modells
3
4
KAPITEL 1. INFORMATION, IHRE REPRÄSENTATION . . .
Modellierungsgegenstände:
• Aspekte des Anwendungsgebietes
• Strukturen der Informatik: Daten, Programme, Prozesse, Hardware, etc.
Einsatzgebiete:
• Eingebettete Hardware / Software - Systeme zur Steuerung von technischen oder physischen Prozessen unter Zeitabsonderung (Fly-by-wire in
Flugzeugen, in Fahrzeugen, Airbag, Stabilitätskontrolle, Motormanagement, Handys, Waschmaschine, Unterhaltungselektronik etc.)
• Rechensysteme mit Nutzerschnittstelen (PC, WS, PDA) für Informationsverarbeitungsaufgaben (Textverarbeitung)
• Vernetzte Rechner (Rechnernetz, Internet, Telekommunikation, . . . ) zur
Lösung von Kommunikations- und Datenübertragungsaufgaben
1.3
Informationsrepräsentationssysteme
R . . . Menge von Repräsentationen
A . . . Menge von Abstraktionen (Informationen)
I: R → A . . . Interpretation(sabbildung)
(A, R, I) . . . Informationsrepreäsentationssystem
Beispiel: Zahlen
N . . . Menge der natürlichen Zahlen (einschließlich 0)
Repräsentationsmengen für natürliche Zahlen:
• Dezimalschreibweise
• Binärschreibweise
• Römische Zahlen
• Strichzahlen: SZ={ε, I, II, III, . . .}
• Hexadezimal
• Oktal
• BCD-codiert
• Exponentialdarstellung
• Primzahldarstellung
• ...
I: SZ → N
(N, SZ, I) Informationsrepräsentationssystem
Oft kann eine Information durch unterschiedliche Repräsentationen dargestellt werden. Für r1 , r2 ∈ R sagen wir: r1 und r2 sind bedeutungsgleich oder semantisch
äquivalent, wenn I[r1 ] = I[r2 ] ist.
1.4. AUSSAGEN ALS BEISPIEL FÜR INFORMATIONEN
1.4
5
Aussagen als Beispiel für Informationen
Beispiele für Aussagen:
• Es ist kalt.
• Es regnet.
• ...
Für jede elementare Aussage ist es - für ein vorgegebenes Bezugssystem - möglich,
ihr den Wert wahr oder falsch zuordnen.
Beispiel: Modellierung durch elementare Aussagen - Airbag
C = Crashsensor spricht an
A = Airbag löst aus
Z = Zündung ist ein
Zusammengesetzte Aussage: (C und Z) dann A
Charakterisierung von Aussagen (Was ist eine Aussage?) - Aristoteles, 384 - 322
v. Chr.:
Eine Aussage ist ein sprachliches Gebilde, von dem es sinnvoll ist zu
sagen, es sei wahr oder falsch.
Beispiel für eine paradoxe Aussage: Die Aussage dieses Satzes ist falsch.
Dieser Satz ist reflexiv und widersprüchlich.
Wie solche Widersprüche vermeiden? Durch Abkehr von der natürlichen Sprache
und Verwendung eines präzise festgelegten Repräsentationssystems für Aussagen.
1.4.1
Boolesche Terme
Atomare Aussagen: Menge vorgegebener Aussagen, die nicht weiter in Aussagen zerlegt werden können
Zusammengesetzte Aussagen: Aus Unteraussagen zusammengesetzte Aussagen
Aussagen können durch aussagenlogische Operationen (Verknüpfungen) zusammengesetzt werden.
6
KAPITEL 1. INFORMATION, IHRE REPRÄSENTATION . . .
Definition: Boolesche Terme
gegeben:
E . . . Menge von elementaren, atomaren Aussagen (Analogie: Zahlen)
ID . . . Menge von Identifikatoren (insbesondere ist x, y, z ∈ ID) (Analogie: Variablen, Unbekannte)
Die Menge der booleschen Terme ist wie folgt festgelegt:
(1) true, false sind boolesche Terme.
(2) Alle atomaren, elementaren Aussagen in E und alle Identifikatoren sind
boolesche Terme.
(3) Ist t ein boolescher Term, so ist (¬t), gesprochen
Term.
nicht t, ein boolescher
(4) Sind t1 und t2 boolesche Terme, so sind folgende Gebilde auch boolesche
Terme:
• (t1 ∧ t2 )
und
• (t1 ∨ t2 )
oder
• (t1 ⇒ t2 )
impliziert
• (t1 ⇔ t2 )
äquivalent
Nach (3) und (4) bilden wir zusammengesetzte boolesche Terme.
¬, ∧, ∨, ⇒, ⇔ heißen boolesche (oder aussagenlogische) Operatoren / Verknüpfungen.
Ein boolescher Term heißt elementar, wenn er keine Identifikatoren enthält.
Abkürzungen (jeweils semantisch äquivalent):
• (t1 ∧ t2 ) steht für (¬((¬t1 ) ∨ (¬t2 )))
• (t1 ⇒ t2 ) steht für ((¬t1 ) ∨ t2 )
• (t1 ⇔ t2 ) steht für ((t1 ∧ t2 ) ∨ ((¬t1 ) ∧ (¬t2 )))
Wir sagen für x ∈ ID: x kommt im booleschen Term t frei vor, falls der Identifikator x im Term t auftritt.
1.4.2
Boolesche Algebra der Wahrheitswerte
Die Menge der Wahrheitswerte hat genau zwei Elemente. Es gibt viele Möglichkeiten, die Menge der Wahrheitswerte darzustellen:
• {wahr, f alsch}
• {true, f alse}
• {L, 0}
• {1, 0}
• {>, ⊥}
• {{∅} , ∅}
Festlegung: B = {L, 0} Menge der Wahrheitswerte.
Abbildungen auf Wahrheitswerten:
1.4. AUSSAGEN ALS BEISPIEL FÜR INFORMATIONEN
7
• not: B → B
b not(b)
L
0
0
L
• id: B → B
b id(b)
L
L
0
0
• or: B × B → B
a b a and b
0 0
0
0 L
L
L 0
L
L L
L
• and:
a
0
0
L
L
B×B→B
b a and b
0
0
L
0
0
0
L
L
• impl:
a
0
0
L
L
B×B→B
b a and b
0
L
L
L
0
0
L
L
• equiv: B × B → B
a b a and b
0 0
L
0 L
0
L 0
0
L L
L
Die Festlegung einer Menge und von Abbildungen führt auf eine Algebra.
impl(or(and(L, 0), L), not(0)) = L
1.4.3
Interpretation boolescher Terme
Für elementare boolesche Terme definieren wir eine Interpretationsfunktion, die
jedem elementaren booleschen Term einen Wahrheitswert zuordnet:
• I[true] = L
• I[f alse] = 0
• I[(¬t)] = not(I[t])
• I[(t1 ∨ t2 )] = or(I[t1 , I[t2 )
Wir belegen die freien Identifikatoren mit Wahrheitswerten durch Abbildung:
β:D→B
β heißt Belegung von D. Sei x ∈ D.
8
KAPITEL 1. INFORMATION, IHRE REPRÄSENTATION . . .
• Iβ [x] = β(x)
• Iβ [(t1 ∧ t2 )] = and(Iβ [t1 ], Ibeta [t2 ])
Mit WBool bezeichnen wir die Menge der elementaren booleschen Terme (d.h. die
Terme ohne freie Identifikatoren), mit WBool (ID) die Menge der booleschen Terme
mit Identifikatoren aus ID.
I : WBool (ID) → (EN V → B)
Sei dabei EN V die Menge der Belegungen von Identifikatoren aus ID. I[t](β) liefert
Wahrheitswerte gegeben durch Iβ [t].
(B, WBool , Iβ und (EN V → B, WBool , I) sind Informationsrepräsentationssysteme.
Damit ist auf booleschen Termen eine Relation semantisch äquivalent definiert.
Beispiel: semantische Äquivalenz
(f alse ⇒ x)
true
Sind diese Terme semantisch äquivalent? Ja, weil Iβ [f alse ⇒ x] =
Iβ [true]∀β ∈ EN V .
Beweis durch Tabelle:
β(x) Iβ [f alse ⇒ x] Iβ [true]
L
L
L
0
L
L
Sind (¬(x ∧ y)) und ((¬x) ∨ (¬y)) äquivalent?
x y Iβ [(¬(x ∧ y))] Iβ [((¬x) ∨ (¬y))]
L L
0
0
L 0
L
L
0 L
L
L
0 0
L
L
1.4.4
Gesetze der booleschen Algebra
(i) Involutionsgesetz
(ii) Kommutativgesetz
(iii) Assoziativgesetz
¬¬x = x
(
x∧y =y∧x
x∨y =y∨x
(
(x ∧ y) ∧ z = x ∧ (y ∧ z)
(x ∨ y) ∨ z = x ∨ (y ∨ z)
(iv) Idempotenzgesetz
(
x∧x=x
x∨x=x
(v) Absorptionsgesetz
(
x ∧ (x ∨ z) = x
x ∨ (x ∧ z) = x
(vi) Distributivitätsgesetz
(vii) Gesetz von de Morgan
(
x ∧ (y ∨ z) = (x ∧ y) ∨ (x ∧ z)
x ∨ (y ∧ z) = (x ∨ y) ∧ (y ∨ z)
(
¬(x ∧ y) = (¬x) ∨ (¬y)
¬(x ∨ y) = (¬x) ∧ (¬y)
1.4. AUSSAGEN ALS BEISPIEL FÜR INFORMATIONEN
(viii) Neutralitätsgesetz
9
(
x ∨ (y ∧ ¬y) = x
x ∧ (y ∨ ¬y) = x
(x ⇒ y) = (¬x) ∨ y
Theorem: Die booleschen Terme auf der rechten und linken Seite der Gleichungen
der Gesetze der booleschen Algebra sind jeweils semantisch äquivalent.
Beweis: durch Tabellentechnik 1.4.5
Anwendung der Gesetze der booleschen Algebra
Die Gleichheit
• t=t
= erfüllt folgende Gesetze einer Äquivalenzrelation:
Reflexivität
• t1 = t2 , dann gilt auch t2 = t1
Symmetrie
• gilt t1 = t2 und t2 = t3 , dann gilt auch t1 = t3
Transitivität
Definition: Substitution
Die Substitution ist eine Operation auf Termen. Sie entspricht einer Abbildung:
WBool (ID) × WBool (ID) × ID → WBool (ID)
t[t0 /x]
Substitution von t0 für x in t
Beispiel: Substitution
((x ∧ x) ∨ ¬c)[(a ∨ b)/x] = (((a ∨ b) ∧ (a ∨ b)) ∨ ¬c)
Ergebnis ist der Term, der aus t entsteht, indem jedes Vorkommen von x in t durch
t0 ersetzt wird.
Verallgemeinerung: simultane Substitution t[t1 /x1 , t2 /x2 , . . . , tn /xn ]
Beispiel: Simultane und sequentielle Substitution
(x ∧ y)[(y ∧ z)/x, z/y] = ((y ∧ z) ∧ z)
((x ∧ y)[(y ∧ z)/x])[z/y] = (z ∧ z ∧ z)
simultan
sequentiell
Theroem: Verträglichkeit der Substitution mit der semantischen Äquivalenz:
Falls (für x ∈ ID, t0 ∈ WBool (ID)) gilt β(x) = Iβ [t0 ], dann git (für t ∈ WBool (ID):
Iβ [t] = Iβ [t[t0 /x]].
Durch die Substitution können wir genau definieren, was es heißt, ein Gesetz t1 = t2
anzuwenden:
Instanz bilden (Spezialfall): t1 [t01 /x1 , . . . , t0n /xn ] = t2 [t01 /x1 , . . . , t0n /xn ]
Will ich das Gesetz auf den Term t anwenden, so markiere ich die Anwendungsstelle: Ich suche Term t0 , t00 ∈ WBool (ID) und x ∈ ID, so dass gilt: t = t0 [t00 /x].
Wenn t00 genau mit t1 [[t01 /x1 , . . . , t0n /xn ] übereinstimmt, dann kann ich t00 durch
t2 [t01 /x1 , . . . , t0n /xn ] ersetzen, d. h.
t = t0 [t1 [t01 /x1 , . . . , t0n /xn ]/x] = t0 [t2 [t01 /x1 , . . . , t0n /xn ]/x]
Die Anwendungsstelle des Gesetzes wird auch als
Redex bezeichnet.
10
KAPITEL 1. INFORMATION, IHRE REPRÄSENTATION . . .
1.5
Information und Repräsentation:
Normalform
Da wir konkret auf Informationen (Semantik, Bedeutung) stats auf Repräsentationen (Syntax) arbeiten müssen, ist es sinnvoll, einen engen Bezug zwischen den
beiden herzustellen.
1.5.1
Umformung von Repräsentation als Informationsverarbeitung
Sei (A, R, I) ein Informationsrepräsentationssystem. Jede Abbildung ϕ0 : A → A auf
Informationen muss konkret durch eine Abbildung ϕ : R → R dargestellt werden.
Das erfordert folgende Beziehung: ϕ(r) = r0 ⇒ ϕ0 (I[r]) = I[r0 ]. Dies liefert das
folgende kommutierende Diagramm:
ϕ
A −−−−→
x

I
A
x

I
R −−−−→ R
f
Äquivalenztransformation: Gilt ∀r ∈ R : I[r] = I[ϕ(r)], so heißt ϕ Äquivalenzrelation, d.h. ϕ ist Identität auf dem Bild von I.
Normalform N ⊆ R ausgezeichnete Teilmenge der Repräsentation.
N heißt eindeutig, wenn ∀r, r0 ∈ N : I[r] = I[r0 ] ⇒ r = r0 .
N heißt vollständig ⇔ ∀a ∈ A : ∃r ∈ N : I[r] = a.
1.5.2
Zeichenfolgen: Wörter über einem Alphabet
Sei C eine Menge von Zeichen. Ein Wort der Länge n (oder eine Sequenz) über C
ist gegeben durch hc1 . . . c[ n]i (Notation für Wörter), wobei c1 , . . . , cn ∈ C.
Beispiel:
c = {a, b, . . . , z}
hheutei
Wort der Länge 5 über C.
Die Menge aller Wörter der Länge n bezeichnen wir mit C n (n-faches kartesisches
Produkt). Die Elemente von C n heißen auch n-Tupel und werden oft auch durch
(c1 , . . . , cn ) repräsentiert. Für n = 0 bekommen wir das leere Wort, darbestellt
durch hi oder ε, d.h. C 0 = {ε}.
C 1 = {hci, c ∈ C}
Achtung: C 1 6= C!
Die Menge aller Wörter über C ist durch C ∗ bezeichnet.
C∗ =
[
Cn
n∈N0
Beispiel:
∗
• Strichzahlen: {I} = {ε, I, II, III, . . .}
• Bytes: B8
∗
• {0, 1, . . . , 9}
1.5. INFORMATION UND REPRÄSENTATION: NORMALFORM
11
Zur Darstellung von Texten verwenden wir spezielle Symbole (Leerzeichen, Zeilenvorschub, . . . ).
(C ∗ )∗ = {hhc1 . . . cn ihc01 . . . c0n i . . .i, . . .} Wörter von Wörtern über C, Sequenzen von
Sequenzen über C.
∅∗ = {ε} (∅∗ )∗ = {ε, hεi, hεεi, . . .} (Unendliche Menge aus nichts)
Notation: C + = C ∗ \ {ε}
Operationen auf Zeichenfolgen:
• conc : C ∗ × C ∗ → C ∗
Konkatenation
conc(hc1 . . . cn i, hc01 . . . c0n i) = hc1 . . . cn c01 . . . c0n i
Häufig schreiben wir die Konkatenation infix: für w, v ∈ C ∗ : w ◦ v =def
conc(w, v).
Algebraische Gesetze:
– w◦ε=w
ε◦w =w
ε ist ein neutrales Element bezüglic hder Konkatenation.
– (w1 ◦ w2 ) ◦ w3 = w1 ◦ (w2 ◦ w3 )
Assoziativität
• length : C ∗ → N, length(hc1 . . . cn i) = n
∗
Achtung: length auf {I} ist die klassische Interpretation.
∗
Für w, v ∈ {I} : length(w ◦ v) = length(w) + length(v).
Schreibweise für w ∈ C ∗ : |w| = length(w).
Definition: Formale Sprache
Die formale Sprache S über C ist definiert als:
S ⊆ C∗
12
KAPITEL 1. INFORMATION, IHRE REPRÄSENTATION . . .
Kapitel 2
Algorithmen und
Rechenstrukturen
Bestimmte Aufgabenstellungen lassen sich schematisch, durch Algorithmen lösen.
Dabei betrachten wir in der Regel Aufgaben, die von gewissen Eingabegrößen
abhängig sind (Eingabeparameter) und in der Regel werden bestimmte Ergebnisse
durch den Algorithmus zurückgegeben (Resultate, Ausgaben).
Beispiel: Multiplikation von Binärzahlen
Eingabe: Zwei Binärzahlen b1 , b2 ∈ B∗
Ausgabe: Binärzahl r ∈ B∗ , wobei für I : B∗ → N (Interpretation) gilt:
I[b1 ] · I[b2 ] = I[r].
2.1
Der Begriff Algorithmus
Definition: Algorithmus (informell)
Ein Algorithmus ist ein schematisch anwendbares Verfahren [zur Lösung einer
bestimmten Aufgabe] mit einer präzisen (d.h. in einer eindeutig verständlichen
Sprache abgefassten) endlichen Beschreibung unter Verwendung effektiver (d.h.
tatsächlich ausführbarer) Verarbeitungsschritte.
Beispiel:
(1) Multiplikation zweier Zahlen in Dezimaldarstellung
(2) Euklids Algorithmus zur Berechnung des ggT zweier Zahlen a, b ∈
N (Eingabe). Der Algorithmus zur Berechnung des ggT(a, b)
lautet wie folgt:
(i) falls a = b, dann gilt ggT(a, b) = a
(ii) falls a < b, dann gilt ggT(a, b) = ggT(a, b − a)
(iii) falls a > b, dann gilt ggT(a, b) = ggT(a − b, b)
Für Algorithmen unterscheiden wir fundamental zwei Sichten:
(i) funktionale Sicht: Welche Aufgabenstellung bewältigt der Algorithmus?
13
14
KAPITEL 2. ALGORITHMEN UND RECHENSTRUKTUREN
(ii) operationelle Sicht: Durch welche elementaren Verarbeitungsschritte wird die
Aufgabe gelöst?
(iii) (Warum?)
Merkmale für Algorithmen
Ein Algorithmus heißt:
• terminierend, wenn er stets (für alle zulässigen Folgen von Schritten) nach
endlich vielen Schritten endet.
• deterministisch, wenn bei der Auswahl der Verarbeitungsschritte keine Freiheit besteht.
• determiniert, wenn das Resultat (unabhängig von der Wahl der Verarbeitungsschritte im nicht-deterministischen Fall) eindeutig bestimmt ist.
• sequentiell, wenn alle seine Verarbeitungsschritte stets strikt hintereinander ausgeführt werden.
• parallel (nebenläufig), wenn gewisse Verarbeitungsschritte zeitlich (oder logisch) nebeneinander ausgeführt werden.
Klassische Beschreibungselemente für Algorithmen:
• Ausführung elementarer Schritte (Umformung gewisser Werte, Änderung gewisser Zustände)
• Fallunterscheidung (Ausführen von Schritten abhängig von Bedingungen)
• Wiederholung und Rekursion
2.1.1
Textersetzungsalgorithmen
Wir betrachten zunächst nur Algorithmen auf Texten ∈ V ∗ (Wörter über einem
endlichen Zeichenvorrat V).
Textersetzungsregel: (v, w) ∈ V ∗ × V ∗ geschrieben v → w (*)
Beispiel:
textersetzungssystem → tes
Konvention: In Textersetzungsregeln lassen wir die Klammern h. . .i weg.
Für beliebige Wörter α, w ∈ V ∗ nennen wir α ◦ v ◦ ω → α ◦ w ◦ ω eine Anwendung
der Regel (*).
Ein Textersetzungssystem über einer Menge V von Zeichen ist eine endliche Menge
von Textersetzungsregeln. Für ein Textersetzungssystem T nennen wir eine Folge
von Wörtern t0 , . . . , tn ∈ V ∗ eine Berechnung, falls ∀i : ti → ti+1 die Anwendung
einer Regel aus T ist.
Achtung: Es existieren endliche und unendliche Berechnungen!
Ein Wort t ∈ V ∗ heißt terminal bezüglich T, wenn keine Regel aus T auf t anwendbar ist, d. h. @t0 ∈ V ∗ , so dass t → t0 Anwendung einer Regel aus T ist. Eine
endliche Berechnung t0 , . . . , tn heißt vollständig, wenn tn terminal ist. Hier heißt t0
Eingabe und tn Ausgabe.
Der Textersetzungsalgorithmus für T erzeugt eine Berechnung für die Eingabe
t0 ∈ V ∗ wie folgt: Solange eine Regel auf das letzte Wort in der Berechnungssequenz anwendbar ist, wende eine Regel an und füge die rechte Seite der Anwendung
zur Berechnungssequenz hinzu.
2.1. DER BEGRIFF ALGORITHMUS
15
Beispiel: Addition von 1 zu einer Zahl in Binärdarstellung
Aufgabe: x1 . . . xn Darstellung einer Zahl k in Binärschreibweise, y1 . . . ym
Darstellung von k+1 in Binärschreibweise.
Ax1 . . . xn SE → · · · → Ay1 . . . ym E (A, E und S sind Hilfszeichen)
V = {L, 0, A, S, E}
Textersetzungsregeln:
• 0S → L
• LS → S0
• AS → AL
AL0LL0LLSE → AL0LL0LS0E →
AL0LL0S00E → AL0LLL00E
Beispiel: Revertieren einer Binärzahl
Aufgabe: Ax1 . . . xn E → · · · → Axn . . . x1 E
Algorithmus:
• RL → SLR
• R0 → S0R
• SLL → LSL
• S0L → LS0
• SL0 → 0SL
• S00 → 0S0
• SLE → LE
• S0E → 0E
• RE → E
AR00LE → AS0R0LE → AS0S0RLE →
AS0S0SLRE → AS0S0SLE → AS0S0LE →
AS0LS0E → AS0L0E → ALS00E →
AL0S0E → AL00E
Textersetzungsalgorithmen sind in aller Regel nicht deterministisch. In einem Wort
gibt es in der Regel mehrere Anwendungsstellen für Regeln.
2.1.2
Deterministische Textersetzung
Will man den Nichtdeterminismus vermeiden, dann können wir das durch Konventionen erreichen, die angeben, wann welche Regel zuerst angewendet werden soll /
darf.
Definition: Markov-Strategie zur Anwendung von Regeln
Sind in einem Textersetzungssystem mehrere Regeln anwendbar, so wird immer
diejenige Regel angewendet, die in der Aufschreibung der Regeln zuerst kommt.
Ist die ermittelte Regel an mehreren Stellen anwendbar, so wählen wir die Anwendungsstelle, die am weitesten links liegt.
Durch diese Strategie wird der Textersetzungsalgorithmus deterministisch.
In der Menge der Berechnungen für ein Wort wird jeweils genau eine Berechnung
16
KAPITEL 2. ALGORITHMEN UND RECHENSTRUKTUREN
und damit genau ein Resultat ausgezeichnet.
Weitere Konvention: Terminierung über die Punktregel: Wir können zusätzlich
gewisse Ersetzungsregeln auszeichnen, indem wir (für v, w ∈ V ∗ ) w → .v schreiben.
Dann soll gelten, dass der Textersetzungsalgorithmus unmittelbar nach der Anwendung einer solchen Regel terminiert.
Beispiel: Punktregel
Durchschieben des Zeichens R in einem Binärwort bis hinter das erste L
V = {R, 0, L}
R0 → 0R
RL → .LR
2.1.3
Durch Textersetzungssysteme induzierte Abbildungen
Ein Textersetzungssystem T definert im deterministischen (auch im determinierten)
Fall eine Zuordnung für jedes Wort w ∈ V ∗ vermöge w → · · · → v eine Berechnung
(v ist terminal), oder wir erhalten für w eine unendliche Berechnung. In diesem Fall
sagen wir, dass das Ergebnis undefiniert ist, dargestellt durch ⊥.
Damit gilt: Jeder Textersetzungsalgorithmus T induziert eine Funktion fT : V ∗ →
V ∗ ∪ {⊥} definiert durch
fT (w) = v
falls v terminal und w → . . . → v
fT (w) = ⊥
falls die Berechnung für das Wort w nicht terminiert.
Dies definiert die funktionale Sicht, die Berechnung selbst w → w1 → w2 → . . . → v
die operationale Sicht.
Textersetzungsalgorithmen arbeiten auf konkreten Darstellungen von Information.
Betrachten wir eine konkrete Interpretation I : V ∗ → A (V ∗ Repräsentationen, A
Informationen):
fT
V ∗ −−−−→ V ∗ ∪ {⊥}




Iy
yI
A −−−−→
f
2.2
A0
Rechenstrukturen
Algorithmen arbeiten über Datenstrukturen. Textersetzungsalgorithmen nutzen nur
eine sehr elementare, uniforme Datenstruktur, nämlich Wörter (d.h. Zeichenfolgen).
Wir betrachten nun Rechenstrukturen als eine strukturierte, für viele Aufgaben
besser geeignete Darstellung von Information.
2.2.1
Funktionen und Mengen als Rechenstrukturen
Zur Strukturierung und Modellierung umfangreicher Daten, Begriffe und Zusammenhänge ist es immer nützlich, zunächst eine Familie von Mengen zu identifizieren,
die gewisse Informationen enthalten. Wir sprechen von Trägermengen.
inhaber : Konto → Kunde
2.2. RECHENSTRUKTUREN
17
Definition: Rechenstrukturen
Seien S und F Mengen von Bezeichnungen (Namen). S steht für die Namen für
Trägermengen, F für die Menge der Namen von Funktionen.
Eine Rechenstruktur A besteht aus je einer Trägermenge sA für jedes s ∈ S und
A
je einer
Funktion f zwischenden Elementen der Trägermengen für jedes f ∈ F .
A = ( sA ; s ∈ S , f A ; f ∈ F )
f A ist eine n-stellige Funktion auf gewissen Trägermengen.
A
A
f A : sA
1 × . . . × sn → sn+1
Beispiel: Die Rechenstruktur der Wahrheitswerte BOOL
bool
S = {bool
bool}
F = {true, false, ¬, ∨, ∧, ⇒, . . .}
bool BOOL = B ∪ {⊥}
trueBOOL :→ B ∪ ⊥
Abkürzung: M ⊥ := M ∪ ⊥
falseBOOL : B⊥ → B⊥
¬BOOL : B⊥ → B⊥
∨BOOL , ∧BOOL , ⇒BOOL : B⊥ × B⊥ → B⊥
falseBOOL = 0
trueBOOL = 0
¬BOOL (b) = not(b)
BOOL
¬
fürb ∈ B
(⊥) = ⊥
Beispiel: Die Rechenstruktur der Sequenzen SEQ
bool
S = {bool
bool, nat
nat, seqm
seqm, m }
F ={true, false, ¬, ∨, ∧, . . . ,
?
zero, succ, pred, add, mult, ≤, =, . . . ,
empty, make, conc, first, rest, last, lrest, length, . . .}
18
KAPITEL 2. ALGORITHMEN UND RECHENSTRUKTUREN
Trägermengen:
bool SEQ = B⊥
natSEQ = N⊥
seqm SEQ = (M ∗ )⊥
m SEQ = M ⊥
gegebene Trägermenge M
?
= : N⊥ × N⊥ → B⊥


L falls n = m
⊥
?
= (n, m) = 0 falls n 6= m


⊥ falls n = ⊥ ∨ m = ⊥
für n, m ∈ N
firstBOOL : (M ∗ )⊥ → M ⊥
(
m1 für k ≥ 1
BOOL
first
(hm1 m2 . . . mk i) =
⊥
für k = 0
...
Beispiel: Geldautomat als Rechenstruktur G
S = {karte, pin, betrag, bool, zustand}
F = {ein, pinein, betragein, pinok, ausgabe, aus}
einG : zustandG × karteG → zustandG
pineinG : zustandG × pinG → zustandG
betrageinG : zustandG × betragG → zustandG
pinokG : zustandG → boolG
ausgabeG : zustandG → betragG
ausG : zustandG → zustandG
karteG : zustandG → boolG
Wir verzichten auf die Angabe einer Trägermenge zustandG und betrachten nur eine wichtige Gleichung:
Sei z ∈ zustandG und karteG (z) = 0 (d.h. keine Karte im Automaten).
ausgabeG betrageinG pineinG einG (z, k) , p , b =
(
b falls pinokG pineinG einG (z, k) , p = L
=
0 sonst
2.2.2
Signaturen
Die Namen (Bezeichnungen, Identifikatoren) für die Trägermengen und Funktionen
einer Rechenstruktur bilden ihre Signatur.
2.2. RECHENSTRUKTUREN
19
pin
karte
aus
ein
pin_ein
zustand
karte
betrag_ein
betrag
ausgabe
pin_ok
bool
Abbildung 2.1: Signaturgraph
Definition: Signatur
Eine Signatur Σ = (S, F ) ist ein Paar von Mengen:
• S ist die Menge der Bezeichnungen für Trägermengen. Die Elemente von S
heißen Sorten oder Typen.
• F ist eine Menge von Bezeichnungen für Funktionen (wir sprechen von
Funktionssymbolen oder Konstanten).
Dabei ist für jedes f ∈ F eine Funktionalität festgelegt:
f ct
ctf = (s1 , s2 , . . . , sn )sn+1
Im nullstelligen Fall: f ct
ctf = s1
Beispiel:
f ct pinein = (zustand, pin) zustand
Oft wird eine grafische Angabe von Signaturen bevorzugt. Dann verwenden wir ein
Signaturdiagramm (s. Abbildung 2.1).
2.2.3
Grundterme
Für eine gegebene Signatur können wir Funktionsterme (auch Funktionsausdrücke),
kurz Terme, schreiben. Jeder Term hat eine Sorte (einen Typ).
Definition: Terme
Die Menge der Terme der Sorte s ∈ S über einer Signatur Σ = (S, F ) ist wie folgt
definiert:
(i) Jedes nullstellige Funktionssymbol f ∈ F mit f ct
ctf = s ist ein Term.
(ii) Ist f ∈ F mit f ct
ctf = (s1 , . . . , sn )s und sind t1 , . . . , tn Terme der entsprechenden Sorten s1 , . . . , sn , dann ist f (t1 , . . . , tn ) ein Term der Sorte s.
Beispiel: Terme über der Signatur zur Rechenstruktur SEQ
Terme der Sorte bool: {true, false, ¬(true), . . . , ∧(true, false), . . .}
Terme der Sorte nat: {zero, succ(zero), +(zero, succ(zero)}
¬(zero) ist kein Term, weil f ct
ct¬ = (bool) bool
20
KAPITEL 2. ALGORITHMEN UND RECHENSTRUKTUREN
1235
723
+
270
*
Abbildung 2.2: Berechnungsvorgang für ∗(+(1235, 723), 270)
Terme definieren für eine entsprechende Rechenstruktur Berechnungsvorgänge (s.
Abbildung 2.2).
≤ (3, 7)
geklammerte Präfixschreibweise
3≤7
Infixschreibweise
Konvention: Wir schreiben gewisse Terme zur besseren Lesbarkeit statt in geklammerter Präfix- in Infix-, Postfix- oder Mixfix-Schreibweise.
Beispiel: Schreibweisen von Termen
√
Postfix-Schreibweise
10!, 78 , 12
3 < 7 < 20
Mixfix-Schreibweise
Wir bezeichnen mit WΣs die Menge der Terme der Sorte s ∈ S zur Signatur
Σ = (S, F ). Für jeden Term t ∈ WΣs existiert ein Wert für jede Σ-Rechenstruktur
A. Dieser Wert liegt in der Trägermenge sA und wird durch die Interpretation
IsA : WΣs → sA festgelegt.
IsA [f ] = f A
mitff ct
ctf = s
Beispiel:
N AT
[zero] = zeroN AT = 0
Inat
A
Is [f (t1 , . . . , tn )] = f A (IsA [t1 , . . . , IsA [tn ])
falls f ct
ctf = (s1 , . . . sn )s
SEQ
Iseqnat
[conc(emptyset, make(zero))] =
SEQ
SEQ
concSEQ (Iseqnat
[emptyset, Iseqnat
[make(zero)]) =
ε ◦ h0i = h0i
2.2.4
Terme mit Identifikatoren
Typischerweise arbeiten wir beim Umgang mit Termen zusätzlich mit Identifikatoren (Bezeichnungen, Variablen). Die Identifikatoren stehen für frei wählbare Werte
aus gewissen Trägermengen.
∗(+(x, y), z)
2.2. RECHENSTRUKTUREN
21
Den Identifikatoren sind Sorten zugeordnet. Damit kann bei der Termbildung auf
die richtige Sortenangabe geachtet werden.
∧(≤ (x, y), x)
kein Term
Um einem Term t über der Signatur Σ mit freien Variablen aus X einen Wert in
einer Algebra A zuordnen zu können, verwenden wir wieder die Idee der Belegung.
X sei dabei eine Familie von sortierten Variablen.
X := {Xs : s ∈ S}
Xs . . . Menge der Identifikatoren der Sorte s. Eine Belegung von X ist eine Abbildung von der Menge aller Identifikatoren in die Menge aller Werte in A:
β : {x ∈ Xs : s ∈ S} → a ∈ sA : s ∈ S
wobei für x ∈ X : β(x) ∈ sA , d.h. Identifikatoren der Sorte s sind jeweils mit Werten
der Sorte s belegt.
Die Interpretation von Termen t für eine gegebene Belegung β bezeichnen wir mit
IβA [t]
Definition: Interpretation
IβA [x] = β(x)
IβA [f (t1 , . . . , tn )]
=f
A
mitx ∈ Xs
(IβA [t1 ], . . . , IβA [tn ])mitf
∈F
Beispiel: Interpretation
mult(ad(x, y), z)
β
x 2
y 3
z 4
IβN AT [mult(add(x, y), z)] =
= multN AT (IβN AT [add(x, y)], IβN AT [z]) =
= addN AT (IβN AT [x], IβN AT [y]) · 4 =
= (2 + 3) · 4 = 5 · 5 = 20
Für das punktweise Ändern einer Belegung verwenden wir folgende Notation:
β[m/x]
x ∈ Xs , m ∈ sA
bezeichnet wieder eine Belegung:
(
m
fallsx = y
(β[m/x])(y) =
β(y) sonst
22
KAPITEL 2. ALGORITHMEN UND RECHENSTRUKTUREN
Beispiel:
β
2
3
4
x
y
z
2.3
β[6/z]
2
3
6
Algorithmen als Termersetzungssysteme
Termersetzungssysteme (TES) liefern übersichtlichere Algorithmen als Textersetzung.
2.3.1
Termersetzung
Gegeben sei eine Signatur Σ, eine Familie X von Identifikatoren. Mit WΣs (x) bezeichnen wir die Menge der Terme über Σ und X der Sorte s.
Termersetzungsregel: (t, r) ∈ WΣs (x) × WΣs (x), geschrieben t → r
Beispiel:
pred(succ(x)) = x
Für beliebige Identifikatoren t1 , . . . , tn und Terme t1 , . . . , t2 der korrekten Sorten
heißt
t[t1 /x1 , . . . , tn /xn ] → r[t1 /x1 , . . . , tn /xn ]
eine Anwendung. Sei c ein beliebiger Term, der den Identifikator v genau einmal
enthält (v sei von der gleichen Sorte wie t und r). Anwendung c[t0 /v] → c[r0 /v],
wobei t0 → r0 Instanz der Regel t → r sei.
2.3.2
Termersetzungssysteme
Ein TES ist eine (endliche) Menge von Termersetzungsregeln. Eine Berechnung ist
eine endliche Folge
t0 → t1 → . . . → tn
von Termen oder eine unendliche Folge
t0 → t1 → . . . → . . .
von Termen mit der Eigenschaft, dass für alle ti → ti+1 die Anwendung einer Regel
ist.
Eine endliche Berechnung heißt vollständig, wenn tn terminal ist, d.h. es existiert keine Regel mit tn → t. Die terminalen Terme bilden ein Normalformensystem.
Beispiel: Termersetzung
2.3. ALGORITHMEN ALS TERMERSETZUNGSSYSTEME
23
nat
S = {nat
nat}
F = {zero, pred, succ}
pred(succ(x)) → x
Berechnung:
pred(succ(succ(pred(succ(x))))) →
succ(pred(succ(x))) →
succ(x)
Terme in Normalform:
zero, succ(zero), succ(. . . (zero) . . .), pred(zero), succ(. . . (pred(. . . (zero) . . .) . . .)
Beispiel: Binäre Addition als Termersetzung
bit
S = {bit
bit, seq bit
bit}
F = {stock, badd, empty}
seq bit
seq bit
f ct stock = (seq
bit, bit
bit)seq
f ct empty = seq bit
seq bit
seq bit
f ct badd = (seq
bit, seq bit
bit)seq
Zur Notation: Eine Binärzahl hL0Li stellen wir als
stock(stock(stock(empty, L), 0), L)
dar (Termnormalformdarstellung von Binärzahlen).
• badd(empty, x) → x
• badd(x, empty) → x
• badd(stock(x, 0), stock(y, 0)) → stock(badd(x, y), 0)
• badd(stock(x, 0), stock(y, L)) → stock(badd(x, y), L)
• badd(stock(x, L), stock(y, 0)) → stock(badd(x, y), L)
• badd(stock(x, L), stock(y, L)) →
→ stock(badd(badd(x, y), stock(empty, L)), 0)
2.3.3
Korrektheit von TES
Sei Σ eine Signatur, X eine Familie von Identifikatoren, R ein TES. Sei A eine
Rechenstruktur der Signatur Σ. Eine Termersetzungsregel t → r heißt korrekt
(bezüglich A), wenn für alle Belegungen beta gilt:
IβA [t] = IβA [r]
Beispiel:
IβN AT [empty] = 0
IβN AT [stock(t, b)] = IβN AT [t] · 2 + IβN AT [b]
IβN AT [badd(t1 , t2 )] = IβN AT [t2 ] + IβN AT [t2 ]
Ein TES heißt partiell korrekt, wenn jede seiner Regeln korrekt ist.
24
KAPITEL 2. ALGORITHMEN UND RECHENSTRUKTUREN
Beispiel: Korrektheit der obigen dritten Regel
IβN AT [badd(stock(x, 0), stock(y, 0))] = β(x) · 2 + β(y) · 2
IβN AT [stock(badd(x, y), 0)] = (β(x) + β(y)) · 2
Dies ist für jede Regel zu machen, um die partielle Korrektheit zu zeigen.
Neben der Frage, ob die Umformungen durch Termersetzung die Werte der Terme
unverändert lassen, interessiert uns auch die Frage, ob jede Berechnung terminiert.
Ein TES heißt (total) korrekt, falls es partiell korrekt ist und jede Berechnung
terminiert. Der Beweis der Terminierung erfolgt über eine Gewichtsfunktion:
G : WΣ (X) → N
Für eine geeignete Gewichtsfunktion zeigen wir, dass für jede Regel t → r gilt:
G[t] > G[r].
Berechnung:
t 0 → t 1 → t2 → . . .
G[t0 ] > G[t1 ] > G[t2 ] > . . .
2.4
Aussagenlogik und Prädikatenlogik
Eine der ältesten Ideen zur Führung unangreifbarer Argumentation ist die Verwendung formaler Beweissysteme.
Idee: Verwendung festgelegter Regeln zur Argumentation.
Wir betrachten dazu Terme der Sorte bool
bool, die Aussagen darstellen.
Problem: In Rechenstrukturen können Terme der Sorte bool auch den Wert undefiniert haben.
Wir schränken uns auf sogenannte zweiwertige Logik ein, d.h. auf Terme der Sorte
bool
bool, deren Werte für jede Belegung entweder L oder 0 sind.
Wichtige Aussagen, die wir häufig betrachten, sind Gleichungen: t1 = t2 (Gleichung zwischen Termen gleicher Sorte).
Starke Gleichheit:
(
L fallsIβA [t1 ] = IβA [t1 ]
A
Iβ [t1 = t2 ] =
0 sonst
Schwache Gleichheit:

A
A

L falls⊥ 6= Iβ [t1 ] = Iβ [t1 ]
IβA [t1 = t2 ] = 0 falls⊥ 6= IβA [t1 ] 6= IβA [t1 ] 6= ⊥


⊥ falls⊥ = IβA [t1 ] ∨ IβA [t1 ] = ⊥
Beispiel: Gleichheit
Starke Gleichheit: (predN AT (0) = succN AT (predN AT (0))) → (⊥ = ⊥) →
L
Schwache Gleichheit: (predN AT (0) = 0) → ⊥
Die starke Gleichheit verwenden wir in logischen Aussagen (zweiwertige Logik), die
schwache Gleichheit in Programme / Algorithmen, da die Gleichung nicht terminiert, falls die Berechnung eines der Werte der Terme t1 oder t2 nicht terminiert.
Achtung:
t1 = t2
?
t1 = t2
ist zweiwertig
ist dreiwertig
2.4. AUSSAGENLOGIK UND PRÄDIKATENLOGIK
2.4.1
25
Aussagenlogik
Im Zentrum der Logik steht der Ableitungsbegriff: Aus einer Menge H gegebener Aussagen werden nach festgelegten Regeln weitere Aussagen t abgeleitet. Wir
schreiben
H`t
um auszudrücken, dass sich aus der Menge der Aussagen H die Aussage t ableiten
lässt.
Beispiel:
{x ∧ y} ` x
Wir suchen ein Regelsystem, in dem die Ableitung korrekt vorgenommen werden
kann. Notation: ` t, falls ∅ ` t.
Ableitungsregeln der Aussagenlogik (Schlussregeln)
Seine t, t1 , t2 beliebige (zweiwertige) boolesche Terme.
• ` t ∨ ¬t
tertium non datur
• {t1 , ¬t2 ∨ t2} ` t2
modus poneus
• Ist t1 = t2 Anwendung einer Regel der booleschen Algebra (oder der eingeführten Abkürzungen, dann gilt {t1 } ` t2
Mehr Schlussregeln werden nicht benötigt.
Sei H eine Menge von Aussagen (zweiwertige boolesche Terme); wir sagen: die
Aussage t ist direkt aus H ableitbar und schreiben H ` t, falls t mit einer der
Schlussregeln aus H ableitbar ist, d.h. falls h0 ⊆ H existiert und H 0 ` t entspricht
einer der drei Schlussregeln.
Konsequenz: Unser Ableitungsbegriff ist monoton: Gilt H ` t und ferner H ⊆ H 0 ,
so gilt H 0 ` t.
Manchmal schreibt man statt
{t1 , . . . , tn } ` t
auch
t1 . . . t n
t
gelesen aus t1 , . . . , tn lässt sich t ableiten.
Format eines Beweises, ausgehend von einer Menge von Annahmen (Axiomen) H
(Beweis für: H ` tn ):
H ` t0
H ∪ {t0 } ` t1
H ∪ {t0 , t1 } ` t2
..
.
H ∪ {t0 , . . . , tn−1 } ` tn
Lemma: Gilt H ` t1 ⇒ t2 , so gilt auch H ∪ {t1 } ` t2 .
Theorem: {false} ` t, d.h. jede Aussage t lässt sich aus false ableiten.
26
KAPITEL 2. ALGORITHMEN UND RECHENSTRUKTUREN
Terminologie: Die Menge der Annahmen H heißt auch Axiome. Eine Menge H von
Axiomen und eine Menge von Ableitungsregeln bilden ein Ableitungssystem, auch
formales System oder Theorie genannt. Die ableitbaren Aussagen nennen wir Theoreme. Eine Theorie heißt inkonsistent, falls jede Aussage ableitbar ist. In unserem
Fall ist die Theorie bereits inkonsistent, wenn für eine Aussage t gilt: H ` t und
H ` ¬t. Begründung: H ` (t ∧ ¬t) = false.
Theorem: Korrektheit von Ableitungen Gilt H ` t, dann gilt für jede Belegung
β der Identifikatoren in H bzw. t: Sind alle Aussagen in H für β wahr, d.h. gilt
∀t ∈ H : Iβ [t0 ] = L, dann gilt auch Iβ [t] = L.
Eine Theorie ist also korrekt, wenn aus wahren Annahmen die Wahrheit der abgeleiteten Aussagen folgt.
2.4.2
Prädikatenlogik
In der Aussagenlogik werden nur Aussagen betrachtet und die Frage, ob diese den
Wert L oder 0 besitzen. Der innere Aufbau atomarer Aussagen ist nicht von Interesse.
Beispiel: Atomare Aussagen
• Hilde studiert Informatik.
• Klaus studiert BWL.
Ein Prädikat ist eine Abbildung
p : M1 × M2 × . . . × M n → B
Beispiel:
Sei M die Menge der Personen, G die Menge der Studienfächer.
studiert : M × G → B
studiert(Hilde, Informatik)
studiert(Klaus, BWL)
Mit Hilfe von Prädikaten können wir Terme schreiben, die Aussagen bilden. Auf
diese können alle aussagenlogischen Verknüpfungen angewandt werden.
Beispiel: Wie können wir die Aussage
Hans ist Student formulieren?
∃x ∈ G : studiert(Hans, x)
Um bestimmte allgemeine Aussagen formulieren zu können, werden Quantoren verwedet:
• Allquantor: ∀mx : t oder ∀x ∈ M : t
• Existenzquantor: ∃mx : t oder ∃x ∈ M : t
Beispiel:
Nicht alle Personen studieren Informatik:
¬∀y ∈ M : studiert (y, Informatik)
Durch die Verwendung von Quantoren bekommen wir wieder Aussagen. Der Identifikator x in ∀x . . . bzw. ∃x . . . heißt durch den Quantor gebunden.
(∀x ∈ M : t) ≡ (∀y ∈ M : t[y/x]), falls y in t nicht frei vorkommt.
2.4. AUSSAGENLOGIK UND PRÄDIKATENLOGIK
27
Beispiel: Namenskonflikt
∀x ∈ G : ∃y ∈ M : studiert(y, x)
Hier ist x nicht durch y ersetzbar.
Regeln für die Substitution mit gebundenen Bezeichnungen: Seien x, y Bezeichnungen. (∀x ∈ M : t)[t0 /y] = ∀x ∈ M : (t[t0 /y]), falls x nicht frei in t0 vorkommt.
Kommt x frei in t0 vor, muss die gebundene Bezeichnung x vor der Substitution
umbenannt werden. Umbenennungsregeln: (∀x ∈ M : t) = (∀y ∈ M : t[y/x]), falls
y nicht frei in t vorkommt.
Gesetze:
• ∀x ∈ M : t
=
¬∃x ∈ M : ¬t
• ∃x ∈ M : t
=
¬∀x ∈ M : ¬t
Auch für Quantoren lassen sich Ableitungsregeln angeben. Wir schreiben
(Σ, H) ` t
falls für den booleschen Term (zweiwertig!) t gilt: t ist Term über der Signatur Σ
und lässt sich aus der Menge der Aussagen H ableiten. Alle Ableitungsregeln für
Aussagenlogik gelten auch für die so erweiterte Sprache der Aussagen.
Ableitungsregeln für die Prädikatenlogik
Sei M Trägermenge zur Sorte m , sei M 6= ∅, M \ {⊥} =
6 ∅, sei x von der Sorte m :
(Σ, H) ` t
x ist nicht frei in H und kein Element von Σ
mx : t
∀m
mx : t sei t1 Term der Sorte m , t1 6= ⊥
(Σ, H) ` ∀m
(Σ, H) ` t[t1 /x]
(Σ, H) ` t[t1 /x] sei t1 Term der Sorte m , t1 6= ⊥
mx : t
(Σ, H) ` ∃m
mx : t sei f ct
(Σ0 , H) ` ∃m
ctx = m in Σ und Σ0 = Σ ohne x
(Σ, H) ` t
Wieder gilt eine Monotonie: Gilt (Σ, H) ` t und H ⊆ H 0 , Σ ⊆ Σ0 mit Σ = (S, F ),
Σ0 = (S 0 , F 0 ), S ⊆ S 0 , F ⊆ F 0 , so gilt (Σ0 , H 0 ) ` t.
Enthält die Signatur Σ0 Sorten und Funktionen, die nicht in Σ vorkommen, aber
auch nicht in H oder t, so gilt ((Σ, H) ` t) ⇔ ((Σ0 , H) ` t).
Interpretation von Quantoren über einer Algebra A der Signatur Σ (β Belegung, t
zweiwertiger boolescher Term):

0
A
0

L falls für die Belegung β : Iβ [t] = L mit β = β[w/x]
A
A
mx : t] =
Iβ [∀m
mit w ∈ m \ {⊥}


0 sonst

0
A
0

L falls eine Belegung β existiert Iβ 0 [t] = L mit β = β[w/x]
mx : t] =
IβA [∃m
und w ∈ mA \ {⊥}


0 sonst
28
KAPITEL 2. ALGORITHMEN UND RECHENSTRUKTUREN
Kapitel 3
Programmiersprachen
Komplexe Algorithmen lassen sich kaum lesbar und handhabbar durch Text- oder
Termersetzung beschreiben. Benötigt wird eine angepasste Notation: wir sprechen
von Programmiersprachen.
Definition: Programmiersprache
Eine Programmiersprache ist eine formale Sprache über einer endlichen Zeichenmenge V (d.h. die Programmiersprache ist ⊆ V ∗ ) zur Beschreibung von Algorithmen, Datenformaten und Anweisungen / Ausdrücken bezogen auf eine konkrete
oder abstrakte Maschine.
Eine Programmiersprache ist dafür konstruiert, auf einem Rechner verarbeitet zu
werden. Genau: Die Programme (Wörter der formalen Sprache) können auf dem
Rechner ausgeführt werden.
Es gibt eine große Zahl von Programmiersprachen - jeder Rechner hat eine eigene
Programmiersprache; wir sprechen von Maschinensprachen.
Daneben existieren rechnerunabhängige Sprachen (höhere oder problemorientierte Programmiersprachen), die auf vielen Rechnern realisiert werden können.
Diese haben folgendes Ziel:
• Formulierung von Programmen, die auf vielen unterschiedlichen Rechnern einsetzbar sind (Portabilität).
• Formulierung der Programme mit Mitteln, die eher dem zu lösenden Problem
dienen, als sich an der Struktur des Rechners zu orientieren (Lesbarkeit).
Eine Programmiersprache hat eine äußere Form (die formale Sprache) - wir sprechen von Syntax. Durch die Syntax wird festgelegt, wie die Programme aufgebaut
sind.
Zusätzlich hat jedes Programm einer Programmiersprache eine Bedeutung (Semantik). Dadurch ist festgelegt, was das Programm leistet, d.h. was passiert, wenn das
Programm ausgeführt wird. Dabei unterscheiden wir:
• funktionale Semantik: Welche Ergebnisse / Effekte werden durch das Programm erzielt?
• operationale Sematik: Durch welche Einzelschritte werden die Ergebnisse
/ Effekte erzielt?
3.1
Syntax - BNF
Eine Programmiersprache ist - syntaktisch gesehen - eine formale Sprache. Allerdings sind Programmiersprachen oft sehr kompliziert in ihrer syntaktischen Struktur. Deshalb setzen wir eine bestimmte Notation ein (Backus - Normalform, BNF),
29
30
KAPITEL 3. PROGRAMMIERSPRACHEN
um Programmiersprachen syntaktisch zu beschreiben. (Die BNF ist nach John
Backus benannt, einem amerikanischen Informatiker, der Fortran schuf und BNF
zur Beschreibung von Algol entwickelte.)
3.1.1
BNF
Ein Teil der BNF - Notation sind reguläre Ausdrücke. Sie dienen zur Beschreibung einfacher formaler Sprachen.
Beispiel: E-Mail-Adresse
[a|b| . . .]∗ .[a|b| . . .]∗ @[a|b| . . .]∗ [.[a|b| . . .]∗ ]∗
BNF-Notation ist selbst eine Sprache zur Beschreibung von Sprachen. Sei ein Zeichensatz V = {v1 , . . . , vn } gegeben.
regulärer Ausdruck Bedeutung: ⊆ V ∗
Terminalzeichen v ∈ V
v
{hvi} ⊆ V ∗
Vereinigung (R, Q reguläre R—Q
L[R] ∪ L[Q]
Ausdrücke mit Sprachen
L[R], L[Q] ⊆ V ∗ )
Konkatenation
RQ
{x ◦ y : x ∈ L[R],
y ∈ L[Q]}
Option
{R}
ε ∪ L[R]
∗
Iteration
{R} , R∗
{x1 ◦ . . . ◦ xn : n ∈ N∧
x1 , . . . , xn ∈ L[R]}
+
{R} , R+
{x1 ◦ . . . ◦ xn : n ∈ N \ {0} ∧
x1 , . . . , xn ∈ L[R]}
Klammern
[R]
L[[R]] = L[R]
leere Sprache
{} , ε
L[{}] = 0, L[ε] = {ε}
Die Schreibweise der regulären Ausdruücke ist dann umständlich, wenn Abkürzungen hilfreich wären.
Beispiel: Aufbau eines Buches
hBuchi ::=hTitelihVorwortihInhaltsverzeichnisi
hKapitelihKapiteli+ hIndexi
hTiteli ::=hTitelbezeichnungihAutorihVerlagi
hKapiteli ::=hKapitelüberschriftihVorspannihAbschnittihAbschnitti+
...
Idee: Jedes Wort h. . .i ist Bezeichnung für eine formale Sprache. Wir sprechen von
Nichtterminalen.
Eine BNF-Beschreibung hat allgemein die Form:
he1 i ::= R1
he2 i ::= R2
..
.
hen i ::= Rn
3.1. SYNTAX - BNF
31
(Definitionssystem).
Dabei sind he1 i, . . . , hen i Nichtterminalzeichen und R1 , . . . , Rn reguläre Ausdrücke,
in denen auch Nichtterminalzeichen auftreten dürfen.
Jedes Nichtterminal bezeichnet eine formale Sprache.
Beispiel: Klammergebirge
hklammergebirgei ::= (hklammergebirgei)
|hklammergebirgeihklammergebirgei
| ()
L[hklammergebirgei] = {h()i, h(())i, h()()i, . . .}
In BNF dürfen wir auch rekursive Definitionen für Sprachen schreiben, also Definitionssysteme, bei denen die definierten Nichtterminale beliebig auf der rechten Seite
in regulären Ausdrücken verwendet werden.
Beispiel: Aussagenlogik
hAussagei ::= true
| false
|hatomareAussagei
|hIdentifikatori
|(¬hAussagei)
|hAussagei[∧| ∨ | ⇒ | ⇐ | ⇔ | . . .]hAussagei
Die formalen Sprachen L[he1 i], . . . , L[hen i], die wir mit einem BNF-Definitionssystem verbinden, definieren wir wie folgt: Wir konstruieren eine Folge von formalen
Sprachen
E10 , E20 , . . . , En0
E11 , E21 , . . . , En1
...
wie folgt:
• Ei0 = ∅ für i = 1, . . . , n
sei die formale Sprache, die wir erhalten, indem wir für he1 i, . . . , hen i in
• ej+1
i
Ri die formalen Sprachen E1j , . . . , Enj verwenden.
Beispiel: Klammergebirge
Folge der formalen Sprachen E 0 , E 1 , . . . für hklammergebirgei:
E0 = ∅
E 1 = {h()i}
E 2 = {h(())i, h()()i, h()i}
E3 = . . .
32
KAPITEL 3. PROGRAMMIERSPRACHEN
PROGRAM
identifier
(
identifier
)
;
block
.
;
Abbildung 3.1: Syntax-Diagramm eines Pascal-Programms
Beobachung: Eij ⊆ Eij+1
S j
Wir definieren Ei =
Ei als die formale Sprache zu hei i. Wir erhalten dadurch
j∈N
eine induktive Definition für die formale Sprache Ei für hei i. Ferner gilt Ei = L[Ri ],
wobei wir in Ri für die he1 i, . . . , hen i jeweils die formalen Sprachen E1 , . . . , En verwenden.
Beispiel: Klammergebirge
E = {h(i ◦ e ◦ h)i : e ∈ E} ∪ {e1 ◦ e2 : e1 , e2 ∈ E} ∪ {h()i}
Fixpunktgleichung für die Sprache E
Können wir die Sprachen E1 , . . . , En ausschließlich durch diese Fixpunktgleichungen charakterisieren? Ja, wir müssen, falls verschiedene Lösungen existieren (d.h.
verschiedene Sprachen E10 , . . . , En0 ), jeweils die Sprachen wählen, die in der ⊆ Ordnung am kleinsten sind.
3.1.2
Syntaxdiagramme
Statt BNF-Notation werden auch Syntax-Diagramme verwendet, um formale Sprachen zu beschreiben.
Pascal-Programm: s. Abbildung 3.1
in BNF:
hprogrammi ::= PROGRAM hidentifieri (hidentifieri {, hidentifieri}∗ ; hblocki.
3.1.3
Kontextbedingungen
Durch die BNF-Notation können wir formale Sprachen definieren. Allerdings gibt
es in Programmiersprachen in der Regel zusätzliche Forderungen an die Elemente dieser formalen Sprache, die erfüllt sein müssen, damit wir von wohlgeformten
Programmen sprechen.
3.2
Semantik
Liegt die Syntax einer Sprache fest: L ⊆ V ∗ , wobei in der Regel L ⊆ L0 , L0 durch
BNF beschrieben und p ∈ L0 ist in L, wenn p alle Kontextbedingungen erfüllt, so
können wir für die Elemente in L die Semantik definieren. Im Idealfall geben wir
drei Varianten von Semantik:
(i) funktional (Welche Ergebnisse liefert ein Programm?)
(ii) operational (Durch welche Schritte wird das Ergebnis erzielt?)
(iii) axiomatisch (Wie können wir beweisen, dass das Programm das gewünschte
leistet?)
3.3. IMPLEMENTIERUNG VON PROGRAMMIERSPRACHEN
3.3
33
Implementierung von Programmiersprachen
Im einfachsten Fall entspricht ein Programm direkt einer Folge von Schritten, die
in einem Rechner ausgeführt werden.
Entspricht ein Programm p ∈ L nicht genau den Schritten, die ein Rechner zur
Verfügung stellt, so kennen wir zwei Verfahren zur Implementierung des Programms
auf einem Rechner:
(i) Interpretation: Wir realisieren auf unserem Rechner ein Programm (in der
Maschinensprache), das das Programm p ∈ L als Eingabe nimmt und diese
interpretiert.
(ii) Übersetzung: Wir übersetzen p ∈ L in die Maschinensprache. In der Regel wird
diese Übersetzung wieder durch ein Programm vorgenommen (Compiler).
3.4
Methodik der Programmierung
Die Erstellung (Entwicklung) umfangreicher Programme (Software) ist schwierig und aufwändig. Deshalb ist ein systematisches, wohlüberlegtes Vorgehen ratsam.
Grundprinzip: Niemals ein Programm schreiben, bevor man genau verstanden hat,
was es leisten soll.
Erst spezifizieren, dann programmieren, zuletz verifizieren.
34
KAPITEL 3. PROGRAMMIERSPRACHEN
Kapitel 4
Applikative / funktionale
Sprachen
Applikative oder funktionale Programmiersprachen zeichnen sich dadurch aus, dass
man Algorithmen in der Form von Funktionsdefinitionen beschreibt, die so aufgebaut sind, dass damit die Funktionswerte für beliebige Parameter algorithmisch
ermittelt werden können. Beherrschendes Element dieser Sprachen:
• Funktionsdefinition
• Funktionsanwendung (Applikation)
4.1
Rein applikative Sprachen
Beispiel: Fibonacci


0
fib(n) = 1


fib(n − 2) + fib(n − 1)
n 0 1 2 3
fib(n) 0 1 1 2
als Pascal-Funktion:
1
2
4
6
8
10
4
3
5
5
6
8
7
13
für n = 0
für n = 1
für n > 1
...
...
function fib (n: integer): integer;
begin
if n = 0 then
fib := 0
else
if n = 1 then
fib := 1
else
fib := fib (n-1) + fib (n-2)
end;
4.1.1
Syntax von Ausdrücken
In BNF-Schreibweise definieren wir die Syntax von Ausdrücken als formale Sprache
für hexpi:
35
36
KAPITEL 4. APPLIKATIVE / FUNKTIONALE SPRACHEN
hexpi ::= hidi
|⊥
|hfunctionapplicationi
|hconditionalexpi
|(hexpi)
|hmonadopihexpi
|ihexpihdyadopihexpi
|hsectioni
hmonadopi ::= −|¬
·
?
hdyadopi ::= +| − | · | = | < | ≤ | = | ≥ | > | ∧ | ∨ | ⇒ | ⇐ | ⇔ |◦
·
4.1.2
Bedeutung von Ausdrücken
Grundsätzlich sind wir an drei Formen der Beschreibung von Ausdrücken interessiert:
(i) operational: Angabe von Termersetzungsregeln
(ii) funktional: Angabe einer Interpretationsfunktion
(iii) axiomatisch: Angabe von Umformungsregeln
Um Programme schreiben zu können, setzen wir eine gegebene Signatur Σ = (S, F )
voraus, sowie eine Rechenstruktur A zu dieser Signatur. Wir verwenden eine Reihe
von Mengen, auf die wir die Bedeutung von Ausdrücken stützen.
ID
Menge von Identifikatoren
D = a ∈ sA : s ∈ S
Menge der Datenelemente
A
A
F CT ={f : sA
1 × . . . × sn → sn+1 : n ∈ N0 ∧ f strikt
∧ ∀i : 1 ≤ i ≤ n + 1 : si ∈ S}
Eine Funktion heißt strikt, wenn:
A
A
f : sA
1 × . . . × sn → sn+1
f (a1 , . . . , an ) = ⊥ ⇐ ai = ⊥ für ein i, 1 ≤ i ≤ n
H = D ∪ F CT Menge der semantischen Elemente
Beobachtung: Ist ein Term t über Σ mit Funktionen gebildet, die alle strikt sind (in
A), so gilt: Enthält t einen Teilterm t0 mit I[t0 ] = ⊥, so gilt I[t] = ⊥.
In Programmiersprachen verwenden wir in der Regel Sprachkonstrukte, die nicht
strikt sind, d.h. deren Wert ungleich ⊥ ist, auch wenn Teilterme auftreten, deren
Wert ⊥ ist.
Beispiel: Nicht strikte Funktion
;
Sequentielles Oder ∨
cor : B⊥ × B⊥ → B⊥
4.1. REIN APPLIKATIVE SPRACHEN
37
cor 0 L ⊥
0
0 L ⊥
L
L L L
⊥
⊥ ⊥ ⊥
cor(t1 , t2 ) wird ausgewertet, indem wir zunächst t1 auswerten. Falls t1
den Wert L liefert, setzen wir den Wert von cor(t1 , t2 ) = L. Sonst t2
auswerten und dann den Wert von cor ermitteln. Falls die Auswertung
von t1 nicht terminiert, terminiert insgesamt die Auswertung von cor
nicht.
cor ist nicht strikt. Damit gilt die obige Regeln Ausdrücke mit Teilausdrücken mit
Wert ⊥ haben den Wert ⊥ nicht für Ausdrücke, die mit nicht strikten Funktionen
gebildet werden.
Uneingeschränkte Termersetzung führt auf unendliche Berechnungen, falls Teilausdrücke Berechnungen haben.
Beispiel: cor
cor(true, x) → true
cor(false, x) → x
Sei t0 → t1 → . . . unendliche Berechnung für t0 .
cor(true, t0 ) → true
cor(false, t0 ) → cor(false, t1 ) → . . .
Fazit: Für nicht strikte Funktionen benötigen wir ein Termersetzungskonzept, für
das wir die Anwendung der Regeln genauer steuern können. Wir verwenden bedingte Termersetzungsregeln.
Beispiel: (Fortsetzung: cor)
cor(false, x) → x
t1 → t2 ⇒ cor(t1 , t3 ) → cor(t2 , t3 )
Dabei dürfen Termersetzungsregeln nicht mehr beliebig im Inneren von Termen angewendet werden, sondern werden von Bedingungen gesteuert.
Beispiel: (Fortsetzung: cor)
cor(true, x) → true
dadurch verhindert: cor(true, t0 ) → cor(true, t1 ) → . . .
38
KAPITEL 4. APPLIKATIVE / FUNKTIONALE SPRACHEN
4.1.3
Konstanten und Identifikatoren
Alle Programmiersprachen verwenden Konstanten oder Identifikatoren.
Syntax:
∗
hidi ::= hletteri {hcharacteri}
hletteri ::= a|b| . . . |z
hcharacteri ::= hletteri|0| . . . |9
Sei ID die Menge der Identifikatoren, d.h. der Wörter zur formalen Sprache hidi.
Um Identifikatoren Werte zuzuordnen, verwenden wir Belegungen:
ENV =def {β : ID → H}
Interpretation von Ausdrücken:
I : hexpi → (ENV → H)
Für x ∈ ID:
Iβ [x] = β(x)
Iβ [x] ist dabei die Interpretation des Ausdrucks x (in unserem Fall des Identifikators
x unter der Belegung β).
4.1.4
Bedingte Ausdrücke
Ein bedingter Ausdruck hat die folgende Form:
if B then E else E 0 f i
B: Bedingung
then E: then-Zweig
else E’: else-Zweig
Nebenbedingungen:
• B Ausdruck der Sorte bool
• E, E’ Ausdrücke der gleichen Sorte
• In B, E, E’ werden Identifikatoren im Hinblick auf ihre Sorten einheitlich
verwendet.
hconditionalexpressioni ::= ifhexpi thenhexpi
∗
{elifhexpi thenhexpi}
elsehexpi fi
Beispiel: Ausdrücke, die gegen Nebenbedingungen verstoßen
• if 1+2 then 5 else 7 fi (Die Bedingung muss vom Typ bool sein.)
• if 1¿2 then 5 else true fi (E und E’ müssen gleiche Sorten haben.)
• if x then x+1 else x+2 fi (Der Identifikator x muss einheitlich mit
einer festgelegten Sorte verwendet werden.)
4.1. REIN APPLIKATIVE SPRACHEN
39
Beispiel:
• if x ≥ y then x else y fi (max (x, y))
·
• if x = 0 then 0 else y = x fi (Abfangen eines Sonderfalls)
·
• if x then y else true fi (nicht strikte Erweiterung der Implikation)
Achtung: if . . . fi ist eine nicht strikte Konstruktion:


Iβ [E] fallsIβ [B] = L
Iβ [if B then E else E’ fi] = Iβ [E 0 ] fallsIβ [B] = 0


⊥
fallsIβ [B] = ⊥
Gleichungen: Es gilt
if true then
if false then
if ¬B then
if ⊥then
E
E
E
E
else
else
else
else
E’
E’
E’
E’
fi = E
fi = E 0
fi = if B then E’ else E fi
fi = ⊥
Termersetzungsregeln für if . . . fi:
B → B 0 ⇒ if B then E else E’ fi → if B’ then E else E’ fi
if true then E else E’ fi → E
if false then E else E’ fi → E 0
Ein Ausdruck if B then E else E’ fi wird ausgewertet, indem wir zunächst B
auswerten und dann, abhängig vom Ergebnis, E bzw. E’ auswerten.
Warum fi?
Algol-Syntax:
• if B then E else E 0
• if B then E
• if B1 then if B2 then E1 else E2
wie klammern?
fi dient als schließende Klammer, verbessert die Lesbarkeit.
Geschachtelter if-Ausdruck:
1
2
4
if x=y then E1
else if x<y then E2
else E3
fi
fi
Oder kürzer:
1
2
4
if x=y then E1
elif x<y then E2
else E3
fi
40
KAPITEL 4. APPLIKATIVE / FUNKTIONALE SPRACHEN
4.1.5
Funktionsapplikation
Sei F ein Ausdruck, der eine Funktion bezeichnet - ein Funktionsausdruck der Sorte
(S1 , . . . , sn )sn+1 . Dann ist F (E1 , . . . , En ) für beliebige Ausdrücke E1 , . . . , En der
Sorten s1 , . . . , sn ein Ausdruck der Sorte Sn+1 (Funktionsapplikation). Einzige
zusätzliche Nebenbedingung: Die Sorten der Identifikatoren in E1 , . . . , En und F
sind konsistent.
Zur Terminologie:
F (E1 , . . . , En )
E1 , . . . , E n
aktuelle Parameterausdrücke
Werte von E1 , . . . , En aktuelle Parameter
BNF:
∗ hfunction applicationi ::= hfunctioni (hexpi {, hexpi} )
Interpretation:
(
f (Iβ [E1 ], . . . , Iβ [En ]) falls ∀i, 1 ≤ i ≤ n : Iβ [Ei ] 6= ⊥
Iβ [F (E1 , . . . , En )] =
⊥
sonst
(strikte Funktionsanwendung)
Dabei ist f die Funktion, die durch F bezeichnet wird: f = Iβ [F ].
Regel für die Funktionsauswertung (sei 1 ≤ i ≤ n):
Ei → Ei0 ⇒ F (E1 , . . . , En ) → F (E1 , . . . , Ei−1 , Ei0 , Ei+1 , . . . , En )
Dies heißt: F (E1 , . . . , En ) wird ausgewertet, indem wir E1 , . . . , En auswerten.
Achtung: Terminiert die Auswertung von Ei für ein i, 1 ≤ i ≤ n nicht, so terminiert
die Auswertung von F (E1 , . . . , En ) nicht.
Prinzip: Zuerst E1 , . . . , En auswerten, dann die Funktionsapplikation auswerten
(Call-by-value, Wertaufruf von f ). Dies entspricht der Annahme, dass alle Funktionen strikt sind.
4.1.6
Funktionsabstraktion
Typischerweise arbeiten wir mit Ausdrücken mit frei belegbaren Identifikatoren.
Beispiel:
x2 + ax + b
x2 + 5x + 7
y 2 + 5y + 7
(Letztere beide sind in Darstellung und Interpretation verschieden.)
Falls x bzw. y nur Platzhalter für beliebige Werte sind, ist es besser, dies kenntlich
zu machen:
nat
nat : x2 + 5x + 7
(nat
natx)nat
Funktionsabstraktion
(4.1)
Der Ausdruck bezeichnet eine Funktion, d.h. die Abbildung, die durch
x 7→ x2 + 5x + 7
ausgedrückt wird, d.h. x wird abgebildet auf x2 + 5x + 7.
nat
nat
4.1 ist Ausdruck für eine Funktion mit Funktionalität (nat
nat)nat
nat.
nat
nat : y 2 + 5y + 7
(nat
naty)nat
(4.2)
4.1. REIN APPLIKATIVE SPRACHEN
41
Die Interpretationen von 4.1 und 4.2 sind gleich. Dies erkennt man, wenn man eine
Hilfsbezeichnung f für 4.1 bzw. 4.2 einführt:
nat
∀nat
natx : f (x) = x2 + 5x + 7
nat
∀nat
naty : f (y) = y 2 + 5y + 7
Allgemeine Form der Funktionsabstraktion:
(s1 x1 , . . . , sn , xn )sn+1 : E
Bezeichnungen:
(s1 x1 , . . . , sn , xn )sn+1
x1 , . . . , xn
s1 , . . . , sn
sn+1
E
Kopfzeile
formale Parameter
Sorten der formalen Parameter
Ergebnissorte
Ergebnisausdruck, Rumpf
Beispiel:
nat
nat : x · x + a · x + b
(nat
natx, nat
nata, nat
natb)nat
Applikation:
nat
nat : x · x + a · x + b)(7, 3, 10)
((nat
natx, nat
nata, nat
natb)nat
Interpretation:
Iβ [s1 x1 , . . . , sn xn )sn+1 : E] = f
wobei f eine Abbildung
A
A
f : sA
1 × . . . × sn → sn+1
A
ist, mit (für a1 ∈ sA
1 , . . . , an ∈ sn ):
(
Iβ 0 [E] fallsai 6= ⊥∀i, 1 ≤ i ≤ n
f (a1 , . . . , an ) =
⊥
fallsai = ⊥für eini, 1 ≤ i ≤ n
wobei
β 0 = η[a1 /x1 , . . . , an /xn ]
(ergibt die Belegung, die wir aus β erhalten, indem wir die Belegungen von x1 , . . . , xn
durch a1 , . . . , an ersetzen).
Achtung: Wir geben keine Auswertungsregeln für die Funktionsabstraktion, sondern
nur für die Funktionsapplikation von Funktionsabstraktionen.
((s1 x1 , . . . , sn xn )sn+1 : E)(E1 , . . . , En ) → E[E1 /x1 , . . . , En /xn ]
falls alle Ei terminal, d.h. in Normalform sind.
Beispiel: (Fortsetzung)
(x2 + ax + b)[7/x, 3/a, 10/b] = (72 + 3 · 7 + 10)
Achtung: Falls wir die obige Regel in Fällen anwenden, in denen ein Argument nicht
in terminaler Form ist, d.h. nicht auf seinen Wert reduziert ist, bekommen wir
eine Diskrepanz zu unserer Striktheitsfestlegung.
42
KAPITEL 4. APPLIKATIVE / FUNKTIONALE SPRACHEN
Beispiel:
·
nat
nat : ifx = 0then0elsexyfi)(0, 1 − 0)
((nat
natx, nat
naty)nat
·
Sofortiges Ensetzen der Argumentausdrücke liefert:
·
if0 = 0then0else0 · 1 − 0fi
·
Dies entspricht der Call-by-name - Auffassung: Die Auswertungsregel wird angewendet, ohne dass die Parameterausdrücke ausgewertet sind. Dies ist im Gegensatz
zu unserer Festlegung: Wir arbeiten stets mit Call-by-value.
In einer Funktionsabstraktion (s1 x1 , . . . , sn xn )sn+1 : E sind die Identifikatoren (wie
bei der Quantifizierung) gebunden.
Regel für Substitution:
((s1 x1 , . . . , sn xn )sn+1 : E)[E 0 /y] = (s1 x1 , . . . , sn xn )sn+1 : (E[E 0 /y])
falls y verschieden von x1 , . . . , xn und in E 0 kommen x1 , . . . , xn nicht frei vor.
((s1 x1 , . . . , sn xn )sn+1 : E)[E 0 /xi ] = (s1 x1 , . . . , sn xn )sn+1 : E
Beispiel:
nat
nat : x + x)[5/x] = ((nat
nat
nat : x + x)
((nat
natx)nat
natx)nat
α - Konversion: Umbenennung formaler Parameter
((s1 x1 , . . . sn xn )sn+1 : E) = ((s1 y1 , . . . sn yn )sn+1 : E[y1 /x1 , . . . , yn /xn ])
falls y1 , . . . , yn in E nicht frei vorkommen.
Analyse der Bezeichnerstruktur eines applikativen Programms:
(1) Kennzeichnen der freien Identifikatoren
(2) Zuordnen der gebundenen Identifikatoren zu ihren Bindungen
(3) Zuordnung der aktuellen Parameterausdrücke zu den formalen Parametern
BNF: Funktionsabstraktion
∗
hfunctioni ::= hidentifier | (hsortihidentifieri {, hsortihidentifieri} )hsorti : hexpi
Nebenbedingung: Die formalen Parameter werden sortenkonform im Rumpf verwendet.
Bemerkung: Die Funktionsabstraktion orientiert sich am λ-Kalkül von Alonso Church:
Es werden keine Sorten geschrieben und nur einstellige Funktionen betrachtet. Statt
nat
nat : x + 1 schreibt man im λ-Kalkül: λx : x + 1.
(nat
natx)nat
Funktionsabstraktion (bzw. λ-Notation) bietet uns die Möglichkeit, Ausdrücke zu
schreiben, deren Werte Funktionen sind.
Bemerkung:
Es gibt einen engen
Zusammenhang zur Schreibweise der Mengenlehre
·
·
·
·
nat
bool : x > 6 ∧ x − 8 < 2000.
x ∈ N : x > 6 ∧ x − 8 < 2000 : (nat
natx)bool
Durch die Kopfzeile einer Funktionsabstraktion entsteht eine Bindung für formale Parameter, der Rumpf entspricht dem Bindungsbereich, wir sprechen von der
4.2. DEKLARATIONEN IN APPLIKATIVEN SPRACHEN
43
Lebensdauer der Bindung (bzw. der Bezeichnung). Die Bindung ist für die Lebensdauer gültig, unterbrochen nur durch weitere Bindungen des gleichen Identifikators.
nat
nat :
(nat
natx)nat
?
bool
nat : if x then y · y else y + y fi)
if x = 0 then ((bool
boolx, nat
naty)nat
(0 < x, 2x) else 3xfi)(5)
Stets ist höchstens eine Bindung gültig. Die Lebensdauern können sich überlagern.
4.2
Deklarationen in applikativen / funktionalen
Sprachen
Beispiel:
nat
nat : 3x3 + 5x2 + 7x + 3)((a + b) − c)
((nat
natx)nat
bequemer:
nat
pnat
natx = a + b − c
3x3 + 5x2 + 7x + 3y
Deklarationen dienen einer Bindung eines Identifikators mit gleichzeitiger Wertbelegung. Dies ist oft lesbarer als die Funktionsabstraktion und -applikation. Deklarationen verwenden wir in Abschnitten (Blöcken); die Abschnittsklammern
begrenzen den Bindungsbereich.
Syntax:
hsectioni ::=phinneriy
∗
|ifhinnerithenhinneri {elifhinnerithenhinneri} elsehinnerifi
hinneri ::=hsort declarationi; hexpi
|helement declarationi; hexpi
∗
| {hfunction declarationi} ; hexpi
|hprocedural inneri
4.2.1
Elementdeklaration
Allgemeine Form:
sx = E
Elementdeklaration / -vereinbarung
Nebenbedingungen:
(1) Der Identifikator x kommt im Ausdruck E nicht frei vor.
(2) E hat die Sorte s .
Beispiel: Elementdeklaration
seqnat
pseqnat
seqnatx = h1 3 7i; x ◦ xy hat Wert h1 3 7 1 3 7i
if bool
boolb = true; b ⇒ ¬b then nat
natz = 7; z · z else 23 fi
Auch Schachtelungen sind erlaubt:
nat
nat
nat
pnat
natz = pnat
natz = 3; 3 · z − zy; pnat
natz = 5; z · zy + zy
44
KAPITEL 4. APPLIKATIVE / FUNKTIONALE SPRACHEN
Interpretation:
I[ssx = E] : EN V → EN V
(
β[Iβ [E]/x] falls Iβ [E] 6= ⊥
I[ssx = E](β) =
Ω
sonst
Ω ∈ EN V : Ω(z) = ⊥ ∀z
(
II[D](β) [E] falls I[D](β) 6= Ω
Iβ [pD; Ey] =
⊥
sonst
Auswertung von Abschnitten:
pssx = E; E 0 y → ((ssx)ss0 : E 0 )(E)
4.2.2
Funktionsdeklaration
Ebenso wie Elemente deklarieren wir Funktionen:
nat
nat : if x ≥ y then y else x fi
f ct
ctmin = (nat
natx, nat
naty)nat
Allgemeine Form:
f ct
ctf = (s1 x1 , . . . , sn xn )sn+1 : E
(4.3)
Syntax:
hfunction declarationi ::= f ct
cthidi = hfunction abstractioni
Achtung: In (4.3) darf f frei vorkommen, als Funktionsidentifikator für eine Funktion
der Sorte (s1 x1 , . . . , sn xn )sn+1 . In diesem Fall heißt die Deklaration rekursiv.
Regel zur Auswertung:
pff ct
ctf = F ; Ey → E[F/f ]
Regel für den rekursiven Fall: siehe später.
4.3
Rekursive Funktionsdeklarationen
Rekursive Funktionsdeklarationen bilden das Kernstück der funktionalen Programmierung. Typischerweise werden Programmieraufgaben wie folgt formuliert: Gib
eine Funktion(sdeklaration) an, die für folgende Argumente . . . einen Wert berechnet, der folgende Eigenschaften hat: . . . .
Beispiel: Rekursive Funktionen
(1) Umrechnen einer Binärzahl in ihren Wert (Binärzahldarstellung: seqbit
seqbit):
1
2
4
fct calcval = (seq bit x) nat:
if x ?= empty then 0
elif last(x) ?= L then 1 + 2*calcval(lrest(x))
else 2*calcval(lrest(x))
fi
(2) Revertieren einer Sequenz: Eine gegebene Sequenz soll in umgekehrte Reihenfolge gebracht werden.
4.3. REKURSIVE FUNKTIONSDEKLARATIONEN
1
2
4
45
fct revertiere = (seq data s) seq data:
if s ?= empty then empty
else conc(<last(s)>, revertiere(lrest(s)))
fi
(3) Feststellen, ob zwei Sequenzen über einer linear geordneten Menge in der
alphabetischen Ordnung stehen. (Sei data eine Menge von Elementen mit
linearer Ordnung ≤.)
1
2
4
6
fct isalpha = (seq data s, seq data t) bool:
if s ?= empty then true
elif t ?= empty then false
elif first(s) < first(t) then true
elif first(s) > first(t) then false
else isalpha (rest(s), rest(t))
fi
(4) Sortieren einer Sequenz durch Mischen: Wir betrachten Sequenzen von natürlichen Zahlen. Eine Sequenz heißt absteigend sortiert, wenn sorted(s) gilt, wobei:
1
2
4
fct sorted = (seq nat s) bool:
if length(s) <= 1 then true
else first(s) >= first(rest(s)) and sorted (rest(s))
fi
Mischen zweier sortierter Sequenzen:
1
2
4
6
fct merge = (seq nat s, seq nat t) seq nat:
if s ?= empty then t
elif t ?= empty then s
elif first(s) >= first(t) then
conc(<first(s)>, merge(rest(s), t))
else conc (<first(t)>, merge(s, rest(t)))
fi
Behauptung: sorted(s) ∧ sorted(t) ⇒ sorted(merge(s, t))
Sortieren durch Mischen:
1
2
4
6
fct mergesort = (seq nat s) seq nat:
if length(s) <= 1 then s
else nat h = length(s) / 2;
seq nat lpart = part(s, 1, h);
seq nat rpart = part(s, h+1, length(s));
merge (mergesort(lpart), mergesort(rpart))
fi
8
10
12
fct part = (seq nat s, nat a, nat b) seq nat:
if a > 1 then part (rest(s), a-1, b-1)
elif a ?= b then <first(s)>
else conc (<first(s)>, part(rest(s), 1, b-1))
fi
Bemerkungen:
46
KAPITEL 4. APPLIKATIVE / FUNKTIONALE SPRACHEN
(1) Es gilt sorted(mergesort(s)) für jede Sequenz s von natürlichen Zahlen.
(2) Rufen wir part(s, a, b) mit Werten für s, a, b auf, so dass 1 ≤ a ≤ b ≤ length(s)
nicht gilt, dann wird bei der Auswertung des Aufrufs irgendwann eine Operation auf ein Argument angewendet, bei der das Ergebnis bot, d.h. nicht
definiert ist (Bsp. 0 − 1, rest(empty)). DAnn ist das Ergebnis von part(s, a, b)
ebenfalls undefiniert. (Laufzeitfehler)
Wir wenden uns nun der genaueren Deutung rekursiver Deklarationen zu.
1
2
4
fct mod = (nat a, nat b) nat:
if a < b then a
else nod (a-b, b)
fi
Auswertung durch Termersetzung:
unfold
if-Regeln
unfold
if-Regeln
unfold
if-Regeln
mod(29, 9) −−−−→ if 29 < 9 then 29 else mod(29 − 9, 9) fi −−−−−−→
mod(20, 9) −−−−→ if 20 < 9 then 20 else mod(20 − 9, 9) fi −−−−−−→
mod(11, 9) −−−−→ if 11 < 9 then 11 else mod(11 − 9, 9) fi −−−−−−→
unfold
mod(2, 9) −−−−→
if 2 < 9 then 2 else mod(2, 9) fi
if-Regeln
−−−−−−→
2
Neben der Auswertung durch Termersetzung können wir die Bedeutung rekursiver
Funktionsdeklarationen auch durch
• Induktion
• Fixpunktfestlegung
beschreiben.
mx)n
n:E
f ct
ctf = (m
(4.4)
Idee: Dies entspricht einer Gleichung f (x) = E (Fixpunktgleichung). Wir verbinden
mit der Deklaration (4.4) eine (die?) Funktion, die die Fixpunktgleichung erfüllt.
Offene Fragen:
(1) Gibt es immer eine Lösung für die Fixpunktgleichung?
(2) Gibt es manchmal mehrere Lösungen, und wenn ja, welche verbinden wir dann
mit (4.4)?
Beispiel: (paradox)
nat
nat : f1 (x)
f ct
ctf1 = (nat
natx)nat
nat
nat : f2 (x) + 1
f ct
ctf2 = (nat
natx)nat
Gleichungen:
f1 (x) = f1 (x)
f2 (x) = f2 (x) + 1
4.3. REKURSIVE FUNKTIONSDEKLARATIONEN
4.3.1
47
Induktive Deutung rekursiver Funktionsdeklarationen
Idee: Wir approximieren für die rekursive Funktionsdeklaration
mx)n
n:E
f ct
ctf = (m
(4.5)
die deklarierte Funktion f induktiv, d.h. wir konstruieren eine Folge von Funktionen
f0 , f1 , f2 , . . ., deren Grenzwert f darstellt.
Mit (4.5) verbinden wir ein Funktional: Sei M die Trägermenge zur Sorte m , N die
Trägermenge zur Sorte n (jeweils inklusive ⊥).
τ : (M → N ) → (M → N )
mit (für g : M → N ):
τ [g] : M → N
definiert durch (sei a ∈ M )
(
Iβ 0 [E] falls a 6= ⊥
τ [g](a) =
⊥
sonst
Sei dabei β eine gegebene Belegung (die Belegung, in deren Kontext wir (4.5) betrachten) und
β 0 = β[g/f, a/x]
Beispiel:
1
2
4
fct fac = (nat x) nat:
if x ?= 0 then 1
else x * fac (x-1)
fi
Wir definieren nun induktiv:
f0 (a) = ⊥
fi+1 = τ [fi ]
Beispiel: (Fortsetzung)
a
0
1
2
3
4
5
fac0 (a)
⊥
⊥
⊥
⊥
⊥
⊥
fac1 (a)
1
⊥
⊥
⊥
⊥
⊥
fac2 (a)
1
1
⊥
⊥
⊥
⊥
fac3 (a)
1
1
2
⊥
⊥
⊥
fac4 (a)
1
1
2
6
⊥
⊥
(
a! falls a < i
faci (a) =
⊥ sonst
(Induktive Folge)
48
KAPITEL 4. APPLIKATIVE / FUNKTIONALE SPRACHEN
Beobachtung: Gilt fi (a) = b, b 6= ⊥ ⇒ ∀j ∈ N : j ≥ i : fj (a) = b.
Grenzwert:
(
fi (a) falls ∃i ∈ N : fi (a) 6= ⊥
f∞ (a) =
⊥
sonst
Wir können auch eine partielle Ordnung auf der Menge der Funktionen einführen:
f v g ⇔ ∀a ∈ M : f (a) = g(a) ∨ f (a) = ⊥
Idee: f v g heißt f approximiert g , d.h. hat für a ∈ M f (a) einen Wert b =
6 ⊥,
so gilt g(a) = b, d.h. g hat für a den gleichen Wert. Gild f (a) = ⊥, dann kann
g(a) = ⊥ gelten, oder auch nicht.
f approximiert die Information, die g trägt.
Aufgrund unserer Konstruktion gilt
fi v fi+1 ∀i ∈ N
Die überall undefinierte Funktion
Ω:M →N
mit
Ω(a) = ⊥∀a ∈ M
ist kleinstes Element, d.h.
Ω v g∀g : M → N
Allgemeine Definition zur Ordnungstheorie: Sei (M, v) eine partiell geordnete Menge. Für jede Teilmenge M 0 ⊆ M heißt ein Element x ∈ M kleinste obere
Schranke von M 0 , falls gilt:
(1) x ist obere Schranke von M 0 , d.h. ∀y ∈ M 0 : y v x
(2) x ist kleinste obere Schranke, d.h. für jede obere Schranke z ∈ M gilt x ⊆ z.
Achtung: Nicht für jede Menge M 0 ⊆ M existiert solch eine kleinste obere Schranke.
Falls die kleinste obere Schranke existiert, schreiben wir
lub(M 0 )
(least upper bound)
Eine Kette
x0 v x1 v x2 v . . .
ist eine Menge
{xi ∈ M : i ∈ N} mit xi v xi+1 ∀i ∈ N
Eine Menge heißt kettenvollständig (auch vollständig partiell geordnet),
wenn jede Kette eine kleinste obere Schranke besitzt.
Anmerkung: Die oben definierten Funktionen fi bilden eine Kette mit
f∞ = lub {fi : i ∈ N}
Sei tau eine Abbildung auf geordneten Mengen. τ heißt monoton, wenn gilt:
x1 v x2 ⇒ τ (x1 ) v τ (x2 )
Anmerkung: In der obigen Konstruktion
f0 = Ω
fi+1 = τ (fi )
ist τ monoton: fi v fi+1
Beweis durch Induktion und Monotonie:
fi v fi+1
f0 = Ω v f1
⇒ τ [fi ] v τ [fi+1 ] = fi+1 v fi+2
4.4. REKURSIONSFORMEN
4.3.2
49
Deutung rekursiv deklarierter Funktionen durch
kleinste Fixpunkte
Seien alle Definitionen wie oben.
Satz von Knaster-Tarski:
Sei (M, v) partiell geordnete Menge, kettenvollständig mit kleinstem Element; sei
τ : M → M monoton. Dann existiert ein kleinster Fixpunkt, d.h. eine kleinste
Lösung der Gleichung x = τ [x].
Genauer:
(1) Es existiert eine Lösung der Gleichung x = τ [x].
(2) Es existiert eine kleinste Lösung x0 , d.h. x0 = τ [x0 ] und x = τ [x] ⇒ x0 v x.
x0 heißt kleinster Fixpunkt von τ . Wir bezeichnen x0 auch mit fix τ .
Im Fall rekursiv definierter Funktionen gilt:
fix τ = lub {fi : i ∈ N}
Beispiel:
Die Fakultätsfunktion ist Fixpunkt von τ für
(
1
fallx a = 0
τ [g](x) =
g(a − 1) · a sonst
d.h.
a! = if a = 0 then 1 else (a − 1)!a fi
4.4
Rekursionsformen
Die Art und Weise, wie im Rumpf rekursiv definierter Funktionen rekursive Aufrufe
auftreten, ist für die Klassifizierung von Rekursion von Bedeutung.
4.4.1
Lineare Rekursion
Wir sprechen von linearer Rekursion, wenn jeder Aufruf im Rumpf zu höchstens
einem weiteren rekursiven Aufruf führt. Dies gilt insbesondere, wenn im Rumpf
genau ein Aufruf der rekursiv definierten Funktion auftritt, bzw. wenn in jedem
Zweig der if - Anweisung im Rumpf höchstens ein Aufruf vorkommt. Wir erhalten
eine lineare Folge von Aufrufen.
Beispiel:
• Fakultät
fib(4)
|
fib(3)
|
fib(2)
|
fib(1)
|
fib(0)
50
KAPITEL 4. APPLIKATIVE / FUNKTIONALE SPRACHEN
• Aussortieren von Elementen aus einer Sequenz (von natürlichen Zahlen), die
kleiner sind als ein gegebenes Element:
1
2
4
fct lp
if s
elif
else
fi
= (seq nat s, nat a) seq nat:
?= empty then empty
first(s) < a then conc (<first(s)>, lp(rest(s)))
lp(rest(s))
(Analog: hp (alle Elemente, die größer sind als das gegebene Element) und ep
(Elemente gleich dem gegebenen Element))
4.4.2
Repetitive Rekursion
Eine lineare Rekursion heißt repetitiv, wenn der Rumpf folgende Form hat:
1
2
4
6
fct f = (m1 x1, ..., mn xn) m:
if B1 then ...
elif B2 then f(E1, ..., En)
...
else ...
fi
Dabei stellen E1, . . . , En keine rekursiven Aufrufe dar.
Genauer: Alle rekursiven Aufrufe sind von einer Form, dass auf das Ergebnis keine
weiteren Operationen mehr angewendet werdne.
Beispiel: Repetitive Fakultätsberechnung
1
2
4
fct repfac = (nat x, nat y) nat:
if x ?= 0 then y
else repfac (x-1, y*x)
fi
Wichtig: Repetitive Rekursion benötigt keine Stapel von Zwischenergebnissen.
4.4.3
Kaskadenartige Rekursion
Bei nichtlinearer Rekursion tritt in gewissen Zweigen mehr als ein rekursiver Aufruf
auf.
1. Fall: Die Aufrufe sind unabhängig voneinander, d.h. keiner der Aufrufe tritt in
den aktuellen Parameterausdrücken eines anderen rekursiven Aufrufs auf (→
Kaskadenrekursion).
2. Fall: Ein rekursiver Aufruf ist im aktuellen Parameterausdruck eines anderen rekursiven Aufrufs.
Beispiel: Quicksort
Idee: absteigend sortieren durch Aufspalten
4.4. REKURSIONSFORMEN
1
2
4
51
fct qs = (seq nat s) seq nat:
if length(s) <= 1 then s
else nat x = first (s);
conc (qs(hp(s, x)), conc (ep(s, x), qs(lp(s, x))))
fi
Dieser Algorithmus erzeugt eine Kaskade von Aufrufen (einen sogenannqs
ten Aufrufbaum).
qs(hp. . . )
qs(lp. . . )
qs(hp. . . ) qs(lp. . . ) qs(hp. . . ) qs(lp. . . )
4.4.4
Vernestete Rekursion
Beispiel: Ackermann
1
2
4
fct ackermann = (nat m, nat n) nat:
if m ?= 0 then n+1
elif n ?= 0 then ackermann(m-1, 1)
else ackermann(m-1, ackermann(m, n-1))
fi
ack : N × N → N


falls m = 0
n + 1
ack(m, n) = ack(m − 1, n)
falls n = 0 ∧ m 6= 0


ack(m − 1, ack(m, n − 1)) sonst
Die Anzahl der rekursiven Aufrufe hängt empfindlich von m ab.
Bemerkung: Die Ackermannfunktion ist ein wichtiges Beispiel bei der Diskussion
der Rekursion.
Spezialfälle
Bm (n) : N → N
Bm (n) = ack(m, n)
Es gilt:
B0 (n) = n + 1
B1 (n) = n + 2
B2 (n) = 2n + 3
(Nachfolgerfunktion)
(Addition von 2)
(Multiplikation)
B3 (n) = 2n+3 − 3
B4 (n) = . . .
(Exponentialfunktion)
(keine einfache math. Beschreibung)
Bemerkung: Aus praktischer Sicht ist die Bedeutung der vernesteten Rekursion
gering.
52
KAPITEL 4. APPLIKATIVE / FUNKTIONALE SPRACHEN
4.4.5
Verschränkte Rekursion
Gegeben sei ein System rekursiver Funktionsdeklarationen:
f ct
ctf1 = (m1 x1 )n1 : E1
..
.
f ct
ctfk = (mk xk )nk : Ek
Treten in E1 , . . . , Ek die Funktionsbezeichner f1 , . . . , fk (k ≥ 2) auf, so spricht mann
von verschränkter Rekursion.
Beispiel: Teilbarkeit einer Zahl durch 3
Funktionsaufruf: rest0(n).
1
2
4
6
8
fct rest0 = (nat x) bool:
if n ?= 0 then true
else rest1(n-1)
fi
fct rest1 = (nat x) bool:
if n ?= 0 then false
else rest2(n-1)
fi
10
12
14
fct rest2 = (nat x) bool:
if n ?= 0 then false
else rest0(n-1)
fi
Stützgraph: graphisches Hilfsmittel zur Beschreibung der im Rumpf einer rekursiven
Funktion auftretenden Funktionen.
4.5
4.5.1
Techniken applikativer Programmierung
Spezifikation der Aufgabenstellung
Erstes Prinzip der disziplinierten Programmierung
Niemals sollte mit der eigentlichen Programmierung begonnen werden, bevor
die Aufgabenstellung genau verstanden und beschrieben ist.
Erläuterung: Eine formale, mathematische Beschreibung einer Aufgabenstellung
nennt man formale Spezifikation. Diese Anforderungsspezifikation ist im Idealfall
mit einer formalen Sprache formuliert.
Vorgehensweise bei formaler Spezifikation
(1) Spezifikation des Funktionsraumes und Parameterbeschränkung.
• mathematisch:
f : M1 × . . . × Mn → Mn+1
∀x1 ∈ M1 , . . . , xn ∈ Mn : ¬Q (x1 , . . . , xn ) ⇒ f (x1 , . . . , xn ) = ⊥
4.5. TECHNIKEN APPLIKATIVER PROGRAMMIERUNG
53
• in Pseudocode:
f ct f = (m1 x1 , . . . , mn xn : Q(x1 , . . . , xn ))mn+1
(2) Spezifikation des Werteverlaufs.
• mathematisch:
f : M1 × . . . × Mn → Mn+1
∀x1 ∈ M1 , . . . , xn ∈ Mn : ¬Q(x1 , . . . , xn ) ⇒ f (x1 , . . . , xn ) = ⊥
∀x1 ∈ M1 , . . . , xn ∈ Mn : Q(x1 , . . . , xn ) ⇒ R(x1 , . . . , xn , f (x1 , . . . , xn ))
• in Pseudocode:
f ct f = (m1 x1 , . . . , mn xn : Q(x1 , . . . , xn ))mn+1 :
some mn+1 y : R(x1 , . . . , xn , y)
Beispiel: ganzzahlige Division
(1) Spezifikation des Funktionsraumes und Beschränkung der Parameter.
• mathematisch:
div : N × N → N
∀x ∈ N, y ∈ N : ¬(y > 0) ⇒ div(x, y) = ⊥
• in Pseudocode:
nat
f ct div = (nat
natx, nat
naty : y > 0)nat
(2) Spezifikation des Werteverlaufs.
• mathematisch:
∀x ∈ N, y ∈ N : (y > 0) ⇒ ∃r ∈ N :
x = y · div(x, y) + r ∧ y > r ≥ 0
• in Pseudocode:
nat
f ct div = (nat
natx, nat
naty : y > 0)nat
nat r : x = y · z + r ∧ y > r ≥ 0
some nat z : ∃nat
Definition: Korrektheit
Eine Rechenvorschrift heißt partiell korrekt bezüglich einer Aufgabenstellung, wenn sie für alle zulässigen Argumente, für die sie terminiert, ein bezüglich
der Aufgabenstellung korrektes Resultat liefert.
Eine Rechenvorschrift heißt total korrekt, wenn sie partiell korrekt ist und
zusätzlich in allen durch die Aufgabenstellung geforderten Fälle terminiert.
Bemerkungen:
• Die i-te Iterierte der Fakultätsfunktion ist partiell korrekt, denn faci berechnet
die Fakultätsfunktion für n < i; für n ≥ i ist faci (n) = ⊥.
• faci ist nicht total korrekt.
• Eine total korrekte Rechenvorschrift kann für Argumente, die bezüglich der
Aufgabenstellung nicht terminieren, dennoch ein definiertes Resultat liefern.
54
KAPITEL 4. APPLIKATIVE / FUNKTIONALE SPRACHEN
• Zusammenfassung:
fpart v fspec v ftot
Wichtig: Systematisches Vorgehen bei der Entwicklung von Programmen!
Fragen
Entwicklungsphase
Welche Aufgabe ist zu lösen?
Anforderungsspezifikation
Welche Lösungsidee, wie strukturieren? Entwurf / Design
Wie im Detail ausprogrammieren?
Implementierung
Löst das Programm die Aufgabe?
Verifikation der Korrektheit
Wie effizient ist das Programm?
Performance / Komplexität
4.5.2
Strukturierung und Entwurf
Liegt eine Spezifikation vor, d.h. eine hinreichend genaue Beschreibung einer zu implementierenden Funktion, so ist der nächste Schritt, zu überlegen, wie die Aufgabe
implementiert werden soll:
(1) Direkte Angabe einer Funktionsdeklaration (u.U. mit Rekursion). (Suche nach
Algorithmus)
(2) Zergliederung der Aufgabe in Teilaufgaben. (divide and conquer) (z.B. Quicksort)
(3) Verallgemeinerung der Aufgabenstellung.
Grund:
(a) verallgemeinerte Aufgabe einfacher oder effizienter zu lösen
(b) Ergebnis öfter einsetzbar
Beispiel: Fibonacci
1
2
4
fct fib = (nat n) nat:
if n <= 1 then n
else fib (n-2) + fib (n-1)
fi
Aufrufbaum von fib:
fib(n)
fib(n − 2)
fib(n − 1)
fib(n − 3)
fib(n − 2)
fib(n − 5) fib(n − 4) fib(n − 4) fib(n − 3)
Der selbe Funktionsaufruf kommt öfter vor.
Verallgemeinerung:
fibe(n, 1, 0) = fib(n + 1)
1
2
4
fct fibe = (nat n, nat a, nat b) nat:
if n ?= 0 then a
else fibe (n-1, a+b, a)
fi
offen: Beweis von (4.6).
(4.6)
4.5. TECHNIKEN APPLIKATIVER PROGRAMMIERUNG
55
Beispiel: Sortieren durch Einbettung
Sortieren duch Einsortieren:
1
2
4
6
8
10
12
fct insertsort = (seq nat s) seq nat: insertseq (empty, s)
fct insertseq = (seq nat s, seq nat r: sorted(s)): seq nat:
if r ?= empty then s
else insertseq (insert (s, first(r)), rest(r))
fi
fct insert = (seq nat s, nat x) seq nat:
if s ?= empty then <x>
elif x >= first(s) then conc (<x>, s)
else conc (first(s), insert (rest(s), x))
fi
Sortieren durch Auswählen:
1
2
4
6
fct selectsort = (seq nat s) seq nat: appsortseq (empty, s)
fct appsortseq = (seq nat s, seq nat r: sortes(s)) seq nat:
if r ?= empty then s
else nat max = selmax(r);
appsortseq (conc (s, <max>), del(r, max))
fi
8
10
12
14
fct selmax = (seq nat s: s <> empty) nat:
if length(s) = 1 then first(s)
else nat max = selmax(rest(s));
if max >= first(s) then max
else first(s)
fi
fi
16
18
20
fct del = (seq nat r, nat x) seq nat:
if r ?= empty then empty
elif first(r) = x then rest(r)
else conc (<first(r)>, del (rest, r))
fi
4.5.3
Hinweise zur Strukturierung der Aufgabenstellung und
der Lösung
Prinzipien:
(1) Einführung geeigneter Hilfsfunktionen und Begriffe (auch schon in der Spezifikation).
(2) Hilfsfunktionen sollten für sich eigenständig zu verstehen sein.
(3) Die Rümpfe der Funktionen sollen von überschaubarer Größe sein (max
Seiten).
(4) Klare Gesamtstruktur des Programms (Architektur).
1
2
−1
56
KAPITEL 4. APPLIKATIVE / FUNKTIONALE SPRACHEN
Beispiel: Hilfsfunktionen und Begriffe
sort(s) = r ⇔ s ∼ r ∧ sorted(r)
s ∼ r:
sorted(r):
s und r haben die gleichen Elemente
r ist sortiert
Ziele:
• Lesbarkeit
• Dokumentation (Erläuterungen einfügen)
• Unabhängigkeit der Lösung der Teilaufgaben (Schnittstellen-Beschreibung der
Teilaufgaben)
• Performance
• Sicherstellung der Korrektheit
4.5.4
Rekursion und Spezifikation
Neben der Zerlegung von Aufgaben in Teilaufgaben ist die Ableitung von rekursiven
Deklarationen die Hauptaufgabe in der Implementierung. Hier hilft es oft, geeignete
Gleichungen abzuleiten, die dann die Grundlage für die Rekursion bilden.
4.5.5
Parameterunterdrückung
Verändern sich bestimmte Parameter in der Rekursion nicht, so kann ich eine Hilfsfunktion angeben, die diese Parameter nicht enthält, sondern als globale Identifikatoren nutzt.
Ausbesserung der Funktion del:
1
2
4
6
fct del = (seq nat r, nat x) seq nat:
| fct h = (seq nat r) seq nat:
if r ?= empty then empty
elif first(r) ?= x then rest(r)
else conc (<first(r)>, h (rest(r)))
fi
h(r)
_|
4.5.6
Effizienz
Definition: Effizienz eines Programms / Algorithmus
Unter der Effizienz eines Programms verstehen wir den Aufwand, der bei der
Ausführung des Programms erforderlich ist, im Verhältnis zur Aufgabenstellung.
Wir messen für funktionale Programme den Aufwand wie folgt:
• Speicheraufwand: wie viele Hilfsgrößen müssen gleichzeitig gespeichert werden?
• Rechenaufwand: Anzahl der erforderlichen Rechenoperationen
• Rechenzeit (Antwortzeit)
Um diese allgemeinen Definitionen besser greifen zu können, führen wir Funktionen
4.5. TECHNIKEN APPLIKATIVER PROGRAMMIERUNG
57
ein, die den jeweiligen Aufwand messen.
Sei die Aufgabe berechne fs : N → N gegeben. Lösung (Rekursion):
nat x)nat
nat : . . .
f ct f = (nat
(4.7)
Wir definieren drei Gewichtsfunktionen:
(1) fa : N → N (Berechnungsaufwand)
fa (n) entspricht der Anzahl der Rechenoperationen bei der Auswertung des
Aufrufs f (n) nach (4.7).
(2) fb : N → N (Speicheraufwand)
fb (n) bezeichnet die maximale Anzahl geschachtelter Bindungen bei der Auswertung des Aufrufs f (n) nach (4.7).
(3) fc : N → N
fc (n) bezeichnet die Rekursionstiefe, d.h. die Höhe des Aufrufbaums für f (n)
nach (4.7).
Beispiel: fibe(10, 1, 0)
Rechenaufwand:
Rechenoperationen:
?
n=0
n−1
n+b
fibea (10, 1, 0) ≈ 30
fibea (n, a, b) ≈ 3 · n
Seien f1 und f2 rekursiv definierte Funktionen, die die selbe Aufgabenstellung lösen.
Wir sagen f1 ist effizienter alls f2 , wenn gilt:
f1 a (x) f2 a (x) ∧ f1 b (x) f2 b (x)
Beispiel: (Fortsetzung)
1
2
4
fct fib_a = (nat n) nat:
if n <= 1 then 1
else 1 + fib_a (n-1) + fib_b (n-2)
fi
Frage: Von welcher Ordnung ist der Berechnungsaufwand? Wie stark wächst der
Aufwand mit wachsendem Parameter n?
Wir sagen, eine Funktion f : N → N wächst mit Ordnung g : N → N, falls ∃k, i, j ∈
N, i > 0, k > 0:
∀n ∈ N : j ≤ n ⇒ g(n) ≤ i · f (n) ≤ k · g(n)
Notation: f (n) ≈ O(g(x)).
58
KAPITEL 4. APPLIKATIVE / FUNKTIONALE SPRACHEN
Beispiel:
fiba (n) = O(2n )
fibea (n) = O(n)
exponentiell
linear
Speicheraufwand:
1
2
4
fct fib_b = (nat n) nat:
if n <= 1 then 1
else 1 + max (fib_b (n-2), fib_b (n-1))
fi
Es gilt fibb (n) = O(n).
Rekursionstiefe: fibc = fibb .
Die Effizienz von Algorithmen wird in der Informatik unter den Stichworten Komplexität und Komplexitätstheorie analysiert.
4.5.7
Dokumentation
Die Qualität eines Programms wird nicht nur durch seine Korrektheit (Programm
liefert korrekte Ergebnisse) und seine Effizienz bestimmt, sondern auch durch seine
Lesbarkeit, Verständlichkeit und Änderbarkeit. Entscheidend dabei: Dokumentation
4.5.8
Test / Integration von Programmen
Programmieren ist fehleranfällig. Folglich sind fertiggestellte Programme sorgfältig
auf Fehler zu überprüfen. Dazu bieten sich folgende Verfahren an:
• Inspektion: Durchsicht von Programmen auf Fehler
• Review: Durchsprache von Programmen im Team
• Testen: Probeläufe von Programmen für ausgewählte Eingabewerte (sog. Testfälle)
• Verifikation: Nachweis der Korrektheit mit logischen Mitteln
Integration: Die Aufgabe des Zusammensetzens größerer Programme aus kleineren
Programmeinheiten.
4.6
Korrektheitsbeweise
Funktionale Programme entsprechen mathematischen Deklarationen und können
direkt mit Mitteln der Logik und Mathematik analysiert werden.
Beispiel: Fibonacci
n ≤ 1 ⇒ fib(n) = n
n > 1 ⇒ fib(n) = fib(n − 1) + fib(n − 2)
4.6. KORREKTHEITSBEWEISE
59
m x)n
n : E entspricht der Gleichung f (x) = E
Eine rekursive Deklaration f ct f = (m
(Fixpunktgleichung) für alle x der Sorte m , x 6= ⊥.
Ferner: mit
kleinste Fixpunkt - Eigenschaft
f0 (x) = ⊥
fi+1 (x) = E[fi /f ]
gilt
f (x) = ⊥ ⇔ ∀i ∈ N : fi (x) = ⊥
4.6.1
Induktion und Rekursion
Induktion und Rekursion sind eng verwandt. Viele Rekursionen entsprechen einem induktiven Prinzip. Um Beweise über rekursiv definierte Funktionen führen zu
können, verwenden wir oft erfolgreich Induktion.
Beispiel:
Beweis: 2n ≤ fib(2n + 1) ≤ 22n+1 Induktionsanfang: n = 0
20 ≤ fib(1) ≤ 21
1≤1≤2
Induktionsannahme:
2n ≤ fib(2n + 1) ≤ 22n+1
Induktionsschritt (exemplarisch nur für 2n ≤ fib(2n + 1))
2n+1 ≤ 2 · fib(2n + 1)
≤ fib(2n + 1) + fib(2n + 2)
= fib(2n + 3)
= fib (2 (n + 1) + 1)
Mit Hilfe von Rekursion können wir insbesondere beweisen, dass
(1) die rekursiv definierte Funktion f einer spezifizierten Funktion fs entspricht,
d.h. f = fs bzw. ∀x : f (x) = fs (x) (Korrektheit).
(2) Partielle Korrektheit: f v fs bzw. ∀x : f (x) v fs (x)
Beispiel:
1
2
4
fct rev = (seq m x) seq m:
if length(x) <= 1 then x
else conc (<last(x)>, conc (rest(lrest(x)), <first(x)>))
fi
Behauptung:
rev(hx1 . . . xn i) = hxn . . . x1 i
Beweis: Die Rekursion liefert
length(x) ≤ 1 ⇒ rev(x) = x
length(x) > 1 ⇒ rev(x) = hlast(x)i ◦ rev(rest(lrest(x))) ◦ hfirst(x)i
n ≥ 2 ⇒ rev(hx1 . . . xn i) = hxn i ◦ rev(hx2 . . . xn−1 i) ◦ hx1 i
60
KAPITEL 4. APPLIKATIVE / FUNKTIONALE SPRACHEN
Induktion über n = length(x): n ≤ 1: trivial
Induktionshypothese: rev(hx1 . . . x0n i) = hx0n . . . x1 i
Induktionsschritt:
rev(hz1 . . . zn+1 i) = hzn+1 i ◦ rev(hz2 . . . zn i) ◦ hz1 i
= hzn+1 i ◦ hzn . . . z2 i ◦ hz1 i
= hzn+1 zn . . . z2 z1 i
Zur Effizienz von rev: (Achtung: rev arbeitet auf Sequenzen, nicht auf
Zahlen!)
1
2
4
fct rev_a = (seq m x) nat:
if length(x) <= 1 then 1
else 6 + rev_a (rest (lrest(x)))
fi
reva (x) ≈ 3 length(x)
Rechenaufwand: Linear in Länge der Sequenz
Beispiel: log
1
2
4
fct log = (nat n) nat:
if n ?= 1 then 0
else log (n / 2) + 1
fi
n = 1 ⇒ log(n) = 0
n 6= 1 ⇒ log(n) = log(n/2) + 1
Behauptung:


⊥ falls n = 0
log(n) = 0 falls n = 1


k für 2k ≤ n < 2k+1
Beweis: Induktion
∀i ∈ N : logi (n) = ⊥, wobei
log0 (n) = ⊥ logi+1 (n)
= if n = 1 then 0 else logi (n/2) + 1 f i
Wir zeigen: logi (0) = ⊥ durch Induktion über i ∈ N. Beweis: trivial
Der Rest der Behauptung folgt durch Induktion über die Fixpunktgleichung
4.6.2
Terminierungsbeweise
Gegeben: Rekursive Deklaration
m x)n
n:E
f ct f = (m
Sei P ein Prädikat auf den Elementen der Sorte m . Wir wollen folgende Aussage
beweisen:
P (x) ⇒ f (x) 6= ⊥
d.h. für alle x, für die P (x) gilt, terminiert die Rekursion für f .
4.6. KORREKTHEITSBEWEISE
61
Beispiel:
1
2
4
fct f = (nat a, nat b) nat:
if a < b then b
else f (a, 2b)
fi
Frage: Für welche Parameterwerte terminiert f ? f terminiert für Werte
b > 0.
Gesucht: Beweis von
b > 0 ⇒ f (a, b) 6= ⊥
Prinzip: Verwendung einer Abstiegsfunktion
Wir suchen eine Funktion
h : M− → N
Abstiegsfunktion
wobei M − die Menge der Elemente zur Sorte m ist (ohne ⊥).
h sei eine Abschätzung für die Höhe des Aufrufbaums. Wir beweisen durch Induktion über k ∈ N:
P (x) ∧ h(x) ≤ k ⇒ f (x) 6= ⊥
Induktionsanfang (k = 0):
P (x) ∧ h(x) = 0 ⇒ f (x) 6= ⊥
Induktionsvoraussetzung:
P (x) ∧ h(x) ≤ k ⇒ f (x) 6= ⊥
Induktionsschritt: zeige
P (x) ∧ h(x) ≤ k + 1 ⇒ f (x) 6= ⊥
(4.8)
Durchführung des Beweises:
Wir wissen: f (x) = E. Um (4.8) zu zeigen, zeigen wir
P (x) ∧ h(x) ≤ k + 1 ⇒ E 6= ⊥
Wenn wir h so wählen, dass alle Aufrufe in E eine Bewertung haben, die kleiner als
k + 1 ist, so können wir die Induktionsannahme einsetzen, d.h. ist f (E0 ) ein Aufruf
in E, der unter der Bedingung C stattfindet, so ist zu zeigen:
C ⇒ h(E0 ) < h(x)
h(E0 ) < h(x) ≤ k + 1
h(E0 ) ≤ k
Beispiel: (Fortsetzung)
Gesucht: h : N × N → N mit
b > 0 ∧ a ≥ b ⇒ h(a, 2b) < h(a, b)
a < b ⇐ h(a, b) = 0
(
0
falls a < b
h(a, b) =
a + 1 − b falls a ≥ b
a + 1 − 2b < a + 1 − b ⇐ b > 0 ∧ a ≥ b
Der entscheidende Schritt beim Terminierungsbeweis ist die Angabe einer geeigneten Abstiegsfunktion.
62
KAPITEL 4. APPLIKATIVE / FUNKTIONALE SPRACHEN
Kapitel 5
Zuweisungsorientierte
Programmierung
Grundidee: Ein Programm besteht aus einer Folge von Anweisungen, die maschinell
(d.h. durch eine Rechenanlage) ausführbar sind.
Frage: Was ist eine Anweisung (ein Befehl)? Eine Anweisung entspricht einer Aufforderung zur Änderung eines Zustands. Damit ist die Idee der Anweisung eng
verbunden mit der Idee des Zustands. Auf Rechnern entspricht dem Zustand die
Belegung der Speicherzellen und der Register. Dabei entspricht jede Ausführung
einer Anweisung der Änderung gewisser Speicherzellen und Register (von Neumann
- Maschine).
5.1
Anweisungen
Ein Programm besteht in der zuweisungsorientierten Programmierung aus Anweisungen. Diese lassen sich auch zu größeren Programmen zusammensetzen.
5.1.1
Syntax
Syntax der Anweisungen (engl.
statements):
hstatementi ::= nop
| abort
|hassignment statementi
|hsequential compositioni
|hconditional statementi
|hwhile statementi
|hblocki
|hprocedure calli
63
64
5.1.2
KAPITEL 5. ZUWEISUNGSORIENTIERTE PROGRAMMIERUNG
Programmvariable und Zuweisungen
In der funktionalen Programmierung verwenden wir Bezeichner / Identifikatoren
zusammen mit Bindungen:
nat x = 3;
pnat
..
.
. . .x . . .
..
.
y
Grundidee der Programmvariablen: Die Bindung kann (dynamisch) geändert werden.
var nat
natv := 3;
..
.
v := v + 1
..
.
Zuweisung
Bedeutung der Zuweisung:
vneu = valt + 1
valt
Wert von v im Zustand vor Ausführung der Zuweisung
vneu Wert von v im Zustand nach Ausführung der Zuweisung
andere Notation: v 0 = v + 1.
5.1.3
Zustände
Ein Zustand eines Programms ist durch die Belegung seiner Programmvariablen
gegeben. Sei ID die Menge der Programmvariablen, W die Menge der Werte.
EN V = {σ : ID → W }
EN V : Belegungen
ST AT E = EN V ∪ {⊥}
Zustand ∼ Belegung
Anweisung ∼ erzeugt neuen Zustand
5.1.4
Funktionale Bedeutung von Anweisungen
Jede Anweisung entspricht einer Abbildung
ST AT E → ST AT E
Interpretation für Anweisungen (hstatementi . . . Menge der Anweisungen):
I : hstatementi → (ST AT E → ST AT E)
5.1.5
Operationelle Bedeutung von Anweisungen
Operationell entspricht jede Anweisung einer Folge von Operationen einer Rechenanlage (s. später).
5.2. EINFACHE ANWEISUNGEN
5.2
65
Einfache Anweisungen
Wir benutzen drei einfache Anweisungen:
nop
abort
tue nichts
werde nie fertig
I[nop](σ) = σ
I[abort](σ) = ⊥
Wichtiger: die Zuweisung v := v + 1
(
σ[σ[v] + 1/v] für σ =
6 ⊥
I[v := v + 1](σ) =
⊥
für σ = ⊥
Allgemeine Form der Zuweisung:
v1 , . . . , vn := E1 , . . . , En
Kollektivzuweisung
In BNF:
∗
∗
hassignment statementi ::= hidi {, hidi} := hexpi {, hexpi}
Kontextbedingungen:
• Anzahl und Sorte der Identifikatoren links von
Sorte der Ausdrücke rechts
:= entspricht Anzahl und
• Identifikatoren werden konsistent bezüglich ihrer Sorten verwendet
Beispiel:
x, y
x, y
x, y
x, y
:= x − 1, y + 1
:= x − 1, y + 1, z
ungültig
:= x − 1, x ∨ y
ungültig
:= y, x
Vertauschung der Belegungen
I[v1 , . . . , vn := E1 , . . . , En ](σ) =
(
σ[Iσ [E1 ]/v1 , . . . , Iσ [En ]/vn ] falls σ 6= ⊥ ∧ ∀i : 1 ≤ i ≤ n : Iσ [Ei ] 6= ⊥
=
⊥
sonst
5.3
Zusammengesetzte Anweisungen
Aus den Einzelanweisungen können wir Anweisungen zusammensetzen.
5.3.1
Sequentielle Komposition
Seien S1, S2 gegebene Anweisungen. Dann ist S1; S2 eine zusammengesetzte
Anweisung: Führe zuerst S1 und danach S2 aus.
hsequential compositioni ::= hstatementi; hstatementi
66
KAPITEL 5. ZUWEISUNGSORIENTIERTE PROGRAMMIERUNG
Beispiel:
z := x; x := y; y := z
(Vertauschen der Belegung von x und y; z ist Hilfsvariable und erhält
den Wert von x)
x := x + 1; x := x + 1
(Entspricht x := x + 2)
Kontextbedingungen:
• Sorten der Bezeichner in S1 und S2 sind konsistent
I[S1; S2](σ) = I[S2] (I[S1] (σ))
Regeln:
S; nop
nop; S
abort; S
S; abort
(S1; S2); S3
5.3.2
entspricht
entspricht
entspricht
entspricht
entspricht
(Funktionskomposition)
S
S
abort
abort
S1; (S2; S3) (⇒ Klammern unnötig)
Bedingte Anweisung
Gegeben: Anweisungen S1, S2, boolescher Ausdruck C
if C then S1 else S2 f i
Zusammenhang zwischen bedingtem Ausdruck und bedingter Anweisung:
if C then x := E1 else x := E2 f i
entspricht
x := if C then E1 else E2 f i
Syntax:
if hexpithen
then
hconditional statementi ::=if
thenhstatementi
∗
elif hexpithen
then
{elif
thenhstatementi}
else
{else
elsehstatementi}
fi
Nebenbedingungen:
• Der Ausdruck C hat Sorte bool
bool.
• Die in C, S1 und S2 auftretenden Identifikatoren besitzen konsistente Sorten.
Semantik:


I[S1](σ) falls Iσ [C] = L ∧ σ 6= ⊥
if C then S1 else S2 f ii](σ) = I[S2](σ) falls Iσ [C] = 0 ∧ σ 6= ⊥
I[if


⊥
falls σ = ⊥ ∨ Iσ [C] = ⊥
Spezialfälle (notationelle Vereinfachungen):
5.3. ZUSAMMENGESETZTE ANWEISUNGEN
(1)
if C then S f i
steht für
if C then S else nop f i
(2)
if C then S elif C 0 then S 0 . . . f i
steht für
if C then S else if C 0 then S 0 . . . f i f i
(3) Case - Anweisung: In manchen Programmiersprachen schreibt man
case E of E1 : S1
..
.
En : Sn
else : Sn+1 esac
?
?
für if E = E1 then S elif . . . elif E = En then Sn else Sn+1 f i
5.3.3
Wiederholungsanweisung
Sei C ein boolescher Ausdruck, S eine Anweisung. Dann ist
while C do S od
eine Anweisung, genannt Wiederholungsanweisung.
Idee: Solange C true ist, wird S ausgeführt.
Nebenbedingungen:
• C hat Sorte bool
bool.
• Identifikatoren in C und S besitzen konsistente Sorten.
Beispiel:
Seien x, y Variable der Sorte nat
nat.
1
2
while x >= y do
r, x := r+1, x-y
od
Diese Programm berechnet auf r das Resultat der ganzzahligen Division
von x und y (genauer: von den Werten, die x und y besitzen, bevor wir
das Programm ausführen). Falls y = 0 gilt: Die Wiederholung terminiert
nicht!
Bedeutung der Wiederholungsanweisung: Es gilt die Fixpunktgleichung
while C do S od ≡ if C then S; while C do S od else nop f i
Wir definieren die Bedeutung von while durch den schwächsten Fixpunkt von
τ : (ST AT E → ST AT E) → (ST AT E → ST AT E)
67
68
KAPITEL 5. ZUWEISUNGSORIENTIERTE PROGRAMMIERUNG
wobei (für g : ST AT E → ST AT E):


6 ⊥ ∧ Iσ [C] = L
g (I[S](σ)) falls σ =
τ [g](σ) = σ
falls σ =
6 ⊥ ∧ Iσ [C] = 0


⊥
sonst
while C do S od
I[while
od] = f ix
ixτ
Bemerkung: Zusammenhang Wiederholung - Rekursion
while C do x := E od kann durch x := f (x) ersetzt werden, wobei fir f wie folgt
deklarieren:
1
2
4
fct f = (m x) m:
if C then f(E)
else x
fi
(repetitive Rekursion)
Beispiel: Suchen eines Elements in einer Sequenz
Gegeben: Sequenz s durch var seq m
m, x der Sorte m . Gesucht: Zahl k,
var bool
die der Position von x in s entspricht. found (var
bool) gibt an, ob x in
s vorkommt.
1
2
4
6
found, k := false, 1;
while (length(s) > 0) and (not found) do
if first(s) ?= x then found := true
else k, s := k+1, rest(s)
fi
od
Falls vor Ausführung des Programms s die Sequenz t enthält, dann
gilt nach Ausführung des Programms (found ∧ t.k = x) ∨ (¬found ∧
x kommt in t nicht vor).
Varianten (Spielarten) zu Wiederholungsanweisungen:
• nichtabweisende Wiederholung:
repeat S until C steht für S; while ¬C do S od
• gezählte Wiederholung:
f or i := E1 to E2 do S od steht für i := E1; while i ≤ E2 do S; i := i + 1 od
Achtung: In S sollten keine Zuweisungen an i erfolgen.
5.4
Variablendeklarationen und Blöcke
Auch Programmvariablen sind zu deklarieren; dies geschieht in Blöcken:
var m x := E; Sy
pvar
In S kann x als lokale Variable verwendet werden.
Allgemeine Form der Variablendeklaration:
var m1 x1 , . . . , varmn xn := E1 , . . . , En
5.5. PROZEDUREN
69
Syntax:
var hsortihidi
hvariable declarationi ::=var
∗
{, var hsortihidi}
∗
:= hexpi {, hexpi}
Nebenbedingungen:
• x1 , . . . , xn treten nicht in E1 , . . . , En auf.
• Sorten und Anzahl stimmen überein.
Beispiel:
var nat h := x; x := y; y := hy ≡ x, y := y, x
pvar
hblocki ::=phinner blockiy
while hinneri do hinner blocki od
|while
if hinneri then hinner blocki else hinner blocki f i
|if
∗
hinner blocki ::= {hdeclarationi; } hstatementi
hdeclarationi ::=hvariable declarationi
|hprocedure declarationi
|helement declarationi
|hfunction declarationi
|hsort declarationi
Nebenbedingungen:
• Sortenkonsistenz
• Jeder Identifikator wird in einem Block höchstens ein Mal deklariert (aber
geschachtelte Deklarationen sind erlaubt).
5.5
Prozeduren
Sollen gewisse Anweisungen an verschiedenen Stellen im Programm genutzt werden,
so führen wir dafür (besonders bei umfangreichen Anweisungen) besser Abkürzungen ein. Dies spart nicht nur Schreib- und Leseaufwand, sondern erlaubt uns auch, in
sich geschlossene Programmteile zu eigenständigen Modulen zusammenzufassen.
5.5.1
Prozedurdeklaration
Beispiel:
var m x, var m y) :
proc vertausche = (var
m h = x; x := y; y := hy
pm
(x, y: formale Parameter)
Sind v und w Programmvariablen der Sorte m , so können wir die Prozedur mit vertausche (v, w) aufrufen (v, w: aktuelle Parameter).
m h = v; v := w; w := hy.
Dies hat die gleiche Wirkung wie pm
70
KAPITEL 5. ZUWEISUNGSORIENTIERTE PROGRAMMIERUNG
Syntax:
proc hidi = hpari {, hpari}∗ : hstatementi
hprocedure declarationi ::=proc
var
hpari ::= {var
var} hsortihidi
m1 x1 , . . . , mn xn ) : S
Allgemeine Form: proc p = (m
Nebenbedingungen:
• Sorten m1 , . . . , mn von x1 , . . . , xn müssen dem Gebrauch in S entsprechen.
• x1 , . . . , xn paarweise verschieden
In Prozeduren können Parameter sehr unterschiedliche Rollen übernehmen.
Beispiel:
proc p = (var
var nat
natv, var nat
natw, var nat
natu, nat
natx) :
w, v := x + v, u + w
Wir unterscheiden:
(1) Eingabeparameter: Programmvariable oder Konstante in der Parameterliste, die sich im Rumpf nicht ändern (keine Zuweisung erfahren).
(2) Ausgabeparameter: Programmvariable in der Parameterliste, an die Zuweisungen vorgenommen werden, deren Wert jedoch nicht abgefragt wird.
(3) Transienter Parameter: Programmvariable dient gleichzeitig zur Einund Ausgabe
Achtung:
• Die Rolle eines Parameters sieht man aus dem Prozedurkopf nicht. Bei umfangreichen Prozeduren ist die Rolle zu dokumentieren.
• Neben Programmvariablen als Parameter und lokalen Programmvariablen im
Rumpf können in Prozeduren auch globale Variablen auftreten. Auch globale
Variablen sind zu dokumentieren.
5.5.2
Prozeduraufruf
Für eine Prozedur
m1 x1 , . . . , mn xn ) : S
proc p = (m
schreiben wir einen Aufruf wie folgt:
p(E1 , . . . , En )
Jeder Aufruf ist eine Anweisung.
Wir haben zwei Arten von formalen Parametern:
• Konstante (d.h. die Sorte mi ist keine Variablensorte); dann muss der aktuelle
Parameter Ei ein Ausdruck de rSorte mi sein.
• Variable (d.h. die Sortenangabe mi ist von der Bauart var si ); dann muss der
aktuelle Parameter eine Programmvariable der Sorte si sein.
5.5. PROZEDUREN
71
Beispiel:
var nat v, nat x) : S
proc q = (var
Aufrufe (sei var nat w im Umfeld deklariert):
q(w, 27)
gültiger Aufruf
q(w, w)
gültiger Aufruf
q(27, 3)
ungültiger Aufruf
q(w + 1, w + 2) ungültiger Aufruf
Bedeutung des Prozeduraufrufs:
nat x = E; S[w/v]y (wenn x in E
Der Aufruf q(w, E) ist äquivalent zu pnat
nicht vorkommt).
Schwierigkeiten beim Prozeduraufruf:
var nat x, var nat v) : Spvar
var nat w := 5;
proc p = (var
p(w, w);
..
.
y
Besetzung verschiedener formaler Parameter durch die gleichen globalen Parameter
ist unbedingt zu vermeiden (Aliastabu).
Achtung: Neben dem Wertaufruf für Konstante und dem Call - by - name / variable
/ reference, die wir verwenden, gibt es noch viele Prozeduraufrufmechanismen.
Beispiel: Call-by-value-result
nat x := E; var nat v := w; S; w := vy.
q(w, E) entspricht hier pnat
5.5.3
Globale Programmvariable in Prozeduren
Im Rumpf von Prozeduren dürfen auch Programmvariablen auftreten, die nicht
lokal und keine formalen Parameter sind:
var nat x, y := . . . ;
proc vertausche =: x, y := y, x;
vertausche; vertausche
Globale Variable sind sorgfältig zu dokumentieren. Aliasing ist zu vermeiden:
var nat x := . . . ;
var nat v) : x, v := v, x + 1;
proc p = (var
p(x)
5.5.4
Rekursion
Auch Prozeduren dürfen rekursiv sein.
1
2
4
var nat x, y, r := a, b, 0;
proc pdiv =:
if x < y then nop
else r, x := r+1, x-y; pdiv
fi
72
KAPITEL 5. ZUWEISUNGSORIENTIERTE PROGRAMMIERUNG
5.6
Block und Abschnitt, Bindung: Gültigkeit /
Lebensdauer
Wir benutzen Identifikatoren in folgenden vier Rollen:
• Konstante
• Programmvariable
• Funktionen
• Prozeduren
In allen Fällen setzen wir voraus, dass die Identifikatoren gebunden sind. Arten der
Bindung:
• vordefiniert (z.B. Funktion head in Gofer)
• Deklaration
• formale Parameter
Zu jeder Bindung gibt es einen Bindungsbereich:
• Deklaration: Block / Abschnitt, in dem die Bindung steht
• formale Parameter: Rumpf der Funktion / Prozedur
Bei der Ausführung von Programmen sprechen wir auch von der Lebensdauer einer
Bindung (Zeitdauer, für die die Bindung besteht). Bindungen können überlagert
werden.
1
2
4
var nat x := 1;
proc p = (var nat v, nat x):
...
v := v + x;
...
6
8
| var bool x := true;
x := x or not x;
_|
x := 5
Hier ist x drei Mal gebunden. Die Lebensdauer entspricht dem Bindungsbereich. Die
Gültigkeit ergibt sich aus der Lebensdauer minus der Lebensdauer von Bindungen
an den gleichen Identifikator in inneren Blöcken / Abschnitten oder Funktions- /
Prozedurdeklarationen.
1
2
4
var nat v := 1;
proc p = (var nat w): w := v + 1
| var nat v, var nat u := 2, 3;
p(u)
_|
Frage: Welchen Wert hat u nach dem Aufruf p(u)?
2? → statische Bindung (static scoping)
3? → dynamische Bindung (dynamic scoping)
Statische Bindung: Globale Identifikatoren richten sich nach der Deklarationsstelle.
Dynamische Bindung: Globale Identifikatoren werden nach Aufrufstelle gebunden.
Wir verwenden statische Bindung. Grund: Lesbarkeit - Zuordnung nach Aufschreibung
5.7. PROGRAMMTECHNIKEN FÜR ZUWEISUNGEN
5.7
73
Programmtechniken für Zuweisungen
Im Gegensatz zur funktionalen Programmierung treten in Anweisungen eine Reihe von technischen Aspekten auf, die mit der Art und Weise zu tun haben, wie
Anweisungen auf Rechnern abgearbeitet werden. Zusätzlich zu der Kenntnis dieser
Aspekte brauchen wir Methoden, um anweisungsorientierte Programme
• zu spezifizieren
• zu analysieren
• als korrekt nachzuweisen
Beispiel:
(sei x, y var nat
nat)
1
2
4
6
{x =
x :=
{x =
y :=
{x =
x :=
{x =
a AND y = b}
x + y;
a + b AND y = b}
x - y;
a + b AND y = a}
y - x;
-b AND y = a}
Idee der Zusicherung: Sei S eine Anweisung und Q sowie R boolesche Ausdrücke
(prädikatenlogische Ausdrücke) über den Programmvariablen in S. Wir schreiben
{Q} S {R}
für die folgende Aussage: Gilt im Zustand vor Ausführung der Anweisung S die
Aussage Q und terminiert S, so gilt im Zustand nach Ausführung von S die Aussage
R.
Beispiel:
{x ≥ 0}
x := x + 1
{x > 0}
Um zu zeigen, dass Aussagen mit Zusicherungen (formalen Kommentaren) für
ein Programm (eine Anweisung) tatsächlich gelten, verwenden wir folgende Regeln:
Axiome des Zusicherungskalküls (Hoare - Kalkül)
• Axiom für nop
nop:
{Q} nop {Q}
• Axiom für abort
abort:
{Q} abort {ff alse
alse}
• Zuweisungsaxiom:
{R[E/x]} x := E {R}
• Regel für das Abschwächen von Zusicherungen (rule of consequence):
Q0 ⇒ Q
{Q} S {R}
{Q0 } S {R0 }
R ⇒ R0
74
KAPITEL 5. ZUWEISUNGSORIENTIERTE PROGRAMMIERUNG
• Regel für sequentielle Komposition:
{Q} S1 {R0 } {R0 } S2 {R}
{Q} S1 ; S2 {R}
• Regel für die bedingte Anweisung:
{Q ∧ C} S1 {R} {Q ∧ ¬C} S2 {R}
{Q} if C then S1 else S2 f i {R}
• Regel für die Wiederholungsanweisung
{R ∧ C} S {R}
{R} while C do S od {¬C ∧ R}
R heißt Invariante.
Beispiel:
1
2
{rest(s) = empty}
s := rest(s)
{s = empty}
4
6
8
{x >= 0 AND y >= 0}
x := x + 1
{x > 0 AND y >= 0}
y := y + 1
{x > 0 AND y > 0}
10
12
14
16
{not found AND length(s) > 0 AND k + length(s) = n+1}
if first(s) ?= x then found := true
else k, s := k+1, rest(s)
fi
{((first(s) = x AND found) OR (not found)) AND
(k + length(s) = n+1)}
Beispiel: Hoare - Kalkül
1
2
4
6
8
10
{s = t}
found, k := false, 1;
{k + length(s) = n+1}
while length(s) > 0 AND not found do
{length(s) > 0 AND not found AND k + length(s) = n+1}
if first(s) = x then found := true
else k, s := k+1, rest(s)
fi
od
{(length(s) = 0 OR found) AND (k + length(s) = n+1)}
Um Zusicherungen über Wiederholungsanweisungen zeigen zu können, müssen wir
geeignete Invarianten finden. Diese helfen, die Logik eines Programms mit Wiederholungsanweisungen zu verstehen.
5.7. PROGRAMMTECHNIKEN FÜR ZUWEISUNGEN
75
Beispiel: (Fortsetzung)
• Invariante des Beispielprogramms:
s = t[k : n] ∧ (x ∈ t ⇔ x ∈ s)
• weitere Invariante: found ⇒ first(s) = x.
Durch die Regeln, die wir bisher betrachtet haben, können wir allerdings nicht
nachweisen, dass eine Wiederholungsanweisung terminiert. Um die Terminierung
von Wiederholungsanweisungen zu zeigen, verwenden wir eine Regel, die sich auf
die Idee der Zusicherungen abstützt.
Um zu zeigen, dass
while C do S od
(5.1)
unter der Vorbedingung Q terminiert, brauchen wir:
• eine Invariante R
• einen Terminierungsausdruck E (E bezeichnet eine ganze Zahl und darf auf
Programmvariablen Bezug nehmen)
• einen Hilfsidentifikator i, der in (5.1), in R und E nicht vorkommt
Beweisbedingungen:
(1) Q ⇒ R:
die Invariante gilt am Anfang
(2) E ≤ 0 ∧ R ⇒ ¬C:
das Programm terminiert, falls E ≤ 0
(3) {E = i + 1 ∧ C ∧ R} S {E ≤ i ∧ R}:
führung von S echt kleiner
R ist invariant und E wird durch Aus-
Bemerkung: E entspricht der Terminierungsfunktion in den Terminierungsbeweisen
der funktionalen Programmierung.
Beispiel: (Fortsetzung)
Wähle für E:
(
length(s) falls¬ found ∧ length(s) > 0
E=
0
falls found
Wir können Q und R trivial als true wählen.
Bemerkung: Zusicherungen können auch zur Spezifikation von Anweisungen verwendet werden. Wir erhalten folgendes Schema für die Spezifikation von Anweisungen:
(1) Angabe der zu verwendenden Programmvariablen und ihrer Sorten und der
Eingabewerte
(2) Angabe der Vorbedingungen
(3) Angabe der Nachbedingungen
Beispiel: (Fortsetzung)
Eingabewerte:
Programmvariablen:
Vorbedingung:
Nachbedingung:
t: seq m
x: m
var bool found
var seq m s
var nat k
s=t
(found ∧t[k] = x) ∨ (¬ found ∧x ∈
/ t)
76
KAPITEL 5. ZUWEISUNGSORIENTIERTE PROGRAMMIERUNG
Kapitel 6
Sortendeklarationen
bool
Bisher haben wir mit einer kleinen Anzahl vorgegebener Sorten gearbeitet (bool
bool,
nat
seq
m
nat,
m). Nun betrachten wir Möglichkeiten, weitere Sorten zu deklarieren.
6.1
Deklaration von Sorten
BNF-Syntax:
sort hsorti = hsort constructi
hsort declarationi ::=sort
hsort constructi ::=henumeration sorti
|hproduct sorti
|hsum sorti
|hsubrange sorti
|harray sorti
|hset sorti
|hfile sorti
|hsorti
6.1.1
Skalare durch Enumeration
Wir können Sorten mit endlich vielen Elementen einfach durch Aufzählung angeben:
Beispiel:
sort color = {blue, yellow, red}
Damit haben wir eine Sorte deklariert, die (außer ⊥) genau drei Elementen besitzt.
1
2
4
fct f = (color x) color:
if x ?= yellow then red
else blue
fi
Wichtig: Auf den Elementen einer Enumerationssorte ist nur die Vergleichsoperation
verfügbar.
Allgemeine Form:
sort enum = {e1 , . . . , en }
77
78
KAPITEL 6. SORTENDEKLARATIONEN
Vergleichsoperatoren:
?
?
ei = ej ≡ i = j
ei ≤ ej ≡ i ≤ j
Syntax:
∗
henumeration sorti ::= { hidi {, hidi} }
Beispiel:
sort currency = {euro, dollar, yen, . . .}
Grundsätzliches zur Sortendeklaration:
• eine Sortenbezeichnung
• eine Menge (Trägermenge) von Sortenelementen
• eine Menge von Funktionen
– zur Konstruktion von Elementen der Sorte (Konstruktoren)
– zum Abfragen von Eigenschaften (Selektoren etc.)
Beispiel:
sort obst = {banane, apfel, birne, . . .}
Sortenbezeichnung: obst
Konstruktoren: banane, apfel, birne
6.1.2
Direktes Produkt / Tupelsorten
Beispiel:
string name, nat alter, sex geschlecht)
sort person = makeperson(string
sort sex = {mann, frau}
Tupelsorten dienen dazu, unterschiedliche Informationen zu einem Paket (Tupel)
zusammenzufassen.
Allgemeine Form:
sort product = construct(ss1 sel1 , . . . , sn seln )
Trägermenge zur Sorte product
product:
M1− × . . . × Mn− ∪ {⊥}
direktes Produkt
wobei Mi− Träger zur Sorte si (ohne ⊥) sind.
Funktionen:
product
f ct construct = (ss1 , . . . , sn )product
product
f ct seli = (product
product)ssi
für 1 ≤ i ≤ n
Es gilt (für ai ∈ Mi− und 1 ≤ i ≤ n):
seli (construct(a1 , . . . , an )) = ai
6.1. DEKLARATION VON SORTEN
79
Beispiel:
1
2
4
alter (makeperson ("Heidi", 22, frau)) = 22
fct geburtstag = (person x) person:
makeperson (name(x), alter(x)+1, sex(x))
Syntax:
hproduct sorti ::= hidi
∗ hsorti {hidi} {, hsortihidi}
Nebenbedingung:
• Selektorfunktionen paarweise verschieden
Achtung: In vielen Programmiersprachen können Produktsorten selektiv geändert
werden (sei v vom Typ var person
person): statt
v := makeperson(name(v), alter(v) + 1, sex(v))
schreiben wir
alter(v) := alter(v) + 1
Beispiel:
sort empty = empty()
6.1.3
Direkte Summe / Varianten
Oft sind wir interessiert, aus einer Anzahl von Sorten eine Vereinigung zu bilden.
nat seuro)|dollar(nat
nat sdollar)
sort currency = euro(nat
Die direkte Summe entspricht der disjunkten Vereinigung von Mengen.
({euro} × N) ∪ ({dollar} × N)
Beispiel:
nat smeter)|feet(nat
nat sfeet)
sort laenge = meter(nat
Allgemeine Form:
sort sum = product1 | . . . |productn
Hierbei steht productj für die Sorte
constructj = sj1 selj1 , . . . , sjnj seljnj
Funktionen:
• Konstruktoren:
sum
f ct constructj = (ssj1 , . . . , sjnj )sum
• Abfrage: Sei y von der Sorte sum
sum.
y in constructj
Diskriminator
beantwortet die Frage, ob y von der Variante productj ist (boolesches Resultat).
80
KAPITEL 6. SORTENDEKLARATIONEN
Beispiel:
var currency c := euro(25)
c in euro true
c in dollar false
Selektion (Projektion):
selij
(
xji
=
⊥
fallsy = constructj (xj1 , . . . , xnnj )
sonst
Beispiel: (Fortsetzung)
seuro(c) = 25
sdollar(c) = ⊥
1
2
4
fct eurowert = (currency x) nat:
if x in euro then seuro (x)
else sdollar (x) * 0.98
fi
Syntax:
6.1.4
n
o∗
hsum sorti ::= hproduct sorti |hproduct sorti
Teilbereichsorten
Manchman möchte man von Sorten m , auf denen eine lineare Ordnung ≤ definiert
ist, eine Teilbereichsorte bilden.
Seien E1 , E2 Ausdrücke der Sorte m , dann bezeichnet E1 : E2 die Teilbereichsorte
(sei M − Trägermenge zu der Sorte m ohne ⊥) mit Trägermenge
x ∈ M − : E 1 ≤ x ≤ E2
Beispiel:
sort bnat = 0 : 248 − 1
sort tag = {mo, di, mi, do, fr, sa, so}
sort werktag = mo : fr
Syntax:
hsubrange sorti ::= hexpi : hexpi
6.2
Felder
Felder dienen dazu, eine große Zahl von Elementen gleicher Sorte darzustellen und
auf diese durch Indexwerte zuzugreifen.
6.2. FELDER
6.2.1
81
Einstufige Felder
Seien i, j ganze Zahlen mit i ≤ j. Dann ist
array m
[i : j]array
die Sorte eines Feldes der Länge j − i + 1 für Elemente der Sorte m .
array m kann man sich wie folgt vorstellen:
[i : j]array
i i + 1 ... j
ai ai+1 . . . aj
d.h. a entspricht einer Abbildung
a : {i, . . . , j} → M −
wobei M − die Menge der Elemente der Sorte m ohne ⊥ bezeichnet.
Beispiel:
array person
[1 : 10]array
Funktionen auf Feldern:
array s
f ct init = [i : j]array
array ss, int
f ct get = ([i : j]array
int)ss
array ss, int
array s
f ct update = ([i : j]array
int, s )[i : j]array
init liefert ein Feld, in dem alle Einträge undefiniert sind:
get (init, k) = ⊥


falls k = k 0 , k =
6 ⊥, 1 ≤ k ≤ j
x
0
0
0
get (update (a, k, x) , k ) = get (a, k ) falls k 6= k , k =
6 ⊥ 6= k 0 , i ≤ k, k 0 ≤ j


⊥
sonst
Notation: Wir schreiben a[k] für get (a, k), a[k] := E für a := update (a, k, E)
(selektives Ändern).
Die Menge aller Felder ist gegeben durch (sei M = M − ∪ {⊥})
({i, . . . , j} → M ) ∪ {⊥}
6.2.2
Felder und selektives Ändern
array int a mit a[k] 6=
Sortieren durch Auswählen in einem Feld: Gegeben sei [1 : n]array
array int va := a. Gesucht ist ein Programm, das
⊥∀1 ≤ k ≤ n und var
var[1 : n]array
auf va die (absteigend) sortierte Sequenz zu a erzeugt (In - Situ - Sortieren, d.h.
Sortieren auf dem Platz). Lösung: Suchen des jeweils maximalen Elements.
1
2
4
6
8
10
for i := 1 to n do
var int max := i;
for j := i+1 to n do
if va[max] < va[j] then
max := j
fi
od
if not (max ?= i) then
va[i], va[max] := va[max], va[i]
fi
od
82
KAPITEL 6. SORTENDEKLARATIONEN
Achtung: Durch Schreibweisen wie a[i] := a[i]+1 erhalten wir Bezeichnungen a[i] für
Programmvariable. var [1 : n] array nat a wird gleichsam als [1 : n] array var nat a
verstanden. Wir erhalten errechnete Programmvariable a[i + j].
Es gilt auch die Zuweisungsregel nach Hoare nicht mehr. Die Aussage
{x = a[j]} a[i] := a[i] + 1 {x = a[j]}
ist für den Fall i = j falsch. Das Hoare - Kalkül ist für Felder nur korrekt, wenn wir
die abkürzende Schreibweise durch die ausführliche ersetzen:
1
2
{x = get (update (a, i, get (a, i) + 1), j)}
a := update (a, i, get (a, i) + 1)
{x = get (a, j)}
6.2.3
Mehrstufige Felder
Allgemeine Sorte:
[n1 : m1 , . . . , nk : mk ] array m
ist Sorte eines k - stufigen Feldes.
Beispiel:
sort matrix = [1 : n, 1 : m] array m
Syntax:
∗
harray sorti ::=[hindexi {, hindexi} ] array hsorti
hindexi ::=hsubrange sorti
|hsorti
Beispiel: Multiplikation Vektor - Matrix
1
2
4
6
8
proc vectmatmult = ([1:n, 1:n] array nat a, [1:n] array nat x,
var [1:n] array nat y):
for i := 1 to n do
y[i] := 0;
for j := 1 to n do
y[i] := y[i] + a[i,j] * x[j]
od
od
Kritischer Punkt: Festlegung der Feldgrenzen
• statisch: Die Feldgrenzen sind durch Zahlenwerte (Konstante) vor Ausführung
des Programms festgelegt.
• dynamsich: Die Feldgrenzen werden zum Deklarationszeitpunkt des Feldes
festgelegt.
• flexibel: Feldgrenzen können während der Lebensdauer des Feldes verändert
werden.
6.3. ENDLICHE MENGEN
6.3
83
Endliche Mengen
Sei m eine beliebige Sorte; mit set m bezeichnen wir die Sorte aller Mengen, die
endliche Teilmengen der Trägermenge zur Sorte m (ohne ⊥) sind. Trägermenge zu
set m ist {s ⊆ M \ {⊥} : s endlich} ∪ {⊥}, wobei M Trägermenge zur Sorte m sei.
Operationen auf Mengen:
∅
leere Menge (emptyset)
∪
Vereinigung (union)
∩
Durchschnitt (average)
{x1 , . . . , xn } bilden endlicher Mengen
\
Mengendifferenz (diff)
∈
Elementrelation (elem)
Beispiel:
Generieren der Menge aller Teilmengen von {1, . . . , n} für ein gegebenes
n ∈ N:
nat n) set set nat
f ct gen = (nat
gen(n) = {s ⊂ {1, . . . , n}}
Gesucht ist ein induktives Prinzip. Für n ≥ 1 sei x = gen(n−1) gegeben.
Es ist x ⊆ gen(n) (x enthält alle Mengen, in denen n nicht vorkommt).
Dann ist gen(n) = x ∪ {s ∪ {n} : s ∈ x}.
1
2
4
fct gen = (nat n) set set nat:
if n ?= 0 then {emptyset}
else set set nat x = get (n-1);
union (x, include (x, n))
fi
6
8
10
fct include = (set set nat x, nat n) set set nat:
if x ?= emptyset then emptyset
else set nat s = any (x);
union ({union (s, {n})}, include (diff (x, {s}), n))
set m
m benutzt, die für beliebige
Wir haben hier die Auswahlfunktion f ct any = (set
m)m
Elemente s der Sorte setm
setm(6= ⊥) folgender Regel genügt:
s 6= ∅ ⇒ any(s) ∈ s (Hilberts Auswahloperator)
Mengen können auch verwendet werden, um gerichtete Graphen darzustellen. Gegeben sei sort node = {a, b, . . . , z}. Ein gerichteter Graph wie in Abbildung 6.1
kann dadurch beschrieben werden, dass wir für jeden Knoten x die Menge g(x) der
Knoten angeben, zu denen von x aus eine Kante führt:
node
set node Graph der Funktion
f ct g = (node
node)set
Beispiel:
Für einen durch g gegebenen Graphen können wir nun die Menge aller
von einem gegebenen Knoten x über Folgen von Knoten erreichtbaren
Knoten berechnen.
x → x1 → x3 → . . . → xn
Kantenzug (Pfad) in g
Naiver Ansatz zur Menge der erreichbaren Knoten:
84
KAPITEL 6. SORTENDEKLARATIONEN
a
b
e
d
c
Abbildung 6.1: Gerichteter Graph
1
2
4
fct reachable = (set node x) set node:
if x ?= emptyset then emptyset
else union (x, {reachable (g (z)): elem (z, x)})
fi
Problem: Der Algorithmus terminiert nicht im Falle von Zyklen!
Frage: Wir brechen wir Zyklen auf?
tc (transitive closure) berechnet alle von x aus erreichbaren Punkte,
ohne auf Wegen zu gehen, die Knoten aus y enthalten, vereinigt mit den
in y enthaltenen Punkten.
1
2
4
6
fct tc = (set node
if x ?= emptyset
else z = any(x);
if elem (z, y)
else tc (union
fi
fi
x, set node y) set node:
then y
then tc (diff (x, {z}), y)
(x, g(z)), union (y, {z}))
Durch den Aufruf tc ({a} , ∅) erhalten wir die Menge aller von a aus
erreichbaren Knoten.
Kapitel 7
Sprünge und Referenzen
Heutige Rechenmaschinen verwenden einen Speicher, der im Wesentlichen ein riearray
siges Feld (array
array) ist. Der Index des Feldes ist die Speicheradresse.
Sowohl die Daten als auch die Programme sind im Speicher abgelegt. Jedes
im Speicher abgelegte Programmstück (jeder Befehl) und jedes Datenelement hat
somit eine Adresse. Wir besprechen nun zwei Ideen für Programmierelemente, die
auf diese Beobachtung zurückzuführen sind:
• Sprünge
• Referenzen
7.1
Kontrollfluss
In einem zuweisungsorientierten (prozeduralen) Programm besteht die Ausführung
aus einer Folge von Befehlen, die ausgeführt werden. Welche Befehle wann ausgeführt werden, wird über Bedingungen gesteuert; wir sprechen vom Kontrollfluss.
7.1.1
Marken und Sprünge
Will man den Kontrollfluss explizit steuern, so bietet sich an:
(1) gewisse Anweisungen S mit Namen m zu versehen (zu markieren) [m: S (Marke)]
(2) einen Befehl zu verwenden, der den Kontrollfluss zu einer markierten Anweisung steuert [goto m (Sprungbefehl)]
Beispiel: Programm mit Sprungbefehl
1
2
4
6
8
y := 1;
m: if y <= n then
if a[y] ?= x then nop
else y := y + 1;
goto m
fi
else y := 0
fi
85
86
KAPITEL 7. SPRÜNGE UND REFERENZEN
Nachbedingung:
{(y = 0 ∧ ∀y ∈ {1, . . . , n} : a[j] 6= y) ∨ (y ∈ {1, . . . , n} ∧ a[y] = x)}
Durch Marken (label) können wir Anweisungen in Programmen eindeutig kennzeichnen. Durch m : S wird eine Marke m deklariert. Für die Deklaration von Marken
gelten die gleichen Regeln wie für andere Deklarationen:
(1) Innerhalb eines Bindungsbereichs darf eine Marke höchstens ein Mal deklariert
werden.
(2) Deklarationen dürfen sich überlagern.
Wir können jedes zuweisungsorientierte Programm (abgesehen von rekursiven Prozeduren) in eine Form bringen, bei der wir nur zwei Arten von Befehlen benötigen:
m1 : x := E
Datenfluss
m2 : if C then goto m3 f i
Kontrollfluss
Diese Befehlsstruktur ist sehr ähnlich dem Aufbau von maschinennahen Sprachen.
Achtung: goto - Befehle sind mit Vorsicht zu genießen, da sie zu sehr fehleranfälligen
Programmen führen.
Erkenntnis (1970): Die Anzahl der goto - Befehle in einem Programm ist direkt
proportional zur Anzahl der Fehler (Grund: Ablaufstruktur ist implizit).
Lösung: Strukturiertes Programmieren - goto vermeiden.
Beispiel: while - Anweisung übersetzt in goto
gotos
while C do S od
ist gleichwertig zu
mwhile: if ¬C then goto whileende f ii;
S;
goto mwhile;
whileende: nop
7.1.2
Kontrollflussdiagramme
Die graphische Darstellung von Programmen wird häufig als ein Mittel angesehen,
Programme einfacher verständlich zu machen. Abbildung 7.1 stellt den Kontrollfluss
des Beispielprogramms aus 7.1.1 dar.
7.2
Referenzen und Zeiger
Die Verwendung von Bezeichnern (Identifikatoren) ist eine der grundlegenden Ideen
der Mathematik, Logik, Informatik. Arten der Verwendung von Identifikatoren:
• gebundene Identifikatoren
• freie Identifikatoren
• Programmvariable
7.2. REFERENZEN UND ZEIGER
87
y := 1
true
false
false
y <= n
true
a[y] = x
y := 0
y := y + 1
Abbildung 7.1: Kontrollflussdiagramm
Nun gehen wir den Schritt vom Bezeichner zu Werten, die als Stellvertreter für
andere Werte stehen. Die Menge der Verweise auf Elemente einer Sorte m bezeichnen
wir mit der Sorte ref m
m. Wichtig ist für die Elemente der Sorte ref m nicht, wie
sie aussehen oder dargestellt werden, sondern welche Operationen für sie verfügbar
sind:
• Dereferenzieren: Übergang von einem Verweis / einer Referenz auf den bezoref m
m
genen Wert: f ct deref = (ref
m)m
?
?
• Identitätsvergleich: ref m x, y: x = y, deref(x) = deref(y) (x = y ⇒ deref(x) =
deref(y))
• Generieren (erzeugen) von Referenzen
funktional:
nil
leere Referenz
nil
deref(nil
nil) = ⊥
ref m
ref m
f ct gen = (ref
m, m )ref
deref (gen (r, a)) = a
Beispiel:
nil
ref nat x = gen(nil
nil, 7)
Wir können natürlich auch Referenzen mit Variablen kombinieren:
var ref m
ref var m
mx
dadurch werden Zuweisungen wie deref(x) := wert möglich
var ref var m
Pointer
Zum Erzeugen von Referenzen werden oft spezielle Prozeduren verwendet:
var ref var m v) : . . .
proc new = (var
Sei var ref var m v deklariert. Dann entspricht new(v) einer Zuweisung an v: Dabei
bekommt v als neuen Wert eine Referenz (Zeiger, Pointer) auf eine neue Programmvariable. Auf diese Weise kann die Anzahl der Programmvariablen dynamisch
(zur Ausführungszeit des Programms) erhöht werden.
Idee: Aufbau von verzweigten Strukturen.
88
KAPITEL 7. SPRÜNGE UND REFERENZEN
Kapitel 8
Rekursive Sorten
Bisher haben wir (abgesehen von Sequenzen und dynamischen Feldern) nur statische Datenstrukturen behandelt. Dies sind Datenstrukturen, bei denen der Umfang der zu speichernden Informationen beschränkt ist. Dabei ist zu berücksichtigen, dass in vielen Implementierungen von Programmiersprachen auch die Größe
der natürlichen Zahlen beschränkt ist.
Im Folgenden studieren wir Datenstrukturen, die geeignet sind, eine unbeschränkte Größenordnung von Information zu speichern.
8.1
Sequenzartige Rechenstrukturen
Die Struktur der endlichen Sequenzen über einer gegebenen Menge M ist für viele
Algorithmen und Anwendungen hilfreich.
Beispiel
8.1.1
Strings, Pfade in einem Graphen etc.
Rechenstruktur der Sequenzen
Sorte seq m
m: Sequenz von Elementen der Sorte m .
Sei M − die Menge der Elemente der Sorte m (ohne ⊥). Trägermenge zu seq m ist
∗
dann: (M − ) ∪ {⊥}.
Operationen auf Sequenzen: empty, make, conc, first, last, rest, lrest.
Beobachtung: Viele Programmiersprachen stellen keine Sorte von Sequenzen allgemein zur Verfügung (höchstens string mit kleinen Längen). Stattdessen werden
Unterstrukturen zur Verfügung gestellt, d.h. Sequenzen, für die nicht alle Operationen zur Verfügung stehen (insbesondere die Konkatenation nicht).
8.1.2
Stack
stackm sei die Sorte der Stapel von Elementen der Sorte m .
Operationen auf Stacks: empty, append, first, rest.
1
2
fct append = (m x, stack m s) stack m:
conc (<x>, s)
Wichtige Gleichungen:
rest (append (x, s)) = s
first (append (x, s)) = x
Prinzip: Last - in - first - out (LIFO)
89
90
KAPITEL 8. REKURSIVE SORTEN
pegel
1
2
3
4
5
6
s1
s2
s3
s4
s5
s6
7
Abbildung 8.1: Stack als Feld dargestellt
Beispiel Typischerweise verwenden wir in der zuweisungsorientierten Programmierung Programmvariablen vom Typ var stack m zur Zwischenspeicherung von
Teilergebnissen. Gegeben sei ein Feld a[1:n] mit Elementen der Sorte nat
nat. Aufgabe:
Das Feld ist aufsteigend zu sortieren.
1
2
4
6
8
10
12
proc partition = (var [1:n] array nat a, nat min, nat max,
var nat r):
|
var nat i, j, x := min+1, max, a[min];
while i <= j do
if a[i] <= x then i := i + 1
elif x <= a[j] then j := j - 1
else a[i], a[j] := a[j], a[i]
fi;
if min < j then a[min], a[j], r := a[j], a[min], j
else r := min
fi
|
14
16
18
20
22
24
proc quicksort = (var [1:n] array nat a):
|
var nat r, min, max;
var stack nat s := append (1, append (n, empty));
while s <> empty do
s, min, max := rest(rest(s)), first(s), first(rest(s));
partition (a, min, max, r);
if min < r-1 then s := append (min, append (r-1, s)) fi;
if r+1 < max then s := append (r+1, append (max, s)) fi
od
|
Dieses Beispiel zeigt, wie durch die Verwendung von Stapeln Rekursion vermieden
werden kann.
Fakt: Jede Rekursion kann durch Stapel ersetzt werden!
Stapel lassen sich rekursiv deklarieren:
m first, stack m rest)
sort stack m = empty
empty|append (m
Solange die Größe von Stapeln beschränkt ist und wir die Schranke kennen, können
wir Stapel einfach durch Felder darstellen (gegeben sei var array [1 : n] m feld und
varnat pegel) [vgl. Abb. 8.1]. Dabei gilt:
• feld[pegel] liefert das erste Element
• rest bedeutet Dekrementieren des Pegels
• append durch Inkrementieren des Pegels und Schreiben in das Feld
8.2. BAUMARTIGE RECHENSTRUKTUREN
91
fe
1
2
le
3
4
5
6
s1
s2
s3
s4
7
Abbildung 8.2: Queue als Feld
• leerer Stapel: pegel = 0
8.1.3
Warteschlangen (Queue)
queue m sei die Sorte der Warteschlangen über der Sorte m .
Operationen auf Warteschlangen: empty, first, rest, stock.
Wichtigste Operation:
1
2
fct stock = (queue m q, m x) queue m:
conc (q, <x>)
Wichtige Gleichungen:
first (stock (empty, x)) = x
rest (stock (empty, x)) = empty
first (stock (stock (q, y) , x)) = first (stock (q, y))
rest (stock (stock (q, y) , x)) = stock (rest (stock (q, y) , x))
Prinzip: First - in - first - out (FIFO)
Darstellung in einem Feld (gegeben seien var array [1 : n] m feld und var nat le, fe)
[vgl. Abb. 8.2]: Zwei Pegel:
• fe ∼ first element
• le ∼ last element
8.1.4
Dateien
Auch Dateien sind sequenzartige Rechenstrukturen.
seq m old, seq m new)
sort band = datei (seq
8.2
Baumartige Rechenstrukturen
Bäume sind für die Informatik zentrale Datenstrukturen.
Definition: Baum über einer gegebenen Menge M (informell)
Ein Baum ist:
(1) leer oder
(2) besteht aus einem Element aus M (genannt die Wurzel des Baums) und
einer endlichen Menge von Bäumen
92
KAPITEL 8. REKURSIVE SORTEN
13
22
12
13
7
22
7
12
3
Beispiel: Bäume über N
4
4
28 7
3 ε 5 5 11 22
23
2
ε
13
Es gibt viele Möglichkeiten, Bäume darzustellen:
22 12
7 ε
{13, {{22, {{7} , ε}} , {12}}}
(((7) , 22, ε) , 13, (12))
Beispiel: arithmetische Ausdrücke als Baum
[(3 + 7) ∗ (8 + 12)] − 22
22
*
+
+
3 7 8 12
Definition: Binärbäume über M (formal)
Wir definieren die Menge der Binärbäume über M induktiv:
T0 = {ε}
leerer Baum
Ti+1 = Ti ∪ (Ti × M × Ti )
Wir definieren
Tree(M ) =
[
i∈N
Ti
8.2. BAUMARTIGE RECHENSTRUKTUREN
93
Bäume als Rechenstruktur Sei tree m die Sorte der Binärbäume über der
Sorte m .
f ct
f ct
f ct
f ct
f ct
f ct
emptytree = tree m
tree m
cons = (tree
m, m , tree m
m) tree m
tree m
left = (tree
m) tree m
tree m
right = (tree
m) tree m
tree m
root = (tree
m) m
tree m
isempty = (tree
m) bool
isempty (emptytree) = true
isempty (cons (t, x, t0 )) = false
left (cons (t, x, t0 )) = t
root (cons (t, x, t0 )) = x
right (cons (t, x, t0 )) = t0
left (emptytree) = ⊥
root (emptytree) = ⊥
right (emptytree) = ⊥
Beispiele: Programme über Bäumen
(1) Umformen von Bäumen in Sequenzen: Vorordnung (preorder): Erst die Wurzel, dann links, dann rechts.
1
2
4
fct pre = (tree m t) seq m:
if isempty(t) then empty
else
conc (<root(t)>, conc (pre(left(t)), pre(right(t))))
fi
(2) Inordnung (inorder): Erst links, dann die Wurzel, dann rechts.
1
2
4
fct in = (tree m t) seq m:
if isempty(t) then empty
else conc (in(left(t)), conc (<root(t)>, in(right(t))))
fi
(3) Postordnung (postorder)
1
2
4
fct post = (tree m t) seq m:
if isemtpy(t) then emtpy
else
conc(post(left(t)), conc(post(right(t)), <root(t)>))
fi
Achtung: Diese Abbildungen sind nicht injektiv, d.h. verschiedene Bäume werden
auf die selbe Sequenz abgebildet.
Mögliche Lösung (für Pre- und Postordnung): ε mit in die Sequenz aufnehmen.
94
KAPITEL 8. REKURSIVE SORTEN
22
Auswahlbäume
22
4
3
12
22
12
1
4 10 22 7 12 ε ε
εε εε ε ε εε ε ε εε
Idee: Die Wurzel ist das Maximum der Wurzeln der Teilbäume (falls diese nicht leer
sind). Die Teilbäume sind entweder beide leer oder beide nicht leer.
Programm, das entscheidet, ob ein Baum ein Auswahlbaum ist:
1
2
4
6
8
fct istct = (tree nat t) bool:
if isempty(t) then true
elif isempty(left(t)) then isempty(right(t))
elif isempty(right(t)) then false
else
isct(left(t)) and isct(right(t))
and root(t) ?= max (root(left(t)), root(right(t)))
fi
Die folgende Rechenvorschrift wandelt eine Sequenz in einen Auswahlbaum um:
1
2
4
6
fct mktree = (seq nat s) tree nat:
if s ?= empty then emptytree
elif length(s) = 1 then cons(emptytree, first(s), emptytree)
else nat k = length(s) / 2;
cctree (mktree (part(s, 1, k)),
mktree (part (s, k+1, length(s))))
fi
8
10
12
14
fct cctree = (tree nat l, tree nat r) tree nat:
if isempty(r) then l
elif isempty(l) then r
elif root(l) > root(r) then cons (l, root(l), r)
else cons (l, root(r), r)
fi
Löschen des maximalen Elements im Auswahlbaum:
1
2
4
6
8
fct deltree = (tree nat t) tree nat:
if isempty(t) then emptytree
elif isempty(right(t)) then deltree(left(t))
elif isempty(left(t)) then deltree(right(t))
elif root(t) ?= root(left(t)) then
cctree (deltree(left(t)), right(t))
else cctree (left(t), deltree(right(t)))
fi
Sortieren durch Auswahlbäume (Heapsort)
1
2
fct heapsort = (seq nat s) seq nat:
treeinsort (mktree(s))
8.3. REKURSIVE SORTENVEREINBARUNGEN
4
6
95
fct treeinsort = (tree nat t) seq nat:
if isemtpy(t) then empty
else conc (treeinsort(deltree(t)), <root(t)>)
fi
Arithmetische Ausdrücke als Bäume
sort op = (0 +0 ,0 −0 ,0 ·0 ,0 /0 )
nat n)|operator(op
op
sort entry = operand(nat
opo)
Prüfen, ob ein Baum einen korrekten arithmetischen Ausdruck darstellt:
1
2
4
fct iae = (tree entry t) bool:
if isemtpy(t) then false
elif root(t) in operator then iae(left(t)) and iae(right(t))
else isempty(left(t)) and isempty(right(t))
fi
Auswerten eines arithmetischen Ausdrucks gegeben als Baum:
1
2
4
6
fct eval = (tree entry t: iae(t)) nat:
if root(t) in operand then n(root(t))
elif root(t) ?= operator(’+’) then
eval (left(t)) + eval (right(t))
...
fi
Ein arithmetischer Ausdruck, dargestellt durch einen Baum t, kann durch post(t) in
eine Sequenz umgewandelt werden. Sei s diese Sequenz, als Stack dargestellt. Wir
werten den Ausdruck in dieser Form wie folgt aus:
1
2
4
6
8
fct steval = (stack entry e, stack nat k) nat:
if isempty(e) then first(k)
elif first(e) in operand then
steval(rest(e), append(n(first(e)), k))
elif first(e) ?= operator(’+’) then
steval(rest(e), append(first(rest(k))+first(k),
rest(rest(k))))
...
fi
8.3
Rekursive Sortenvereinbarungen
Beispiel
tree m left, m root, tree m right)
sort tree m = emptytree|cons (tree
sort arithe xp = operand (nat
nat n) |ausdruck (arith
arithe xp l, op o, arithe xp r)
Frage: Welche Sorte, d.h. welche Trägermenge wird durch eine rekursive Sortenvereinbarung festgelegt?
Sei D die Menge aller Datenelemente. Mit der rechten Seite einer rekursiven
Sortenvereinbarung verbinden wir eine Abbildung
∆ : 2D → 2D
96
KAPITEL 8. REKURSIVE SORTEN
∆ am Beispiel der rekursiven Sortendeklaration für Bäume Sei K die
Trägermenge zur Sorte k und M die Trägermenge zur Sorte m . Der Sortenaustree m left, m root, tree m right) definiert eine Trägermendruck emptytree|cons (tree
ge, die wir als ∆(K) definieren:
∆(K) = {⊥} ∪ {ε} ∪ {(x, y, z) : x, z ∈ K \ {⊥} , y ∈ M \ {⊥}}
Mit ∆ können wir eine Folge von Mengen Ki , i ∈ N definieren:
K0 = {⊥}
Ki+1 = ∆(Ki )
im Beispiel:
K0 = {⊥}
K1 = {⊥, ε}
K2 = {⊥, ε} ∪ {(ε, y, ε) : y ∈ M \ {⊥}}
Für die Konstruktion gilt:
∀i ∈ N : Ki ⊆ Ki+1
K∞ =
[
Ki
Trägermenge der rekursiv definierten Sorte
i∈N
K = ∆(K)
Fixpunkt
kleinster Fixpunkt in Mengeninklusion: K∞
8.3.1
Verwendung von Rekursion für Sortenvereinbarungen
Beispiel: Bäume mit beliebigen Verzweigungen
m root, children ch)
sort bvtree = bvcons (m
bvtree first, children rest)
sort children = emptytree|app (bvtree
8.4
Geflechte
Oft werden in rekursiven Sortendeklarationen Referenzen mit der Rekursion verbunden.
Beispiele
ref tree textlef t, m root, ref rtree right)
sort rtree = emptytree|cons (ref
sort ref tree = ref retree
ref tree left, m root, ref tree right)
sort retree = cons (ref
Hier wird der leere Baum mit nil dargestellt.
Durch rekursive Sorten und Referenzen können wir Sorten einführen, die es
uns erlauben, Datenstrukturen zu behandeln, die gerichteten Graphen entsprechen.
Sehen wir als Knoten in diesen Graphen Programmvariable vor, so können wir die
Graphen selektiv verändern.
8.4. GEFLECHTE
97
Abbildung 8.3: Geflecht
Person 3
Person 4
Person 2
Person 1
Abbildung 8.4: Geflecht über der Sorte person
8.4.1
Einfache Geflechte
Wir können auch mit Geflechten arbeiten, ohne Rekursion bei den Sorten zu verwenden.
Beispiel: Bibliothek
sort buch = cbuch (string
string titel, ref var regal platz)
nat nummer, ref var saal ort)
sort regal = cregal (nat
nat nummer, ref var stockwerk w)
sort saal = csaal (nat
...
Um ein Buch in ein anderes Regal zu stellen, schreiben wir (sei buch b gegeben):
deref (platz (b)) := neuesregal;
Solange wir keine rekursiven Sorten verwenden, sind die Geflechte zyklenfrei.
8.4.2
Geflechte über rekursiv vereinbarten Sorten
Beispiel
string name, ref var person mutter)
sort person = persdaten (string
Führen mehrfach Verweise aus unterschiedlichen Geflechtsteilen auf den gleichen
Record, dann sprechen wir von gemeinsamen Teilstrukturen. Wir können mit dieser
Technik Informationen genau an einer Stelle im Programm verwalten und von vielen
Punkten über Verweise darauf zugreifen.
98
KAPITEL 8. REKURSIVE SORTEN
(a) Einfach verkettete Liste
(b) Einfach verkettete Ringliste
Abbildung 8.5: Verkettete Listen
Abbildung 8.6: Invertierter Baum
8.4.3
Verkettete Listen
Um Sequenzen durch Geflechte darzustellen, verwenden wir verkettete Listen.
sort revl = ref var pevl
m first, var revl rest)
sort pevl = pair (m
Beispiel: Einfach verkettete Listen in Pascal
1
2
4
6
type
ref_elist = ^elist;
elist = record
first: integer;
rest: ref_elist
end;
Rechenvorschrift zum Aufbau eines Listenelements:
1
2
4
6
8
procedure push (n: integer; var v: ref_elist);
var
h: ref_elist;
begin
new (h);
h^.first := n;
h^.rest := v;
v := h
end;
8.4.4
Zweifach verkettete Listen
Will man verkettete Listen in zwei Richtungen durchlaufen, so bieten sich zweifach
verkettete Listen an.
Beispiel: Zweifach verkettete Liste in Pascal
1
2
type
rhead = ^head;
8.4. GEFLECHTE
99
Abbildung 8.7: Zweifach verkettete Liste
4
6
8
10
rzvl = ^zvl;
head = record
first, last: rzvl
end;
zvl = record
vor, nach: rzvl;
elem: integer
end;
Die folgende Rechenvorschrift erzeugt für eine gegebene Zahl n ∈ N, n ≥ 1 eine
zweifach verkettete Liste, die alle Zahlen von 1 bis n enthält.
1
2
4
6
8
10
12
14
16
18
20
22
procedure build (n: integer; var r: rhead);
var
h, a: rzvl;
k: integer;
begin
new (a);
new (r);
r^.first := a;
a^.elem := 1;
k := 2;
while k <= n do begin
new (h);
h^.vor := a;
a^.nach := h;
h^.elem := k;
inc (k);
a := h
end;
r^.last := a;
a^.nach := nil;
r^.first^.vor := nil
end;
Geflechte treten in einer großen Variantenzahl auf.
100
KAPITEL 8. REKURSIVE SORTEN
Kapitel 9
Objektorientiertes
Programmieren
Idee: Beim Aufbau eines Programmes verwenden wir gleichartig strukturierte Bausteine, genannt Objekte.
Zuammenhang zum Record: Objekt ∼ Verweis (Objektidentifikator) auf Programmvariable der Sorte Record + Prozeduren (Methoden), die auf den Elementen (Attributen) des Records arbeiten.
Klasse ∼ Beschreibung von Objekten gleicher Verhaltensstruktur
Wichtige Idee: Daten / Programmvariablen und der Zugriff darauf (durch Methoden) werden zusammengefasst (Verkapselung). Damit werden die Möglichkeiten,
Daten zu manipulieren, bewusst eingeschränkt. Es entsteht die Idee der Schnittstelle (engl. interface).
Beispiel
1
2
4
6
8
class Point =:
var real x1, x2, x3;
method create = (real y1, y2, y3):
x1, x2, x3 := y1, y2, y3;
method shift = (real y1, y2, y3):
x1, x2, x3 := x1+y1, x2+y2, x3+y3;
method dist = (var real d):
d := sqrt (x1*x1 + x2*x2 + x3*x3);
endclass
9.1
Klassen und Objekte
Im Zentrum der objektorientierten Programmierung stehen die Begriffe
se und Objekt.
Objekt: Zusammenfassung von
Klas-
• Variablen und Konstantenbezeichnungen (genannt Attribute)
• Prozeduren (genannt Methoden)
Idee: Auf die Attribute kann von außen nur über die Methoden zugegriffen werden.
Jedes Objekt besitzt einen eindeutigen Identifikator (vergleichbar mit einer Referenz).
101
102
KAPITEL 9. OBJEKTORIENTIERTES PROGRAMMIEREN
9.1.1
Klassen
Klassen dienen der Beschreibung von Objekten.
Klasse:
• Eine Klasse besitzt eine Klassenbezeichnung.
• Eine Klasse definiert eine Menge von Attributen und Methoden.
• Die Attributnamen sind nur innerhalb der Klassendefinition gültig.
• Die Methoden sind außerhalb der Klassendefinition gültig und dienen dem
Zugriff auf die Attribute.
• Klassen beschreiben Objekte.
• Mit Hilfe von Klassen können wir Objekte kreieren (instanziieren).
Beispiel
1
2
4
6
8
10
12
14
16
18
20
Klasse zur Beschreibung eines Kontos:
class account =:
var nat kontonr;
var string inhaber;
var nat stand;
method create = (nat nr, string inh):
kontonr, inhaber, stand := nr, inh, 0;
method buchen = (nat nr, string x, int betrag,
var string report):
if nr <> kontonr then report := "falsche Kontonummer"
elif x <> inhaber then report := "falsche Inhaberangaben"
elif betrag+stand < 0 then
report := "Kontostand ungenuegend"
else
| report := "Buchung erfolgreich";
stand := stand + betrag;
|
fi
method abfragen = ...
method aenderung = ...
...
endclass
Warteschlange:
1
2
4
6
8
10
class Queue =:
var seq data s;
method create =:
s := empty;
method enq = (data x):
s := conc (s, <x>);
method deq = (var data x):
s, x := rest(s), first(s);
method isempty = bool:
s ?= empty;
endclass
9.1. KLASSEN UND OBJEKTE
9.1.2
103
Objekte
Objekte werden durch die Nutzung der Klassen und die Methode create erzeugt.
Ein Objekt besitzt einen eindeutigen Identifikator in Form einer Referenz. Wir
erzeugen ein Objekt durch
q := Queue.create
(9.1)
Dabei ist q eine Variable der Sorte Queue. Die Klassennamen werden also als Sorten
für die Objektidentifikatoren der Objekte der entsprechenden Klasse benutzt.
Durch (9.1) wird ein neues Objekt (wie in Pascal durch new) der entsprechenden Klasse erzeugt (eine Familie von Programmvariablen gemäß der Attribute) sowie eine Referenz darauf (der Objektidentifikator). Diese Referenz wird an q
zugewiesen.
Beispiel
1
Aufruf der Methode enq mit dem Parameter 27 für das Objekt q:
q.enq(27)
Zur Laufzeit eines objektorientierten Programms entspricht jede Klasse einer Menge
von Objekten, die für diese Klasse generiert wurden.
Historisch
Die objektorientierte Programmierung entstand Ende der 60-er Jahre aus Simula
67.
→ O. J. Dahl, Nygaard, Oslo
Smalltalk in den 70-er Jahren, Xerox Parc, C++, Eiffel, Java, ...
Gründe für die Objektorientierung:
• Graphische Benutzerschnittstellen sind so viel einfacher zu programmieren.
• Viele Programmieraufgaben (Steuerung von Geräten) lassein sich objektorientiert sehr einfach und anschaulich programmieren.
Wichtige Begriffe zur objektorientierten Programmierung:
Persistenz: Die Attribute eines Objekts leben so lange wie ds Objekt, sind also
beständig - persistent.
• klassiche prozedurale Programmierung:
1
2
sort account = construct (nat ktonr, ...)
var account x := ...
proc buchen = (...)...
x wird als globale Variable genutzt.
Unterschied: In der objektorientierten Programmierung ist die Gültigkeit der
Attribute eingeschränkt, der Zugriff nur über Methoden möglich, beliebig viele
Exemplare (Objekte) erzeugbar.
Schnittstelle (interface): Übergangspunkt zwischen zwei Systemteilen.
Idee: System wird in Teile strukturiert (in der Objektorientierung: Klassen und
Objekte).
Schnittstelle eines Objekts: Die Methoden, die das Objekt zur Verfügung stellt (vgl.
Signatur)
Idee: Mehrfachverwendung von Klassen durch Klassenbibliotheken (framework),
d.h. vorgefertigte Klassen, beschrieben durch Schnittstellen.
104
KAPITEL 9. OBJEKTORIENTIERTES PROGRAMMIEREN
9.2
Vererbung
Ein weiteres wesentliches Element der Objektorientierung ist die Vererbung.
Für Klassen können wir Klassifikationen angeben (Taxonomien).
Fahrzeuge
Beispiel
Kettenfahrzeuge
Räderfahrzeuge
Einräder
Vierräder
PKW
Pickup
3 Türen 5 Türen
Die Objektorientierung nutzt Taxonomien, um Klassen zueinander in Beziehung zu
setzen.
Container
SeqCont
SetCont
Stack Queue Set Multiset
Dabei folgen die syntaktischen Schnittstellen und die vorhandenen Attribute der
Taxonomie.
9.2.1
Vererbungsbeziehung
Durch die Taxonomie entstehen Beziehungen zwischen Klassen. Wir sprechen von
einer Vererbungsbeziehung. Im Beispiel: SeqCont erbt von Container.
Sprechweise: SeqCont ist Unterklasse von Container, Container ist Oberklasse von
SeqCont.
Technisch heißt das: Eine Unterklasse erhält alle Attribute der Oberklasse und alle
ihre Methoden, aber n. K. noch mehr.
Idee: In einer Unterklasse werden die Methoden und Attribute der Oberklasse nicht
erneut aufgeschrieben, sonder nur die Methoden / Attribute, die neu hinzukommen.
1
2
4
6
class Deque =:
extends Queue
method enql = (data x):
s = conc (<x>, s)
method deqr = (var data x):
s, x := lrest(s), last(s)
endclass
Wichtige Beobachtung: Wir können Objekte der Sorte Deque nutzen, wenn wir
Objekte der Sorte Queue benötigen. Dabei lässt sich kein Unterschied feststellen,
solange wir die neu eingeführten Methoden nicht verwenden.
Prinzip: Ersetzbarkeit der Oberklasse durch Unterklassen.
Das bedeutet, dass wir an allen Stellen, wo wir Objekte der Oberklasse verwenden,
auch Objekte der Unterklasse verwenden dürfen.
1
2
Deque x = Deque.create;
...
Queue y = x;
9.2. VERERBUNG
105
Jeder Objektidentifikator einer Unterklasse ist auch Objektidentifikator der Oberklasse.
Einfachster Fall: alle Attribute / Methoden der Oberklasse werden unverändert von
der Unterklasse übernommen (vererbt).
Überschreiben von Methoden: Bei der Vererbung können auch Methoden nicht unverändert übernommen, sondern überschrieben werden, d.h. in der Unterklasse wird
für eine Methode der Oberklasse ein neuer Rumpf angegeben. Idee: Methoden werden den Spezifika der Unterklasse nach Bedarf angepasst.
Problem: Nun muss festgelegt werden, welche Methode tatsächlich aufgerufen wird,
wenn eine Programmvariable der Sorte Oberklasse mit einem Wert der Sorte Unterklasse belegt ist und eine Methode aufgerufen wird, die in der Oberklasse deklariert
und in der Unterklasse überschrieben wird.
Regel: Es wird immer die Methode angewandt, die dem Wert des Objekts entspricht,
d.h. im obigen Fall die Methode der Unterklasse.
Folge: Es kann i.a. erst zum Aufrufzeitpunkt (nicht aus der Aufschreibung) festgestellt werden, welche Methode ausgeführt wird, da dies von der aktuellen Sorte des
Werts der Variablen abhängt. Wir sprechen von später Bindung (late binding).
Achtung: I.a. arbeiten wir mit Vererbungshierarchien.
Regel: Um die Methode zu identifizieren, die schließlich ausgeführt wird, gehen wir
von der Klasse des Objekts aus, für das die Methode aufgerufen wird, und steigen
im Vererbungsbaum nach oben, bis wir eine explizite Methodendeklaration finden.
Polymorphie (Mehrgestaltigkeit) bezeichnet den Umstand, dass Variablen der Sorte
Oberklasse durch Werte der Sorte Unterklasse belegt werden können.
Einfachvererbung: Jede Klasse besitzt höchstens eine unmittelbare Oberklasse; die
Vererbungsrelation stellt einen Baum dar.
Mehrfachvererbung: Unterklassen können von mehreren Oberklassen unmittelbar
erben.
Beispiel
1
2
4
6
8
10
12
14
16
18
20
22
24
class Stack =:
var m first;
var Stack rest;
var bool isempty;
method create =:
isempty := true;
method push = (m x):
if isempty then
first := x;
isempty := false;
rest := nil;
elif rest ?= nil then
rest := Stack.create;
rest.push (first);
first := x;
else
rest.push (first);
first := x;
fi
method pop = (var m x):
if isempty then --> error
elif (rest ?= nil) or rest.stackempty then
x, isempty := first, true
else
106
26
28
30
32
34
36
38
40
KAPITEL 9. OBJEKTORIENTIERTES PROGRAMMIEREN
x := first;
rest.pop (first);
fi
method stackempty = bool:
isempty;
endclass
class StackCount =:
extends Stack
var nat c;
method create =:
c, isempty := 0, true;
method push = (m x):
c = c + 1;
super.push(x);
...
endclass
Abschreckendes Beispiel:
1
2
4
6
8
10
12
14
class Queue =:
extends Stack
method push = (m x):
if isempty then
rest := nil;
first := x;
isempty := false;
elif rest ?= nil then
rest := Queue.create;
rest.push (x);
else
rest.push(x);
fi
endclass
Achtung: Das Beispiel zeigt einen Missbrauch der Idee der Vererbung: Warteschlangen können nicht Stacks ersetzen, ohne dass sich die Programme unterschiedlich
verhalten.
9.2.2
Bemerkungen zur Objektorientierung
In objektorientierten Sprachen gibt es eine Vielzahl von notationellen Besonderheiten und weiteren Konzepten, die bestimmte Programmierprobleme speziell unterstützen.
• Exceptions
Oft werden Zugriffsrechte speziell geregelt (public / private / final).
Weitere Beispiele: this, super
Abstrakte Klassen: Klassen, für die keine Objekte instanziiert werden können. Analog: abstrakte Methoden.
Neben den bisher betrachteten Methoden / Attributen (die in Java Instanzmethoden / -attribute heißen), gibt es in einigen objektorientierten Sprachen (z.B. Java)
auch Klassenmethoden / -attribute. Diese sind nicht Bestandteil eines einzelnen
Objekts, sondern Bestandteil der Klasse.
Beispiel
Zähler, der die Anzahl der instanziierten Objekte der Klasse zählt.
Teil II
Rechnerstruktur, Hardware,
Maschinennahe
Programmierung
107
109
Stichworte:
• Zeichenweise Darstellung von Information → Codierung
• Schaltungen / Schaltwerke - Schaltlogik
• Rechnerstrukturen: Aufbau von Rechneranlagen
• Maschinennahe Programmierung
110
Kapitel 1
Codierung /
Informationstheorie
In der Codierung studieren wir die Darstellung von Information durch Zeichenfolgen
(vgl. formale Sprache).
1.1
Codes / Codierung
Wir konzentrieren uns stark auf eine Darstellung durch zwei Zeichen (Bits - Binary
Digits).
B = {L, 0}
Bn Menge der Binärwörter der Länge n (n - Bit - Wörter)
Sei A ein Alphabet (linear geordneter endlicher Zeichensatz), B ein Alphabet.
c : A → B Code / Codierung
c : A∗ → B ∗ Wortcodierung
Wichtig auf einer Codierung ist die Umkehrbarkeit:
d : {c(a) : c ∈ A} → A
mit d(c(a)) = a. Voraussetzung: c injektiv.
Binärcodierung:
c : A → B∗
Achtung: B∗ ist lexikographisch linear geordnet (0 < L).
1.1.1
Binärcodes einheitlicher Länge
Wir betrachten nun für gegebenes n ∈ N die Abbildung c : A → Bn .
Achtung: Wir setzen voraus, dass c injektiv ist, aber nicht notwendigerweise surjektiv.
Für zwei Codewörter aus Bn können wir nach dem Abstand fragen, wir sprechen
von Hamming-Abstand:
hd : Bn timesBn → N0
n
X
hd(ha1 , . . . , an i , hb1 , . . . , bn i) =
d(ai , bi )
i=1
111
112
KAPITEL 1. CODIERUNG / INFORMATIONSTHEORIE
wobei für x, y ∈ B gilt
(
1
d(x, y) =
0
falls x 6= y
falls x = y
Auf dieser Basis definieren wir einen Hammingabstand für einen Binärcode c:
hd(c) = min {hd(c(a), c(b)) : a, b ∈ A ∧ a 6= b}
hd(c) = 0
c nicht injektiv.
Um den Hammingabstand hoch zu halten, benutzt man Paritätsbits.
Beispiel Gegeben sei eine Codierung c : A → Bn mit hd(c) ≥ 1. Gesucht ist eine
Codierung c0 : A → Bn+1 mit hd(c) ≥ 2. Wir definieren:
(
L falls die Quersumme von b gerade ist
pb : Bn → B, pb(b) =
0 sonst
und für a ∈ A
c0 (a) = c(a) ◦ hpb(c(a))i
Behauptung: hd(c0 ) ≥ 2
Beweis: Seien a, a0 ∈ A, a 6= a0 gegeben.
1. Fall: hdc (a, a0 ) ≥ 2 ⇒ hdc0 (a, a0 ) ≥ 2
2. Fall: hdc (a, a0 ) = 1. Dann gilt pb(a) 6= pb(a0 ), da die Quersumme von a gerade ist,
wenn die Quersumme von a0 ungerade ist und umgekehrt. Also ist hdc0 (a, a0 ) = 2.
Wichtiges Thema der Codierung: Kryptographie: Verschlüsselung von Nachrichten.
Weiteres Thema: Codierung der Ziffern.
Direkter Code: c : {0, . . . , 9} → B4
c(z)
1-aus-10-Code
z
0 0000
000000000L
1 000L
00000000L0
2 00L0
0000000L00
3 00LL
000000L000
4 0L00
00000L0000
0000L00000
5 0L0L
6 0LL0
000L000000
7 0LLL
00L0000000
8 L000
0L00000000
9 L00L
L000000000
1.1.2
Codes variabler Länge
Wir betrachten nun Codes, bei denen die Codewörter unterschiedliche Längen besitzen.
Beispiele
• altes Fernsprechwählsystem:
1 7→ L0
2 7→ LL0
..
.
0 7→ LLLLLLLLLL0
• Morsecode
1.1. CODES / CODIERUNG
1.1.3
113
Serien-/ Parallelwortcodierung
Bei der Übertragung von binär codierter Information unterscheiden wir zwei Situationen:
• ein Draht / sequentielle Übertragung
• n Drähte (n > 1) / parallele Übertragung
Bei Übertragung eines Wortes w ∈ A∗ mit Binärcodierung c : A → B∗ einfachstes
Vorgehen:
c∗ : A∗ → B∗
c∗ (ha1 , . . . , an i) = c(a1 ) ◦ c(a2 ) ◦ · · · ◦ c(an )
Kritische Frage: Ist c∗ injektiv, wenn c injektiv ist?
Nein! Gegenbeispiel: Seien a1 , a2 ∈ A mit a1 6= a2 , c(a1 ) =< L > und c(a2 ) =<
LL >. Dann ist c∗ (< a1 a1 a1 >) =< LLL >= c∗ (< a1 a2 >).
Frage: Unter welchen Voraussetzungen ist die Antwort ja?
Antwort:
(1) Codes gleicher Länge
(2) die Fano-Bedingung gilt
Fano-Bedingung von Coole: Kein Codewort c(a1 ) für a1 ∈ A ist Präfix eines Codeworts c(a2 ) für a2 ∈ A mit a1 6= a2 .
Behauptung: Gilt für c die Fano-Bedingung, so ist c∗ injektiv.
Beweis (durch Widerspruch): Seien a = ha1 , . . . , an i , b = hb1 , . . . , bm i ∈ A∗ , a 6= b
und c∗ (a) = c∗ (b). O.B.d.A. sei n < m. Sei i so gewählt, dass ak = bk für 1 ≤ k ≤ i
und n = i oder ai+1 6= bi+1 .
1. Fall: ai+1 6= bi+1 ; dann gilt c(ai+1 ) Präfix von c(bi+1 ) oder c(bi+1 ) Präfix von
c(ai+1 ). Widerspruch zur Fano-Bedingung.
2. Fall: n = i; dann gilt (wegen m > n) c∗ (hbi+1 , . . . , bm i) = ε. Widerspruch zur
Fano-Bedingung.
Parallelwortcodierung:
c : A → Bn
ck∗ : A∗ → (Bn )∗
ck∗ (ha1 , . . . , ak i) = hc(a1 )i ◦ · · · ◦ hc(ak )i
Beispiel
   
L
0
L
0 0 0
   
L L  0 
0
L
0
L 0 L
0 0 0
L L 0
0 L 0
Trivial: ck∗ injektiv, falls c injektiv.
1.1.4
Codebäume
Codes lassen sich durch Tabellen oder durch Codebäume (s. Abb. (1.1)) darstellen.
B
C
D
A
L 0LL 0L0 00
Trivialerweise ist für Codes, dargestellt durch Codebäume, stets die Fano-Bedingung gegeben.
114
KAPITEL 1. CODIERUNG / INFORMATIONSTHEORIE
L
0
A
L
0
D
L
0
B
C
Abbildung 1.1: Codebaum
1.2
Codes und Entscheidungsinformation
Kernfrage: Wie viel Information trägt eine Nachricht?
Prinzip: Um so ungewöhnlicher (seltener) eine Information ist, desto mehr Informationsgehalt hat sie. Dafür setzen wir gegebenenfalls Wahrscheinlichkeiten für
Nachrichten voraus. Eine stochastische Nachrichtenquelle für ein endliches
Alphabet A erzeugt eine Folge von Zeichen aus A, bei der zu jedem Zeitpunkt die
Wahrscheinlichkeiten für jedes Zeichen einer vorgegebenen zeitunabhängigen Wahrscheinlichkeitsverteilung entsprechen.
Beobachtung: Seltene Zeichen tragen viel Information, häufige Zeichen
wenig.
P
Gegeben: A Alphabet, p : A → [0, 1] Wahrscheinlichkeiten (d.h. a∈A p(a) = 1 und
p(a) > 0∀a ∈ A).
Mittlerer Entscheidungsgehalt (Entropie):
X
1
H=
p(a) ld
p(a)
a∈A
Entropie: Maß für Gleichverteilung der Wahrscheinlichkeiten: H groß ⇔ Wahrscheinlichkeiten etwa gleichverteilt, H klein ⇔ starke Unterschiede in den Wahrscheinlichkeiten.
H
Zeichen i p(i)
1
a
2
1
1
b
Beispiel
2
3
a
4
≈ 0, 8
1
b
4
Seien für die Zeichen aus A Wahrscheinlichkeiten p : A → [0, 1] sowie eine Binärcodierung c : A → B gegeben. Wir können die mittlere Wortlänge L berechnen als
X
L=
p(a)|c(a)|
a∈A
wobei |c(a)| die Länge von c(a) bezeichnet. Um L klein zu halten, codieren wir Zeichen mit großen Wahrscheinlichkeiten mit kurzen Codewörtern. Größere Spielräume
erhalten wir, wenn wir nicht Einzelzeichen, sondern Wörter aus A∗ codieren. →
Huffman-Algorithmus
P
Jede Menge M ⊆ A von Zeichen hat auch eine Wahrscheinlichkeit p(M ) = a∈M p(a).
Es ist p(A) = 1. Ziel: A so in zwei disjunkte Teilmengen A0 und AL aufteilen, dass
p(A0 ) ≈ 12 ≈ p(AL ) ist.
1.3. SICHERUNG VON NACHRICHTENÜBERTRAGUNG
Beispiel
i
a
b
p(i)
3
4
1
4
i
aa
ab
ba
bb
115
p(i)
L=
9
16
3
16
3
16
1
16
1 X
p(w)|c(w)|
m
m
w∈A
Shannonsches Codierungstheorem:
(1) Für beliebige Wortcodes gilt H ≤ L.
(2) Der Wert L − H kann durch geschickte Wahl der Wortcodes beliebig klein
gemacht werden.
Redundanz eines Codes: L − H
Relative Redundanz eines Codes: 1 − H
L
Gesetz von Merkel: Die Reaktionszeit t einer Versuchsperson, um aus n Gegenständen
einen bestimmten auszuwählen, lässt sich berechen durch
t = 200 + 180 ld(n)[msec]
1.3
Sicherung von Nachrichtenübertragung
Beim Übertragen von Nachrichten sind zwei Aspekte von besonderer Bedeutung:
• Kapazität: Wie viele Informationseinheiten pro Zeiteinheit können übertragen
werden?
• Störung: Wie hoch ist die Wahrscheinlichkeit, dass Daten fehlerhaft übertragen werden?
1.3.1
Codesicherung
Diskreter Kanal ohne Speicher: Überträgt Binärwerte, wobei Störungen auftreten
können, deren Wahrscheinlichkeiten zu jedem Zeitpunkt gleich sind (vgl. stochastische Nachrichtenquelle).
Irrtumswahrscheinlichkeiten:
p0 Wahrscheinlichkeit, dass das Zeichen 0 als L übertragen wird
pL Wahrscheinlichkeit, dass das Zeichen L als 0 übertragen wird
einseitige Störung: (p0 = 0 ∧ pL > 0) ∨ (p0 > 0 ∧ pL = 0)
symmetrische Störung: p0 = pL
In manchen Fällen führt eine Störung nicht zu einer Veränderung L zu 0 oder 0
zu L, sondern führt auf ein Zeichen, das als fehlerhaft erkannt werden kann (Verlustzeichen). Wir erhalten eine Übertragung von Zeichen aus {0, L} in Zeichen aus
{0, L, ⊥} (⊥: Fehlerzeichen).
Ziel bei Kanälen mit Übertragungsfehlern: Die Wahrscheinlichkeit für Fehler soll
möglichst klein gehalten werden, d.h. Fehler sollen erkannt und korrigiert werden
(durch Redundanz).
Lemma: Hat ein Code Hammingabstand h, so können Störungen, die weniger als h
Zeichen betreffen, sicher erkannt werden.
Idee: Prüf- / Paritätsbits einsetzen
116
1.3.2
KAPITEL 1. CODIERUNG / INFORMATIONSTHEORIE
Übertragungssicherheit
Kanäle haben beschränkte Übertrangugskapazität:
s1 : Anzahl der Zeichen, die ein Kanal pro Zeiteinheit überträgt
s0 : Anzahl der Zeichen, die pro Zeiteinheit übertragen werden sollen
R = ss01 : Senderate
R < 1: Überkapazität, d.h. Codesicherung durch Redundanz möglich
R > 1: Unterkapazität: Nachricht kann nur in Teilen übertragen werden, kommt
gestört an.
Beispiel R = 31 → wir übertragen statt der Einzelzeichen jedes Zeichen drei Mal
hintereinander. Bei Störungen mit pE < 0, 5 decodieren wir wie folgt:
000, 00L, 0L0, L00 → 0
Die Fehlerrate sei p für Einzelzeichenübertragung; dann ist 3p2 − 2p3 die Wahrscheinlichkeit einer Störung bei Dreifachübertragung
14
Liegt die Übertragungsrate bei 16
, dann können wir 14-Bit-Wörter als 16-BitWörter mit 2 Paritätsbits übertragen.
Falls R ≥ 1 ist, reicht die Kapazität nicht aus. Trotzdem können wir Nachrichten
(gestört) übertragen.
Beispiel R = 3, d.h. wir sollen drei Mal so viel Nachrichten übertragen, als
möglich ist.
Idee: Drei Bits werden auf ein Bit komprimiert (Mehrheitstechnik. s.o.) und beim
Empfang wieder in drei gleiche Bits umgewandelt.
Irrtumswahrscheinlichkeit: (R > 1)
pE =
p
R−1
+
R
2R
Kapitel 2
Binäre Schaltnetze und
Schaltwerke
Zur Darstellung diskreter (symbolischer) Information reichen zwei Werte (Bits)
aus. Zur Darstellung technischer Art bits es viele Möglichkeiten:
• elektrische:
– Spannung / keine Spannung
– Strom / kein Strom
– Licht / kein Licht
• mechanische:
– Wasser / kein Wasser
– Dampf / kein Dampf
Wir betrachten im Weiteren nicht die technische Darstellung von Bits, sondern studieren, wie wir bestimmte Information (Zahlen, Datenstrukturen) durch Bitsequenzen darstellen können und wir wir Verarbeitungsvorgänge durch boolesche Funktionen realisieren können.
2.1
Boolesche Algebra / Boolesche Funktionen
Die Wahrheitswerte bilden mit den Operationen ¬, ∨ und ∧ eine boolesche Algebra.
Prädikate bilden ebenfalls eine boolesche Algebra: Statt einer Prädikatsdarstellung
p : M → B können wir stets eine Mengendarstellung S ⊆ M verwenden. Jedes
Prädikat über M definiert genau eine Teilmenge von M und umgekehrt.
Sp = {x ∈ M : p(x)}: p(x) = (x ⊂ Sp)
Allgemein gilt: ∧ ∼ ∩, ∨ ∼ ∪, ¬ ∼ Komplement und ⇒∼⊆.
2.1.1
Boolesche Funktionen
n-stellige boolesche Funktion: f : Bn → B
Beispiel:
(1) 0-stellige boolesche Funktionen: Bn = {ε}
Es gibt genau zwei 0-stellige boolesche Funktionen: true, false.
117
118
KAPITEL 2. BINÄRE SCHALTNETZE UND SCHALTWERKE
(2) 1-stellige boolesche Funktionen: f : B → B
true
false
x
¬x
konstant L
konstant 0
Identität
Negation
(3) 2-stellige boolesche Funktionen: f : B2 → B
true
false
x
y
¬x
¬y
x∨y
x∧y
x ∨ ¬y
¬x ∨ y
Konstanten
(x ∧ y) ∨ (¬x ∧ ¬y)
(x ∧ ¬y) ∨ (¬x ∧ y)
¬(x ∨ y)
¬(x ∧ y)
x ∧ ¬y
¬x ∧ y
Äquivalenz
Antivalenz
nor
nand
Bisubtraktion
Projektionen
negative Projektionen
Disjunktion
Konjunktion
Inverse Implikation, Subjunktion
Implikation
(4) 3-stellige boolesche Funktionen: f : B3 → B
...
n
Anzahl der n-stelligen booleschen Funktionen 2(2 )
Für jedes n ∈ N bilden die n-stelligen booleschen Funktionen eine boolesche Algebra.
BFn : Menge der booleschen Funktionen der Stelligkeit n.
Seien f, g ∈ BFn . Dann sind (¬f ), (f ∧ g) ∈ BFn und es gilt:
• (¬f )(x1 , . . . , xn ) = ¬f (x1 , . . . , xn )
• (f ∧ g)(x1 , . . . , xn ) = f (x1 , . . . , xn ) ∧ g(x1 , . . . , xn )
• (f ∨ g)(x1 , . . . , xn ) = f (x1 , . . . , xn ) ∨ g(x1 , . . . , xn )
Außerdem ist (f ∧ ¬f ), (f ∨ ¬f ) ∈ BFn .
Wie lassen sich boolesche Funktionen darstellen?
(1) Durch Formeln oder Aussagenlogik:
Sei f ∈ BFn . Dann können wir f (x1 , . . . , xn ) durch die aussagenlogischen
Terme über den Identifikatoren x1 , . . . , xn (z.B.: f (x1 , x2 , x3 ) = x1 ∨(¬x1 ∧x2 ))
darstellen.
2.2. NORMALFORMEN BOOLESCHER FUNKTIONEN
x1
x2
(2) Durch Tabellen:
x3
f (x1 , x2 , x3 )
0
0
0
0
0
0
L
0
0
L
0
L
0
L
L
L
L
0
0
L
119
L
0
L
L
L
L
0
L
L
L
L
L
(3) Entscheidungsdiagramme (Bäume)
2.1.2
Partielle Ordnung auf BF
Für eine beliebige boolesche Algebra definieren wir eine partielle Ordnung f ≥ g
durch f ≥ g ⇔def f = f ∧ g. Aus den Gesetzen der booleschen Algebra lässt sich
nachweisen, dass ≥ eine partielle Ordnung ist. f ≥ g ist gleichbedeutend mit f ⇒ g.
f ∨ ¬f ist kleinstes Element: true (schwächste Aussage). f ∧ ¬f ist größtes Element:
false (stärkste Aussage).
2.2
Normalformen boolescher Funktionen
Eine boolesche Funktion kann immer durch boolesche Terme dargestellt werden. Es
existieren viele syntaktisch verschiedene Terme, die die gleiche Funktion darstellen:
(¬x1 ∧ ¬x2 ) ∨ (x1 ∧ x2 ) ≡ (x1 ⇒ x2 ) ∧ (x2 ⇒ x1 ) ≡ (¬x1 ∨ x2 ) ∧ (¬x2 ∨ x1 )
Wir behandeln im Weiteren folgende Fragen:
(1) Gibt es einheitliche Termdarstellungen für BF (Normalformen)?
(2) Wie können wir aus einer Tabellen- oder Entscheidungsbaumdarstellung eine
Termdarstellung bekommen?
(3) Wir können wir besonders einfache und kurze Termdarstellungen bekommen?
Diese Fragen sind auch für die technische Realisierung von booleschen Funktionen durch Schaltungen entscheidend, da Termdarstellungen direkt in Schaltungen
umgesetzt werden können.
2.2.1
Das boolesche Normalformtheorem
Ziel: Wir wollen für beliebige boolesche Funktionen eine Termnormalform definieren
und zeigen, wie diese aus einer Tabelle erzeugt werden kann.
Wir definieren eine disjunktive Normalform (DNF):
hdefi ::= hkfi {∨ hkfi}∗ |0|L
hkfi ::= hliterali {∧ hliterali}∗
hliterali ::= {¬} hidi
Beispiel
(x1 ∧ ¬x2 ∧ x3 ∧ ¬x4 ) ∨ (¬x1 ∧ x2 ∧ x3 ∧ x4 ) ∨ (∧x1 ∧ ¬x2 ∧ x3 ∧ ¬x4 )
Als Identifikatoren wählen wir x1 , . . . , xn . In der vollständigen DNF kommt in jedem
Literal jeder Identifikator genau ein Mal vor.
Hilfskonstruktion: Seien b1 , . . . , bn ∈ B. Wir definieren einen Term
minterm(b1 , . . . , bn ) = (v1 ∧ · · · ∧ vn )
wobei
(
xi
vi =
¬xi
falls bi = L
falls bi = 0
120
KAPITEL 2. BINÄRE SCHALTNETZE UND SCHALTWERKE
Beispiel
minterm(0, L, L, 0, L) = (¬x1 ∧ x2 ∧ x3 ∧ ¬x4 ∧ x5 )
Theorem: Für f ∈ BFn gilt:
_
f (x1 , . . . , xn ) =
(f (b1 , . . . , bn ) ∧ minterm (b1 , . . . , bn ))
b1 ,...,bn ∈B
Beweis: durch Induktion über n.
Beispiel
x1
x2
x3
f (x1 , x2 , x3 )
L
L
L
L
L
L
0
L
L
0
L
0
L
0
0
0
0
L
L
0
0
L
0
L
0
0
L
0
0
0
0
0
f (x1 , x2 , x3 ) =(L ∧ x1 ∧ x2 ∧ x3 ) ∨ (L ∧ x1 ∧ x2 ∧ ¬x3 )
∨ (0 ∧ x1 ∧ ¬x2 ∧ x3 ) ∨ (0 ∧ x1 ∧ ¬x2 ∧ ¬x3 )
∨ (0 ∧ ¬x1 ∧ x3 ∧ x3 ) ∨ (L ∧ ¬x1 ∧ x2 ∧ ¬x3 )
∨ (0 ∧ ¬x1 ∧ ¬x2 ∧ x3 ) ∨ (0 ∧ ¬x1 ∧ ¬x2 ∧ ¬x3 )
=(x1 ∧ x2 ∧ x3 ) ∨ (x1 ∧ x2 ∧ ¬x3 ) ∨ (¬x1 ∧ x2 ∧ ¬x3 )
Analog zur disjunktiven gibt es die konjunktive Normalform (KNF); die Rollen von
und und oder sind dort vertauscht.
Die DNF beantwortet die am Eingang gestellten Fragen. Jede boolesche Funktion
hat eine eindeutige vollständige DNF.
2.2.2
Vereinfachte Normalformen (DNF)
Die vollständige DNF lässt sich oft weiter vereinfachen.
Regeln zur Vereinfachung
. . . ∨ (t1 ∧ · · · ∧ ti−1 ∧ xi ∧ ti+1 ∧ · · · ∧ tn ) ∨ . . .
∨(t1 ∧ · · · ∧ ti−1 ∧ ¬xi ∧ ti+1 ∧ · · · ∧ tn ) ∨ . . .
⇒ · · · ∨ (t1 ∧ · · · ∧ ti−1 ∧ ti+1 ∧ · · · ∧ tn ) ∨ . . .
Beispiel
(x1 ∧ x2 ∧ x3 ) ∨ (x1 ∧ x2 ∧ ¬x3 ) ∨ (¬x1 ∧ x2 ∧ ¬x3 )
=(x1 ∧ x2 ) ∨ (¬x1 ∧ x2 ∧ ¬x3 )
Nun können Terme folgender Gestalt auftreten: . . . ∨ (t1 ∧ t2 ) ∨ · · · ∨ (t1 ) ∨ . . . . Diese
Terme können ersetzt werden durch . . . ∨ (t1 ) ∨ . . . .
Durch diese Regeln kann von der vollständigen DNF zur vereinfachten DNF übergegangen werden.
Binäre Entscheidungsdiagramme
Da die Identifikatoren geordnet sind (x1 , . . . , xn ) können wir Entscheidungsbäume
für boolesche Funktionen konstruieren.
• n = 0, d.h. f ∈ BF0 ; der Entscheidungsbaum ist ein Blatt b ∈ B, und zwar
L falls f (ε) = L
0 falls f (ε) = 0
2.3. SCHALTNETZE
121
Abbildung 2.1: Beispiel eines Schaltplans
• n > 0: Wir definieren den Entscheidungsbaum wie folgt:
0
L
EBL EB0
wobei EBL der Entscheidungsbaum der booleschen Funktion fL ∈ BFn−1 mit
fL (x1 , . . . , xn−1 = f (L, x1 , . . . , xn−1 )
und EB0 der Entscheidungsbaum zur booleschen Funktion f0 ∈ BFn−1 mit
f0 (x1 , . . . , xn−1 ) = f (0, x1 , . . . , xn−1 )
seien.
2.3
Schaltnetze
Zur Realisierung von boolschen Funktionen (wir sprechen ab jetzt von Schaltfunktionen) können Schaltnetze verwendet werden. Schaltnetze entsprechen gerichteten,
azyklischen Graphen mit Eingängen und Ausgängen.
Definition: Ein Schaltnetz mit n Eingängen und m Ausgängen ist ein gerichteter,
azyklischer Graph mit n Eingangskanten und m Ausgangskanten, dessen Knoten
mit den Namen boolscher Funktionen markiert sind.
Schaltnetze sind mit boolschen Termen eng verwandt. Jeder boolsche Term lässt
sich als ein Schaltnetz darstellen. Die so entstehenden Schaltnetze haben jedoch
die Eigentümlichkeit, dass jede Kante genau ein Ziel hat. Deshalb führen wir im
Folgenden neben boolschen Termen eine Funktionaltermdarstellung für boolsche
Funktionen ein, durch die beliebige Schaltnetze mit Kanten mit Mehrfachziel (Verzweigung) dargestellt werden können.
Im Folgenden behandeln wir eine Reihe boolscher Funktionen, ihre Darstellung
durch boolsche Terme und durch Funktionsterme (funktionale Terme). Funktionsterme sind durch Verknüpfungen aus Grundfunktionen aufgebaut. Die Verknüpfungen entsprechen der graphischen Darstellung durch Schaltnetze.
Der Schaltplan, gegeben durch das Schaltnetz, kann direkt in eine technische
Realisierung umgesetzt werden.
2.3.1
Schaltfunktionen und Schaltnetze
Schaltfunktion: f : Bn → Nm , m, n ∈ N.
Schaltglied: (s. Abb. 2.2) Ein Schaltnetz besteht aus einer Menge von Schaltgliedern,
die über Kanten verbunden sind. Es entsteht ein gerichteter Graph. In Schaltnetzen
sind keine Zyklen zugelasen. Jede Kante hat genau eine Quelle, unter Umständen
aber mehrere Ziele, d.h. Kanten können sich verzweigen.
122
KAPITEL 2. BINÄRE SCHALTNETZE UND SCHALTWERKE
...n...
f
...m...
Abbildung 2.2: Schaltglied
...n...
&
Abbildung 2.3: Konjunktions-Schaltglieder
• Konjunktion: logisches
• Disjunktion: logisches
und (s. Abb. 2.3)
oder (s. Abb. 2.4)
• Negation: (s. Abb. 2.5)
Sei ein Schaltnetz mit n Eingängen und m Ausgängen gegeben. Wird für jedes
Schaltglied des Netzes eine Schaltfunktion angegeben, so definiert das Schaltnetz
selbst wieder eine Schaltfunktion aus SFm
n . In einem Schaltnetz entspricht
• jedes Schaltglied einer Schaltfunktion
• jede Kante einem Wahrheitswert
2.3.2
Darstellung von Schaltnetzen
Es gibt zwei grundlegende Möglichkeiten, Schaltnetze durch Formeln darzustellen:
(1) benannte Kanten
(2) Kombination (Komposition) von Schaltgliedern
Benannte Kanten: Jeder Knoten in einem Netz (vgl. Abb. 2.6) entspricht einer
Gleichung (y1 , . . . , ym ) = f (x1 , . . . , xn ). Dieses Darstellungsverfahren eignet sich
gut für kleine Schaltnetze ohne Regelmäßigkeiten.
Besser für große Schaltnetze mit starken Regelmäßigkeiten eignen sich kombinatorische Schreibweisen.
...n...
>= 1
Abbildung 2.4: Disjunktions-Schaltglieder
2.3. SCHALTNETZE
123
1
Abbildung 2.5: Negations-Schaltglieder
x1
...
xn
f
y 1 ... y m
Abbildung 2.6: Benannte Kanten
m2
1
(1) Parallele Komposition (s. Abb. 2.7): Seien f1 ∈ SFm
n1 und f2 ∈ SFn2 . Dann ist
m1 +m2
f = (f1 k f2 ) ∈ SFn1 +n2 , und es gilt f (x1 , . . . , xn1 +n2 ) = (y1 , . . . , ym1 +m2 ),
wobei
f1 (x1 , . . . , xn1 ) = (y1 , . . . , ym1 )
f2 (xn1 +1 , . . . , xn1 +n2 ) = (ym1 +1 , . . . , ym1 +m2 )
0
m
(2) Sequentielle Komposition (s. Abb. 2.8): Seien f1 ∈ SFm
n , f2 ∈ SFm . Es ist
m0
f = (f1 ◦ f2 ) ∈ SFn mit f (x1 , . . . , xn ) = f2 (f1 (x1 , . . . , xn )).
Eigenschaften: k, ◦ sind assoziativ, aber nicht kommutativ.
Zusatzfunktionen
• Projektion
πin ∈ SF1n
1≤i≤n
n
πi (x1 , . . . , xn ) = xi
• Identität
I ∈ SF11
In ∈ SFnn
I(x) = x
In (x1 , . . . , xn ) = (x1 , . . . , xn )
f
...
...
f1
f2
...
...
Abbildung 2.7: Parallele Komposition
124
KAPITEL 2. BINÄRE SCHALTNETZE UND SCHALTWERKE
n
...
f
f1
m
...
f2
...
m’
Abbildung 2.8: Sequentielle Komposition
• Verzweigung
V (x) = (x, x)
V ∈ SF21
Vn ∈ SF2n
n
Vn (x1 , . . . , xn ) = (x1 , . . . , xn , x1 , . . . , xn )
• Permutation
P ∈ SF22
Pn ∈ SFnn
P (x1 , x2 ) = (x2 , x1 )
Pn (x1 , . . . , xn ) = (x2 , . . . , xn , x1 )
• Tupelung
m2
1
f1 ∈ SFm
n , f2 ∈ SFn
2
[f1 , f2 ] ∈ SFm−1+m
n
[f1 , f2 ] = (Vn ◦ (f1 k f2 ))
• Senken
U ∈ SF01
Un ∈ SF0n
• Konstanten
K(0), K(L) ∈ SF10
2.3.3
Der
a
b
s
u
Kn (0), Kn (L) ∈ SFn0
Halbaddierer
Halbaddierer ist ein Schaltglied HA ∈ SF22 ; HA(a, b) = (u, s).
0 L 0 L
0 0 L L
0 L L 0
0 0 0 L
s = (¬a ∧ b) ∨ (a ∧ ¬b)
u=a∧b
Antivalenz
Diese Gleichungen führen auf den Aufbau gemäß Abbildung 2.9.
Umformung ergibt: s = ¬(¬(a∨b)∨(a∧b)). Damit kann der Halbaddierer effizienter
aufgebaut werden (s. Abb. 2.10).
2.3. SCHALTNETZE
125
a
b
u
s
Abbildung 2.9: Ineffiziente Schaltung für Halbaddierer
a
b
u
s
Abbildung 2.10: Effizientere Schaltung für Halbaddierer
126
KAPITEL 2. BINÄRE SCHALTNETZE UND SCHALTWERKE
a
b
c
VA
u
s
Abbildung 2.11: Volladdierer
a
b
c
VA
HA
HA
u
s
Abbildung 2.12: Volladdierer aus Halbaddierern
2.3.4
Arithmetische Schaltnetze
Beim Addieren von Binärworten, die Zahlen binär darstellen, verwenden wir Volladdierer (s. Abb. 2.11).
Tabelle:
a 0 L 0 L 0 L 0 L
b 0 0 L L 0 0 L L
c 0 0 0 0 L L L L
s 0 L L 0 L 0 0 L
u 0 0 0 L 0 L L L
n-stellige Binäraddition in Binärzahldarstellung:
+2 ∈ SFn2n : han . . . a1 i +2 hbn . . . b1 i = hsn . . . s1 i
Zuzüglich wird ein Übertrag u erzeugt: u = hun+1 . . . u1 i mit u1 = 0.
Es gilt
(uk+1 , sk ) = VA(ak , bk , uk )
Dies definiert s1 , . . . , sn , d.h. das Ergebnis hsn . . . s1 i (s. Abb. 2.13). un+1 zeigt an,
ob die Arithmetik übergelaufen ist.
Induktive Definition eines Addiernetzes:
AN1 = VA
ANn+1 = (I2 k An )(VA k In )
2.3. SCHALTNETZE
a n bn
127
un
VA
a 2 b2
a 1 b1 0
VA
VA
...
sn
s2
u3
u n+1
s1
u2
Abbildung 2.13: Addiernetz für zwei n-Bit-Worte
(vgl. Abb. 2.14)
Analog definieren wir die Binärsubtraktion −2 : (Bn )2 → Bn , han . . . a1 i −2
hbn . . . b1 i = hsn . . . s1 i.
si = (ai ∧ ¬bi ∧ ¬ui ) ∨ (¬ai ∧ bi ∧ ¬ui ) ∨ (¬ai ∧ ¬bi ∧ ui ) ∨ (ai ∧ bi ∧ ui )
u1 = 0
ui+1 = (¬ai ∧ bi ) ∨ (¬ai ∧ ui ) ∨ (bi ∧ ui )
Achtung: Der Übertrag repräsentiert nun einen negativen Wert.
Bemerkung: Falls un+1 = L ist, dann gilt b >2 a (>2 : (Bn )2 → B).
Die Subtraktion liefert als Abfallprodukt den Größenvergleich von a und b.
2.3.5
Zahldarstellung
Positive Zahlen (natürliche Zahlen im Intervall [0, 2n − 1]) stellen wir durch die
übliche Gewichtdarstellung dar:
w (han . . . a1 i) =
n
X
w(ai )2i−1
i=1
wobei w(L) = 1 und w(0) = 0 seien.
Zur Darstellung negativer Zahlen: Statt Darstellung der Zahl durch den Absolutwert und ein Vorzeichen wählen wir eine Darstellung mit einem negativen Gewicht.
Einerkomplementdarstellung
c1 : [−2n + 1, 2n − 1] → Bn+1
d1 : Bn+1 → [−2n + 1, 2n − 1]
d1 (c1 (z)) = z ∀z ∈ [−2n + 1, 2n − 1]
d1 (hbn+1 . . . b1 i) = (−2n + 1)w(bn+1 +
n
X
w(bi )2i−1
i=1
Achtung: Wir erhalten zwei Darstellungen der Null!
d1 ( h0 . . . 0i ) = 0 = d1 ( hL . . . Li ) = −2n + 1 +
| {z }
| {z }
positive Null
negative Null
n
X
2i−1
i=1
Die Vorteile der Einerkomplementdarstellung sehen wir, wenn wir arithmetische
Operationen ausführen.
128
KAPITEL 2. BINÄRE SCHALTNETZE UND SCHALTWERKE
an
a 1 b1 u
bn
...
AN n
...
sn
u’
a n+1 b n+1
an
bn
s1
a 1 b1 u
...
AN n
u’
VA
u’’
sn
...
s1
AN n+1
s n+1
Abbildung 2.14: zur induktiven Definition des Addiernetzes
2.3. SCHALTNETZE
129
Beispiel: Komplementbildung
d1 (hbn+1 . . . b1 i) = z ⇒ c1 (−z) = h¬bn+1 . . . ¬b1 i
Addition in der Einerkomplementdarstellung: Wir definieren
hsn+2 . . . s1 i = h0an+1 . . . a1 i +2 h0bn+1 . . . b1 i
hrn+2 . . . r1 i = han+1 an+1 . . . a1 i +2 hbn+1 bn+1 . . . b1 i +2 h0 . . . 0sn+2 i
Behauptung: Bei der Addition der durch a und b in Einerkomplementdarstellung
gegebenen Zahlen tritt genau dann ein Überlauf auf (d.h. die Summe der Zahlen
liegt außerhalb von [−2n + 1, 2n − 1]), falls rn+1 6= rn+2 .
an+1 an+1 an . . . a1
bn+1 bn+1 bn . . . b1
sn+1 sn . . . s1
• 1. Fall: an+1 = bn+1 = 0, d.h. beide Zahlen sind positiv, d.h. Überlauf genau
dann, wenn un+1 = L, rn+2 = 0 ⇒ rn+2 6= rn+1 .
• 2. Fall: an+1 6= bn+1 , d.h. eine negative und eine positive Zahl werden addiert.
Gilt un+1 = 0, dann ist rn+1 = rn+2 = L. Gilt un+1 = L, dann ist rn+1 =
r+2 = 0.
• 3. Fall: an+1 = bn+1 = L. Beide Zahlen sind negativ. Nur wenn un+1 = L
ist, findet kein Überlauf statt. Dann gilt rn+1 = rn+2 = L; sonst gilt rn+1 6=
rn+2 ⇒ Überlauf
Ergebnis: In Einerkomplementdarstellung können wir praktisch ohne Aufwand das
Komplement bilden (negieren des Zahlenwerts) und bei Addition der Zahlen unabhängig von der Frage, ob eine der Zahlen (oder beide) negativ ist, ein einfaches
Addiernetz verwenden. Zusätzlich können wir noch durch Verwendung eines zusählichen Bits einen arithmetischen Überlauf feststellen.
Zweierkomplementdarstellung
c2 : [−2n , 2n − 1] → Bn+1
d2 : Bn+1 → [−2n , 2n − 1]
d2 (hbn+1 . . . b1 i) = −2n w(bn+1 ) +
n
X
w(bi )2i−1
i=1
Bemerkungen:
Nur eine Darstellung der Null! d2 (hL . . . Li) = −1.
Komplementbildung:
d2 (hbn+1 . . . b1 i) = z ⇒ c2 (−z) = h¬bn+1 . . . ¬b1 i +2 h0 . . . 0Li
(Einer rückt auf bei der Komplementbildung.)
Dafür wird die Addition einfacher:
hrn+1 . . . r1 i = han+1 an+1 . . . a1 i +2 hbn+1 bn+1 . . . b1 i
Wieder gilt: rn+2 6= rn+1 ⇔ Überlauf bei Addition.
Achtung: Kein Einerrücklauf bei Addition!
130
KAPITEL 2. BINÄRE SCHALTNETZE UND SCHALTWERKE
2.3.6
Weitere arithmetische Operationen
Auch Multiplikation und Division von Binärzahlen können wir durch Schaltnetze
darstellen. Wir beschränken uns auf positive Zahlen.
Multiplikation
∗2 : Bm × Bn → Bm+n , ham . . . an i ∗2 hbn . . . b1 i = hpm+n . . . p1 i. i
Zur Definition von p verwenden wir eine Hilfsdefinition für ri = rm+n+
. . . r1i mit
(
ak−i+1 ∧ bi falls i ≤ k ≤ m + i − 1
i
rk =
0
sonst
1
n
. . . r11 .
Damit gilt hpm+n . . . p1 i = rm+n . . . r1n +2 · · · +2 rm+n
Division
/2 : Bm × Bn → Bm−n+1 für m ≥ n; ham . . . a1 i /2 hbn . . . b1 i = hqm−n+1 . . . q1 i.
Wir setzen ham . . . a1 i <2 hbn . . . b1 0i voraus. Dies kann durch anfügen führenden
Nullen (falls hbn . .
. b1 i =
6 h0
werden. Wir definieren
. . . 0i) immer erreicht
eine Folge
von Binärworten rni . . . r0i durch rn0 . . . r00 = ham . . . am−n+1 i. Gilt rni . . . r0i ≥2
h0bm . . . b1 i, so gilt
qm−n−i+1 = L
. . . r0i+1 = rest( rni . . . r0i −2 h0bm . . . b1 i) ◦ ham−n−i+2 i
rni+1
Sonst ist
qm−n−i+1 = 0
i
rni−1 . . . r0i = rn−1
. . . r0i am−n−i+2
Fazit: Alle arithmetischen Operationen auf natürlichen, ganzen Zahlen und Binärbrüchen (Fixpunktschreibweise) können durch Schaltnetze realisiert werden.
Achtung: Genauigkeit beschränkt - wir rechnen in einem endlichen Teilbereich der
Zahlen.
Gleiches gilt für Operationen der Aussagenlogik.
2.3.7
Schaltnetze zur Übertragung von Information
Wir können über Leitungen Nachrichten übertragen. Sind unterschiedliche Partner
an einer Übertragung beteiligt, so besteht oft der Wunsch, dass Nachrichten gezielt
von einem Partner zum anderen übertrangen werden (Bsp. Telefon, E-Mail). Dies
Erfordert eine Adressierung der Nachrichten (im Bsp. Telefonnummern, E-MailAdressen).
Wir betrachten als elementares Beispiel Schaltungen mit Steuerleitungen. Sei {1, . . . , n}
die Menge der Teilnehmer und c : {1, . . . , n} → Bm eine Codierfunktion, die jedem
Teilnehmer einen Binärcode zuordnet. Weiter sei d : Bm → {1, . . . , m}. Forderung:
d(c(i)) = i ∀i ∈ {1, . . . , n}.
Einfaches
- aus - n - Code
* Beispiel: 1 +
c(i) =
. . 0}
0
. . 0}10
| .{z
| .{z
i−1
n−i
Sei ferner eine weniger redundante Binärcodierung c0 für {1, . . . , n} gegeben:
c0 : {1, . . . , n} → Bj
DF : Bj → Bn
DF(c0 (i))0c(i) ∀1 ≤ i ≤ n
2.4. SCHALTWERKE
131
Teilnehmer 1
j
...
Teilnehmer n
k
...
. . .
k
...
DF
n
...
. k. .
Abbildung 2.15: Multiplexer
Eine Schaltung wie in Abbildung 2.15 bledent aus n Eingangsbündeln der Breite
k alle bis auf eines aus; welches weitergegeben wird, bestimmt die Adresse für DF.
Diese Schaltung nennen wir Multiplexer:
MX : Bj × Bk·n → Bk
Demultiplexer:
DMX : Bj × Bk → Bk·m
2.4
Schaltwerke
Bisher haben wir Schaltnetze betrachtet. Diese hatten keine Zyklen. Nun betrachten
wir Schaltwerke. Diese sind wie Schaltwerke, allerdings sind nun Zyklen zugelassen.
In der Praxis wird eine Schaltung nicht nur ein Mal benutzt (d.h. es wird nicht nur
ein Binärwort eingegeben), sondern nacheinander wird eine Folge von Binärwörtern
eingegeben und an den Ausgängen entsteht eine Folge von Binärworten.
2.4.1
Schaltwerksfunktionen
Schaltfunktion: f : Bn → Bm
Schaltwerksfunktion: g : (Bn )∗ → (Bm )∗ , wobei |g(x)| = |x| für x ∈ (Bn )∗ , d.h.
jedes Eingabewort wird auf ein Ausgabewort abgebildet.
Beispiel: Schaltwerksfunktion
x1 . . . xn y1 . . . ym Zeitpunkt (Takt)
(x1 ) L
0
L
0
1
(y 1 )
2
(x ) 0
L
0
L
2
(y 2 )
3
(x ) L
0
L
0
3
(y 3 )
4
0
L
4
(y 4 )
(x ) 0
L
5
(x ) 0
L
0
L
5
(y 5 )
6
(x ) L
L
L
L
6
(y 6 )
..
..
..
..
..
..
..
.
.
.
.
.
.
.
Achtung: Zwischen xi und y j dürfen auch für i 6= j Abhängigkeiten bestehen.
Wichtig: Zeitflusseigenschaft (Kausalität): Ausgaben zum Zeitpunkt j dürfen höchstens von Eingaben zum Zeitpunkt i ≤ j abhängen.
132
KAPITEL 2. BINÄRE SCHALTNETZE UND SCHALTWERKE
Präfixordnung auf (Bn )∗ : Für x, y ∈ (Bn )∗ gelte
x v y ⇔def ∃z ∈ (Bn )∗ : x ◦ z = y
Die Kausalität entspricht der Präfix-Monotonie: x v x0 ⇒ g(x) v g(x0 ).
Schaltwerksfunktion: g : (Bn )∗ → (Bm )∗ mit |g(x)| = |x| ∀x ∈ (Bn )∗ und g präfixmonoton (d.h. Kausalität ist gegeben).
2.4.2
Schaltfunktionen als Schaltwerksfunktionen
Gegeben sei eine Schaltfunktion f : Bn → Bm . Wir definieren eine Schaltwerksfunktion
f ∗ : (Bn )∗ → (Bm )∗
durch
f ∗ ( x1 , x2 , . . . , xk ) := f (x1 ), f (x2 ), . . . , f (xk )
Die Schaltwerksfunktion f ∗ hat die Eigenschaft, dass die Ausgabe f (xk ) zum Zeitpunkt k lediglich von der Eingabe xk zum Zeitpunkt k abhängt. Die Funktion
speichert nichts von den früheren Eingaben. Solche Schaltwerksfunktionen heißen auch kombinatorisch, die anderen sequentiell.
2.4.3
Schaltwerke
Ein Schaltwerk ist ein gerichteter Graph mit Knoten, die Schaltwerksfunktionen
entsprechen, und Kanten, die Leitungen entsprechen. Jetzt sind Zyklen zugelassen.
In der Praxis werden wir jedoch nur eingeschränkt Zyklen betrachten.
2.4.4
Schaltwerksfunktionen und endliche Automaten
Ein endlicher Automat hat einen Zustand aus einer endlichen Zustandsmenge state.
Er bekommt in einem gegebenen Zustand eine Eingabe aus einer Eingabemenge in
und erzeugt eine Ausgabe y aus einer Ausgabemenge out und einen neuen Zustand aus state. Wir sprechen von einem Zustand.
Wir stellen die Zustandsübergänge durch eine Zustandsübergangsfunktion
δ : state × in → state × out
dar.
Bei Schaltwerken betrachten wir Automaten, bei denen alle drei Mengen durch
Binärwörter dargestellt werden (z, m, n ∈ N):
δ : Bz × Bn → Bz × Bm
Solche Automaten lassen sich kompakt durch Zustandsübergangsdiagramme darstellen.
Beispiel: B2 × B2 → B2 × B2 . Zustandsübergangsdiagramm: s. Abb. 2.16.
Achtung: Bestimmte Binärkombinationen treten als Zustände und Eingänge nicht
auf.
Die Kreise stellen die Zustände dar. Ein Pfeil vom Kreis σ1 zum Kreis σ2 mit
Beschriftung x/y steht für δ(σ1 , x) = (σ2 , y).
Ein Zustandsübergang besteht in einem Wechsel von einem Zustand σ1 zu einem
Zustand σ2 . Ausgelöst wird der Übergang von einer Eingabe x und er erzeugt eine
Ausgabe y.
Schaltwerke können in ihrem Verhalten durch
2.4. SCHALTWERKE
133
L0/L0
00/0L
0L/0L
L0
0L
00/L0
L0/L0
0L/0L
Abbildung 2.16: Zustandsübergangsdiagramm
• Automaten mit Ein-/Ausgabe
• Schaltwerksfunktion
beschrieben werden. Dabei gilt: jede Schaltewerksfunktion definiert einen Automaten.
Automaten definieren Schaltwerksfunktionen
Gegeben sei δ : state × in → state × out. Jeder Zustand σ ∈ state definiert eine
Schaltwerksfunktion fσ : in∗ → out∗ wie folgt: Seien a ∈ in, x ∈ in∗ und b ∈ out.
fσ (hai ◦ x) = hbi ◦ fσ0 (x) ⇔ delta(σ, a) = (σ 0 , b)
fσ beschreibt das Ein-/Ausgabeverhalten des Automaten, wenn er im Zustand σ
gestartet wird.
Beispiel: obiger Automat Es gilt fL0 (h00i ◦ x) = hL0i ◦ fL0 (x) und f0L (h00i ◦
x) = hoLi ◦ f0L (x), d.h. fσ (h00i ◦ x) = hσi ◦ fσ (x) (Lesegleichung: Durch Eingabe
von h00i wird der Zustand ausgegeben (gelesen) und beibehalten).
Für σ, σ 0 ∈ {0L, L0} gilt fσ (hσ 0 i ◦ x) = hσ 0 i ◦ fσ0 (x) (Schreibgleichung: Der Zustand
σ 0 wird geschrieben: Die Eingabe von σ 0 ∈ {0L, L0} führt dazu, dass der Automat
in den Zustand σ 0 übergeht).
Fazit: Der Automat speichert durch seinen Zustand genau ein Bit, das beliebig oft
gelesen oder überschrieben werden kann.
Schaltwerksfunktionen definieren Automaten
Wir ordnen einer Schaltwerksfunktion f : in∗ → out∗ einen Automaten δf : SWF ×
in → SWF × out zu. Dabei steht SWF für die Menge der Schaltwerksfunktioonen
in∗ → out∗ . Dabei gelte für g, g 0 ∈ SWF, a ∈ in, b ∈ out: δf (g, a) = (g 0 , b), wobei b
und g 0 definiert sind durch g(hai ◦ x) = hbi ◦ g 0 (x) für alle x ∈ in∗ .
Da g eine Schaltwerksfunktion ist, ist b durch a eindeutig bestimmt. Es gilt g 0 (x) =
rest(g(hai ◦ x)). Jede Funktion f ∈ SWF definiert einen Zustand des Automaten.
Jedes Schaltwerk kann in seinem Verhalten entweder als Automat mit Ein-/Ausgabe
oder als Schaltwerksfunktion beschrieben werden - die Darstellungen sind ineinander
überführbar.
2.4.5
Schaltwerke zum Speichern: Verzögerungsnetze
Rückkopplung: das Flip-Flop (s. Abb. 2.17)
Es gilt Qneu = ¬(r ∨ Q), Qneu = ¬(s ∨ Q).
Beispiel für das Schalten des Flip-Flops:
• Schreiben:
134
KAPITEL 2. BINÄRE SCHALTNETZE UND SCHALTWERKE
s
Q
Q
r
Abbildung 2.17: Flip-Flop: Nor-Latch
L L L L L ...
s
r
0 0 0 0 0 ...
Q
0 0 L L L ...
Q
L 0 0 0 0 ...
Qneu 0 L L L L . . .
Qneu 0 0 0 0 0 . . .
Idee: Wir wiederholen die gleiche Eingabe aus s und r, bis die Q, Q stabilisieren.
• Lesen:
s
r
Q
Q
Qneu
Qneu
0
0
0
L
0
L
0
0
0
L
0
L
0
0
0
L
0
L
0
0
0
L
0
L
...
...
...
...
...
...
Das Flip-Flop ähnelt in seinem Schaltverhalten stark dem Automaten, den wir
im vorhergehenden Beispiel beschrieben haben. Allerdings treten beim Schreiben
Zwischenzustände auf. Das Flip-Flop stabilisiert sich nach einem Zwischenschritt.
Allgemeine Feststellung:
(1) Bei komplizierten Schaltungen können vorher Zwischenschritte erforderlich
sein, um einen stabilen Zustand und eine stabile Ausgabe zu erreichen.
(2) Unter gewissen Umständen tritt keine Stabilisierung ein.
Bei Schaltungen, die sich stabilisieren, ergeben sich zwei Sichten:
• Mikrosicht: Automat, Schaltwerksfunktion, in der der Stabilisationsvorgang
sichtbar ist.
• Makrosicht: Abstraktion der Mikrosicht. Wir wiederholen in der Mikrosicht
die Eingabe, bis Stabilisierung eintritt, und nehmen die vorliegende Ausgabe
als Ausgabe und den dann vorliegenden Zustand als neuen Zustand. Die Zahl,
wie oft die Eingabe wiederholt wird, bis eine Stabilisierung erreicht wird,
bestimmt die Frequenz und den Takt der Schaltung.
2.4.6
Klassen von Schaltwerken
Typischerweise baut man nicht Schaltwerke mit beliebig komplizierten, verzögerungsfreien Rückkopplungsleitungen, sondern verwendet Flip-Flops zur Realisierung
von Speichern und verwendet sonst nur verzögerte Rückkopplungen.
2.4. SCHALTWERKE
135
Verzögerung (delay):
Dn : (Bn )∗ → (Bn )∗
a ∈ Bn
Dna (hx1 . . . xn i) = hax1 . . . xn−1 i
d.h. die Ausgabe ist um einen Takt verzögert.
2.4.7
Komposition von Schaltwerken und Schaltnetzen
Aus Speichergliedern (Flip-Flops) und Verzögerungsgliedern können wir, unter Verwendung der Konzepte aus den Schaltnetzen, beliebig komplizierte und mächtige
Schaltungen aufbauen:
• Speicher
• Verarbeitungswerke
• Übertragungsnetze
Damit lassen sich durch Schaltungen Bausteine (Chips) realisieren, aus denen dann
Rechner aufgebaut werden können.
136
KAPITEL 2. BINÄRE SCHALTNETZE UND SCHALTWERKE
Kapitel 3
Aufbau von Rechenanlagen
Eine Rechenanlage kann zunächst als riesiges Schaltwerk begriffen werden. Diese
Sichtweise zeigt uns, wie wir uns die Realisierung einer Rechenanlage vorstellen
können, ist aber für die Nutzung und Programmierung nicht sehr hilfreich. Programme entsprechen auf Schaltwerksebene bestimmten gespeicherten Binkombinationen (Bitwörtern).
Für das Entwickeln größerer Programme brauchen wir eine strukturierte Sicht auf
die Rechenanlage, durch die Programme greifbarer werden.
3.1
Strukturierter Aufbau von Rechenanlagen
Das Grobkonzept eines Rechners (von-Neumann-Architektur) ist sein 50 Jahren
praktisch unverändert. Eine Rechenanlage besteht demnach aus:
• Rechnerkern / Prozessor / CPU
• Speicher
• Verbindungshardware (Bussysteme)
• Peripherie (Ein-/Ausgabevorrichtungen)
Wir sind im Weiteren primär an der Programmierung von Rechenanlagen interessiert. Somit interessiert uns die physikalische Struktur nur insoweit, als sie die
Programme auf Maschinenebene beeinflusst.
Dabei stellt sich die Frage, mit welchen Mitteln die Struktur einer Rechenanlage
modelliert wird. Wir verwenden die Idee der Zustandsmaschine um eine Rechenanlage zu beschreiben. Dabei konzentrieren wir uns auf die Zustandsübergänge, die
durch die Ausführung von Befehlen ausgelöst werden. Der Zustand einer Rechenanlage ist gegeben durch die Belegung ihrer Speicherzellen und Register (spezielle
zusätzliche Speicherzellen für besondere Aufgaben).
Zur Erläuterung der Struktur einer Rechenanlage und ihrer Programmierung verwenden wir eine Maschine MI (angelehnt an die VAX von Digital). Die Bestandteile des Rechners, insbesondere seine Speicherzellen und Register, stellen wir durch
Programmvariable geeigneter Sorten dar und die Wirkungsweise der Programme /
Befehle durch zuweisungsorientierte Programme, die auf diesen Programmvariablen
arbeiten.
Wichtig: Im Speicher stehen Programme und Daten!
Der Modellmaschine MI besteht im Prinzip aus vier Rechnerkernen, zwei E/AProzessoren, dem Arbeitsspeicher und dem Bussystem.
137
138
KAPITEL 3. AUFBAU VON RECHENANLAGEN
Rechnerkern
E/A−Prozessoren
Steuerwerk
Peripherie
Rechenwerk
Bus (Verbindung)
Speichereinheit
Abbildung 3.1: Schematischer Aufbau eines von-Neumann-Rechners
Plattenspeicher
Arbeitsspeicher
E/A 1
E/A 2
Adressbus
Datenbus
Steuerbus
Abbildung 3.2: Spezifische Rechnerarchitektur der MI
. . .
. . .
Rechnerkern
Terminals
3.1. STRUKTURIERTER AUFBAU VON RECHENANLAGEN
3.1.1
139
Der Rechnerkern
Rechnerkern: Hardware-Betriebsmittel, welches autonom den Kontrollfluss steuert (entscheidet, welcher Befehl ausgeführt wird) und die Datentransformationen
ausführt. Der Rechnerkern wird in Steuerewerk und Rechenwerk unterteilt.
Das Steuerwerk enthält den Taktgeber und die Register, die die Information enthalten, die für die Ablaufsteuerung erforderlich ist (Befehlszähler, . . . ). Das Steuerwerk
erzeugt auch die Steuersignale für die Ansteuerung des Rechenwerks.
Das Rechenwerk enthält die Operandenregister und die Schaltnetze/-werke zur
Ausführung der Operationen. Damit bestimmt das Rechenwerk den Befehlsvorrat.
Die Register im Rechnerkern sind Speicherzellen mit extrem kurzen Zugriffszeiten.
Die Bestandteile der MI werden durch folgende Deklarationen von Programmvariablen charakterisiert:
16 Register für 32-Bit-Wörter: var [0:31] array bit R0, ..., R15
Aufgaben:
R0 : R13 frei verwendbar für Programmierung
R14
Zeiger für Stapel (stack pointer, SP)
R15
Befehlszähler (program counter, PC)
Zusäzlich im Steuerewerk:
var [0:7] array bit IR
Instruktionsregister
var [0:31] array bit PSL Prozessorstatusregister
Weitere Hilfsregister für Steuer-/Rechenwerk:
var [0:31] array bit tmp0, ..., tmp3 Hilfsregister für die ALU
var [0:7] array bit am
legt Adressiermodus fest
var [0:31] array bit adr
für die Adressrechnung
var [0:31] array bit index
für die Indexrechnung
Die Anzahl der Register in einem Rechnerkern kann sehr stark schwanken (früher
oft nur ein Register: Akkumulator, heute ganze Registerbänke).
3.1.2
Speichereinheit
Die Speichereinheit besteht aus einer Folge von Speicherzellen, auf die über Adressen zugegriffen wird.
MI:
var [0:232 − 1] array [0:7] array bit M Speicher
var [0:31] array bit MAR
Speicheradressregister
var [0:31] array bit MBR
speicherregister
3.1.3
E/A
Die Ein-/Ausgabe erfolgt über Peripheriegeräte durch die E/A-Prozessoren. (genaueres später)
3.1.4
Befehle und Daten auf Maschinenebene
Auf Maschinenebene werden alle Daten und Befehle durch Bitwörter dargestellt
(Binärdarstellung). Ein Befehl ist ein Binärwort, das im Hauptspeicher gespeichert
werden kann und bei Ausführung (laden in den Rechnerkern und Interpretation)
eine Zustandsänderung der Maschine bewirkt.
Bestandteile eines Befehls:
• Charakterisierung der Operation
• Charakterisierung der Operanden
140
KAPITEL 3. AUFBAU VON RECHENANLAGEN
Wir unterscheiden eine ganze Reihe unterschiedlicher Befehlsarten.
Die Operanden kennzeichnen die Werte, mit denen wir arbeiten: Sie werden immer
durch Bitwörter dargestellt, können aber ganz unterschiedliche Werte bezeichnen:
• Adressen
• Zahlen
• Charakter
Darstellung von Zahlen durch Bitwörter:
natürliche Zahlen
Binärschreibweise
ganze Zahlen
Zweierkomplementdarstellung
Fixpunktdarstellung
Darstellung von Zahlen aus [−1, 1[
Gleitpuntkdarstellung Darstellung von Zahlen durch Wert und Exponent
Festpunktdarstellung: Zweierkomplementdarstellung (32 Bit), Multiplikation mit
2−31 .
Gleitpunktdarstellung: b = hb0 b1 . . . bn bn+1 . . . bn+m i mit b0 : Vorzeichen, b1 . . . bn
Exponent, bn+1 . . . bn+m Mantisse.
Wir berechnen den dargestellten Zahlenwert mit Hilfe folgender Hilfswerte:
e=
n
X
2n−i w(bi )
i=1
v=
m
X
2−i w(bn+1 )
i=1
Es gilt 0 ≤ e < 2n , 0 ≤ v < 1.
exp = e − 2n−1 + 1 ⇒ −2n−1 + 1 ≤ exp ≤ 2n−1
man = 1 + v ⇒ 1 ≤ man < 2
Fälle der Interpretation:
• Normalfall: −2n−1 + 1 < exp < 2n−1 . Wir erhalten als dargestellt Zahl
(−2)w(b0 ) · 2exp · man
• Darstellung der Null: e = 0 ∧ v = 0
• Unterlauf: e = 0 ∧ v 6= 0
• Überlauf: e = emax = 2n − 1 ∧ v = 0
• Fehlerfall: e = emax = 2n − 1 ∧ v 6= 0
Unterlauf, Überlauf und Fehlerfall sind keine gültigen Zahldarstellungen.
Auf Maschinenebene werden alle Daten durch Bitsequenzen dargestellt.
Sorte
Kennung
Wortlänge
sort byte = [0:7] array bit
B
8
sort half-word = [0:15] array bit
H
16
sort word = [0:31] array bit
W
32
sort floating-point = [0:31] array bit
F
32 (8 + 23)
sort double-float = [0:63] array bit
D
64 (11 + 52)
Problem: Einer Ziffernfolge 37216 ist nicht mehr anzusehen, ob sie in Oktal-, Dezimaloder Hexadezimaldarstellung ist.
Ausweg: (37216)8 oktal, (37216)10 dezimal, (37216)16 hexadezimal
Konvention: Wir verwenden Dezimaldarstellung und geben Oktal- und Hexadezimaldarstellung explizit an.
3.1. STRUKTURIERTER AUFBAU VON RECHENANLAGEN
141
Auch Befehle werden auf Maschinenebene durch Bitsequenzen darstestellt. Bequemer und lesbarer sind Darstellungen, bei denen die Teile des Befehls durch Namen
udn Dezimalzahlen dargestellt werden.
Bestandteile eines Befehls:
• Operation: Angabe, welche Umformung bzw. welche Umspeicherung ausgeführt
wird
• Operandenspezifikation: Angabe, mit welchen Werten / Speicherzellen / Registern dabei gearbeitet wird
Syntax:
hcommandi ::= hinstructionParti {hopParti {, hopParti}∗ }
Es gibt Maschinen mit starrem Befehlsformat (festgelegte Länge der Bitsequenz).
Nachteil: Jeder Befehl hat die gleiche Anzahl von Bits für Operandenspezifikationen
zur Verfügung. Vorteil: Jeder Befehl passt genau in eine Speicherstelle, das Ansteuern von Befehlen wird einfacher.
Die MI hat flexible Befehlsformate.
Der Instruktionsteil (Operationsangabe) des Befehls wird im Rechnerkern (im Steuerwerk) in Steuerbits umgesetzt, die die entsprechenden Umformungen und Umspeicherungen auslösen.
3.1.5
Operandenspezifikation und Adressrechnung
Jeder Befehl arbeitet mit bestimmten Werten (Bitsequenzen) und bestimmten Speicherplätzen / Registern. Wir sprechen von Operanden.
Beispiel: Einfachste Art der Operandenspezifikation: wir geben das Register W
R9 (W: Kennung, R9: Register) oder die Speicherzelle W 1743 direkt an. Damit
werden die Speicherzellen 1743-1746 angesprochen.
Die einfachste Form der Angabe von Operanden ist die direkte (absolute) Adressierung: Es werden Register / Adresse explizit angegeben. Weil wir an mehr Flexibilität interessiert sind und uns in einem Programm nicht festlegen wollen, an welcher
Stelle im Speicher es (absolut) steht, verwenden wir kompliziertere Adressierverfahren. Dazu verwenden wir eine Operandenspezifikation. Dies ist eine Angabe, aus der wir die Operanden berechnen können (Adressrechnung). Jeder Operand
entspricht einer Lokation (einem Speicherplatz oder Register; genauer: einem
Ausschnitt aus einem Speicher / Register oder einer Folge). Ein Operand bestimmt
einen bestimmten Platz (Lokation) in der Maschine, an der eine Bitsequenz gespeicher ist.
sort operand = register (type k, nat n) | memo (type k, nat n)
sort type = {B, H, W, F, D}
Eine Operandenspezifikation ist ein syntaktischer Ausdruck, der angibt, wie der
Operand zu berechnen ist. Jede Operandenspezifikation wird bei Ausführung eines Befehls im Befehlszyklusin einen Operanden umgerechnet. Dazu verwenden wir
folgende Hilfsprozeduren:
1
2
4
6
8
fct val = (operand x) seq bit:
if x in register then R[n(x)][32-wl(k(x)):31]
else get (wl(k(x))/8, n(x))
fi
fct get = (nat i, nat n) seq bit:
if i = 0 then empty
else conc (M[n], get(i-1, n+1))
fi
142
KAPITEL 3. AUFBAU VON RECHENANLAGEN
Adressierarten:
• direkte Adressierung: Die absolute Adresse (oder das Register) wird im
Adressteil angegeben.
• direkte Angabe des Operandenwerts im Befehl als Binär-, Hexadezimal- oder Dezimalzahl.
• relative Adressierung: Zum Adressteil im Befehl wird der Inhalt eines
Registers addiert.
• indirekte Adressierung / Adresssubstitution: Die Speicherzelle, die
über den Adressteil angesteuert wird, enthält selbst eine Adresse der Speicherzelle, die den Operanden bildet.
• Indizierung: Zur ermittelten Adresse wird ein weiterer Wert addiert (typische Anwendung: hochzählen eines Zählers in einer Wiederholungsanweisung
zum Zugriff auf Feldelemente)
BNF-Syntax der MI-Operandenspezifikation:
hopParti ::= habsoluteAddressi
| himmediateOpi
| hregisteri
| hrelativeAddressi
| hindexedRelativeAddressi
| hindirectAddressi
| hindexedIndirectAddressi
| hstackAddressi
Ein Teil der Adressierart wird durch folgende Werte bestimmt:
1
2
4
sort operandSpec = opspec (nat reg, bool isr, bool idr,
incr icr, nat rel, indexSpec int)
sort indexSpec = {-} | reg (nat)
sort incr = {-1 | 0 | 1 }
• absolute Adressierung:
habsoluteAddressi ::= hintexpi
(integer expression)
• Operand als Wert:
himmediateOpi ::= I hintexpi
• Register als Operand:
hregisteri ::= R0| . . . |R15|P C|SP
• relative Adresse:
hrelativeAddressi ::= {hintexpi +}! hregisteri
• indizierte relative Adressierung:
hindexedRelativeAddressi ::= hrelativeAddressi hindexi
hindexi ::= / hregisteri /
3.1. STRUKTURIERTER AUFBAU VON RECHENANLAGEN
143
• indirekte Adressierung:
hindirectAddressi ::=!(hrelativeAddressi)|!! hregisteri
• indizierte indirekte Adressierung:
hindexedIndirectAddressi ::= hindirectAddressi hindexi
• Stack-Adressierung:
hstackAddressi ::= −! hregisteri |! hregisteri +
3.1.6
Der Befehlszyklus
Der Befehlszyklus bezeichnet die Folge der Schritte, die bei der Ausführung eines
Befehls ausgeführt werden. Die Maschinen durchlaufen immer wieder die gleiche
Sequenz von Schritten und führen dabei jeweils einen Befehl aus. Wir beschreiben
den Befehlszyklus wiederum durch ein Programm. In der MI besteht ein Befehl aus
einem Operationsteil und bis zu vier Operanden.
In der Maschine ist der Operationsteil durch ein Byte repräsentiert. In diesem Byte
sind die folgenden Informationen verschlüsselt:
fct args = (byte) nat
Anzahl der Operanden
fct kenn = (byte) type
Kennung
fct result = (byte) bool Wird ein Resultat erzeugt?
Der Befehlszyklus der MI entspricht dem folgenden Programm.
1
2
4
6
8
10
12
14
16
18
20
22
24
26
28
while not(stop) do
fetch_operator;
if 1 <= args(IR) then
fetch_operand (kenn(IR), tmp1[32-wl(kenn(IR)):31])
fi;
if 2 <= args(IR) then
fetch_operand (kenn(IR), tmp2[32-wl(kenn(IR)):31])
fi;
if 2 <= args(IR) then
fetch_operand (kenn(IR), tmp3[32-wl(kenn(IR)):31])
fi;
execute (IR);
if result (IR) then
put_result
fi
od
proc fetch_operator =:
fetch (B, R15, IR);
R15 := R15 + 1
proc fetch = (type k, [0:31] array bit adr,
var array[0:wl(k)-1] array bit r):
MAR := adr;
for i := 1 to wl(k)/8 do
MBR[(i-1)*8:i*8-1] := M[MAR];
MAR := MAR + 1
od;
r := MBR[0:wl(k)-1]
144
KAPITEL 3. AUFBAU VON RECHENANLAGEN
30
32
34
36
38
proc put = (type k, [0:31] array bit adr,
var array[0:wl(k)-1] array bit r):
MAR := adr;
MBR[0:wl(k)-1] := r;
for i := 1 to wl(k)/8 do
M[MAR] := MBR[(i-1)*8:i*8-1];
MAR := MAR + 1;
od
Für die Adressrechnung zur Ermittlung der Operanden benötigen wir eine weitere
Information, die aus dem Byte ermittelt wird, das den Adressiermodus bestimmt:
fct reg = (byte) nat
Registerzahl
fct isr = (byte) bool
steht Operand im Register?
fct isrel = (byte) bool relative Adressierung?
fct idr = (byte) bool
indirekte Adressierung?
fct isidx = (byte) bool Indexadressierung?
fct icr = (byte) incr
Registerinkrement / -dekrement
Bei der Ausführung eines Befehls (execute) wird nicht nur ein Ergebnis berechnet
und zurückgeschrieben. Es werden auch bestimmte Bits in einem zusätzlichen Register (dem Prozessorstatusregister) gesetzt. Dabei sind folgende vier Bits für uns
von besonderer Bedeutung:
C carry
Übertrag
V overflow Überlauf
Z zero
Resultat ist Null
N negative Resultat ist negativ
Die Schritte zum Bereitstellen der Operanden und der Abspeicherung des Resultats
sind für alle Befehle, abhängig von der Zahl der Argumente und der Frage, ob ein
Resultat anfällt, gleich. Die Wirkung eines Befehls wird durch die Rechenvorschrift
proc execute = (byte): ...
beschrieben.
3.2
Hardwarekomponenten
Wir unterscheiden folgende Bestandteile einer Rechenanlage:
• Prozessor
• Speicher
– Hauptspeicher
– Hintergrundspeicher
– Speichermittel für das Archivieren
• Eingabegeräte
• Ausgabegeräte
• Datenübertragungsgeräte / Netze
Kapitel 4
Maschinennahe
Programmierung
Die im letzten Kapitel eingeführten Programmstrukturen beschreiben am Beispiel
der MI die Wirkungsweise eines Rechners. Nun führen wir die einzelnen Befehle ein,
beschreiben ihre Wirkung und formulieren erste kurze Maschinenprogramme.
Grundsätzliche Feststellungen:
• Ein Maschinenprogramm ist eine endliche Folge von Befehlen, die zur Ausführungszeit im Hauptspeicher stehen. Damit hat jeder Befehl eine absolute
und eine relative Adresse.
• Diese Befehle werden, gesteuert durch den IC (PC) nacheinander ausgeführt.
• Jedes Programm, das nicht in Maschinensprache geschrieben ist, muss
– von Hand oder durch ein Übersetzungsprogramm (Compiler) in Maschinensprache übersetzt werden, bevor es ausgeführt werden kann, oder
– ein Interpretationsprogramm (Interpreter) angegeben werden; dies ist
ein in Maschinensprache gegebenes Programm, das das Programm in
Programmiersprache durchläuft und dabei die entsprechenden Befehle
ausführt.
4.1
Maschinennahe Programmiersprachen
Struktur, Form und Umfang eines Befehlssatzes und der einzelnen Befehle wird
durch den Rechnerkern geprägt.
Maschinenprogramme sind Folgen von Befehlen und weisen somit keine besondere
Struktur auf. Es ist Aufgabe des Programmierers, Maschinenprogramme strukturiert zu schreiben. Deshalb:
! Trickreiche Programmierung vermeiden!
! Transparente Programmstrukturierung anstreben!
! Ausreichend dokumentieren!
! Umfangreiche Programme in Programmteile strukturieren!
145
146
KAPITEL 4. MASCHINENNAHE PROGRAMMIERUNG
4.1.1
Binärwörter als Befehle
In der MI entspricht ein Programm zur Ausführungszeit einer Folgt von Bytes im
Speicher. In dieser Form ist ein Programm für Menschen praktisch unlesbar.
∗
hbinaryMachineCommandi ::= hbytei
hbinaryAddressi ::={0|L}8L
hbinaryMachineProgrammi ::={hbinaryAddressi hbytei}∗
|{hbinaryAddressi hbinaryMachineCommandi}
Wir vermeiden diese Form der Befehlsangabe und wählen eine etwas lesbarere. Dabei werden Befehle durch Angabe der Befehlsbezeichnung, des Typs der Operanden
und der Adressen in Dezimalschreibweise beschrieben.
4.1.2
Der Befehlsvorrat der MI
Die Wirkung eines Befehls wird durch die Rechenvorschrift execute im Befehlszyklus festgelegt. Dabei werden gewisse Hilfsregister / Spezialregister und insbesondere das PSL (Prozessorstatusregister) geändert.
Beispiele für Befehle der MI:
• Transportbefehle - Umspeichern von Informationen
MOVE W 1000, 2000
Es wird der Inhalt der Speicherzellen 1000 - 1003 in tmp1 geladen, in tmp0
umgesetzt und in die Speicherzellen 2000 - 2003 zurückgespeichert.
MOVE B 1000, 2000
Der Inhalt der Speicherzelle 1000 wird mit Hilfe der temporären Register in
die Zelle 2000 geschrieben.
MOVE B 1000, R4
R4[24:31] wird durch das Byte in Zelle 1000 überschrieben.
MOVEA 1000, 2000
(move address) ist wirkungsgleich zu MOVE W I1000, 2000: der Wert (die
Adresse) 1000 wird den Speicherzellen 2000 - 2003 zugewiesen.
CLEAR B R7
Dieser Befehl bewirkt das Überschreiben von R7[24:31] durch das Byte 0016 .
• Logische Operationen
OR W R8, 3001, R1
bewirkt bitweise Disjunktion der 32 Bit in R8 mit den 32 Bit in 3001 - 3004; das
Resultat wird in Register R1 geschrieben. Typische Anwendung: Maskieren
von Werten.
• Arithmetische Operationen
ADD, SUB, MULT, DIV
Wirkung entsprechend der Zahldarstellung (gesteuert über die Kennung), s.
später
• Vergleichsoperationen
CMP W 3007, R6
Dieser Befehl dient dazu, gewisse Bits im PSL zu setzen. Im Beispiel werden
die 32 Bit in den Zellen 3007 - 3010 in tmp1 und die 32 Bit aus R6 in tmp2
geschrieben. Dann wird tmp1 mit tmp2 verglichen:
1
2
Z := (tmp1 = tmp2)
N := (tmp1 < tmp2)
4.1. MASCHINENNAHE PROGRAMMIERSPRACHEN
147
• In Sprungbefehlen können wir nun auf die so gesetzten Bits Bezug nehmen.
JUMP 5000
Einfacher (unbedingter) Sprung: der Befehlszähler wird mit Adresse 5000 belegt.
JEQ 5000
(jump equal): Der Befehlszähler wird mit Adresse 5000 belegt, falls Z=L
gilt; anderenfalls hat der Befehl keine Wirkung.
• Shiftbefehle erlauben es, die Bits in einem Operanden zu verschieben. (im
Bsp: Rechtsshift um 2 Bit)
LL000L0L
0LLL000L
Zwei Bits werden frei und können entweder durch Nachziehen von 0 oder L
belegt werden oder durch kreisförmiges Durchschieben.
Dieser Befehlssatz ist nur ein Ausschnitt der wichtigsten Befehle der MI. Reale Maschinen haben oft erheblich umfangreichere Befehlssätze, wobei oft nur Abkürzungen für Befehle oder das Zusammenfassen mehrerer Befehle zu einem dadurch
möglich wird.
Wir haben als wichtige Befehle nur die ausgeklammert, die für die Systemprogrammierung von Bedeutung sind.
4.1.3
Einfache Maschinenprogramme
Zur Ausführungszeit der Programme stehen Daten und Programme gleichermaßen
binär codiert im Speicher. Dabei ist es ratsam, Daten und Programme in getrennten
Speicherbereichen abzulegen.
Beispiel: Speicherorganisation
Adresse (hex) Verwendung
0000 0000
reserviert
0000 0400
Programmspeicher
3FFF FFFF
4000 0000
Kontrollbereich (Daten, Stack)
7FFF FFFF
8000 0000
Systembereich
BFFF FFFF
C000 0000
reserviert für Systemprogramme
FFFF FFFF
Die im Beispiel verwendeten Adressen sind virtuell, d.h. die tatsächlich im Hauptspeicher vorhandene Menge von Adressen ist erheblich kleiner. Zur Ausführungszeit
werden den virtuellen Adressen richtige Adressen zugeordnet. Ein Teil der Daten
steht im Hintergrundspeicher und wird bei Bedarf in den Hauptspeicher geladen.
Zur Erhöhung der Lesbarkeit unserer Programme arbeiten wir mit Adressen, ohne
uns um die Frage zu kümmern, ob diese virtuell oder real sind, und verwenden in
Programmen symbolische Adressen für Sprungbefehle.
Beispiele für Maschinenprogramme
148
KAPITEL 4. MASCHINENNAHE PROGRAMMIERUNG
(1) Berechnen des Maximums zweier Zahlen: Seien zwei Zahlen (32 Bit) im Speicher durch die Adressen in den Registern R1 und R2 gegeben. Das Maximum
der Zahlen soll in Register R0 geschrieben werden.
1
2
4
6
CMP W !R1, !R2
JLE max2
MOVE W !R1, R0
JUMP ende
max2: MOVE W !R2, R0
ende:
(2) Durchsuchen eines Speicherabschnitts. Gegeben seien Adressen in R0 und R1,
wobei R0 ¡ R1 gelte. Der Speicher von R0 bis R1 soll nach einem 32-Bit-Wert,
der in R2 steht, durchsucht werden. Falls der Wert gefunden wird, ist die
Adresse dazu in das Register R3 zu schreiben. Anderenfalls wird 0 nach R3
geschrieben.
Bei etwas komplizierteren Aufgaben ist es hilfreich, die Lösung zuerst als problemorientiertes Programm zu formulieren und dann erst das MI-Programm
zu schreiben.
1
2
4
6
8
R3 := R0;
suche: if M[R3] =
else R3 :=
if R3
fi;
R3 :=
fi
ende:
R2 then goto ende
R3 + 4;
<= R1 then goto suche
0
Umsetzung in ein MI-Programm:
1
2
4
6
8
MOVE W R0, R3
suche: CMP W !R3, R2
JEQ ende
ADD W I4, R3
CMP W R3, R1
JLE suche
CLEAR W R3
ende:
(3) Sortieren der Inhalte eines Speicherbereichs.
Aufgabe: Man sortiere die Bytes im Speicher zwischen der Adresse in R0 und
der Adresse in R1 aufsteigend. R2, . . . , R11 dürfen als Hilfsregister verwendet
werden.
Wir verwenden einen einfachen Algorithmus:
1
2
4
6
8
R2 := R0;
while R2 < R1 do
if M[R2] > M[R2+1] then
M[R2], M[R2+1] := M[R2+1], M[R2];
if R0 < R2 then R2 := R2 - 1 fi
else R2 := R2 + 1
fi
od
4.1. MASCHINENNAHE PROGRAMMIERSPRACHEN
149
Wir brechen das Programm in Sprungbefehle auf:
1
2
4
6
8
10
12
R2 := R0;
if R2 >= R1 then goto ende fi;
R3 := R2 + 1;
if M[R2] > M[R3] then goto vertauschen fi;
R2 := R3;
goto m_while;
vertauschen: R4[24:31] := M[R2];
M[R2] := M[R3];
M[R3] := R4[24:31];
if R0 = R2 then goto m_while;
R2 := R2 - 1;
goto m_while;
ende:
m_while:
Nun ist das Schreiben des MI-Programms einfach:
1
2
4
6
8
10
12
14
16
4.1.4
MOVE W R0, R2
CMP W R1, R2
JLE ende
ADD W I1, R2, R3
CMP B !R3, !R3
JLT vertauschen
MOVE W R3, R2
JUMP loop
vertauschen: MOVE B !R2, R4
MOVE B !R3, !R2
MOVE B R4, !R3
CMP W R0, R2
JEQ loop
SUB W I1, R2
JUMP loop
ende:
loop:
Assemblersprachen
Assemblersprachen sind maschinennahe Programmiersprachen, die durch geringfügige Erweiterung reiner Maschinensprachen entstehen. Die Umsetzung von Assemblersprachen in reine Maschinensprachen erfolgt durch Programme, sogenannte
Assemblierer (engl. assembler). Typische Erweiterungen in Assembler sind:
• mehrfache Marken
• eingeschränkte arithmetische Ausdrücke
• direkte Operanden (in der MI ohnehin vorgesehen)
• symbolische Adressierung (analog zu Programmvariablen)
• Segmentierung (Bindungs- und Gültigkeitsbereiche für Namen)
• Definition und Einsetzung von Substitutionstexten (Makros)
• Substitution von Makros = Makroexpansion
Über die geschickte Einführung von Makros können wir auf bestimmte Anwendungssituationen zugeschnittene Assemblerstile definieren.
150
4.1.5
KAPITEL 4. MASCHINENNAHE PROGRAMMIERUNG
Ein- und Mehradressform
Eine wesentliche Charakteristik einer Maschinensprache ist die Anzahl der Operanden pro Befehl.
• starres Befehlsformat: einheitliche Zahl von 0, 1, 2, 3 Operanden pro Befehl
• flexibles Befehlsformat (siehe MI): Befehle haben zwischen 0 und n ∈ N Operanden
Bei starrem Befehlsformat sprechen wir von Einadressform, wenn jeder Befehl genau
einen Operanden enthält (entsprechend Zwei- und Dreiadressform). Wir erhalten bei
Dreiadressform folgende Syntax:
hbefehli ::= hregisteri ::=[hoperandi
| − hoperandi
|AC
| hoperandi hoperatori hoperandi]
if hbedingungi then goto hmarkei f i
|if
|skip
goto hmarkei
|goto
hbefehlsfolgei ::={{hmarkei}∗ hbefehli}∗
hspeedi ::= hkonstantei
|AC
|h[1]|h[2]| . . .
|a|b| . . .
hbedingungi ::= hregisteri = 0
| hregisteri =
6 0
| hregisteri < 0
| hregisteri ≤ 0
hoperatori ::={+, −, ∗, /, . . .}
hregisteri ::=AC
|a|b| . . .
|h[1]|h[2]| . . .
Wir sprechen von einem Programm in Einadressform, wenn in jedem Befehl außer
AC höchstens eine weitere Registerangabe auftritt. Der AC ist das zentrale Rechenregister, auf dem alle Berechnungen ausgeführt werden.
Achtung: Dreiadressformen können immer in Einadressformen übersetzt werden.
Beispiel
a := b + c
Dreiadressform
wird umgeschrieben in
AC := b
AC := AC + c
a := AC
4.1. MASCHINENNAHE PROGRAMMIERSPRACHEN
4.1.6
151
Unterprogrammtechniken
Wird eine Folge von Befehlen an mehreren Stellen in einem Maschinenprogramm
benötigt, so bietet es sich an, das Programm nicht mehrfach zu schreiben, sondern
• ein Makro zu definieren
• ein Unterprogramm zu definieren, d.h. ein Programmstück, auf das über eine
Marke gesprungen werden kann, und das über einen Rücksprung verlassen
wird.
Beim Springen in ein Unterprogramm sind folgende Daten zu übergeben (d.h.
abzuspeichern, so dass das Unterprogramm darauf Zugriff hat):
• Rücksprungadresse
• Parameter
Beim Rücksprung sind unter Umständen gewisse Resultate aus dem Unterprogramm
an das aufrufende Programm zu übergeben. Techniken zur Übergabe der Daten:
• Abspeichern in bestimmten Zellen im Unter- oder Hauptprogramm
• Abspeichern in Registern
• Abspeichern im Stack
Wichtig: Einheitliche, für das ganze Programm gültige Regeln für die Übergabe der
Parameter und Rücksprungadresse festlegen.
Geschlossenes Unterprogramm: Ansprung erfolgt über Anfangsadresse, Rücksprung
über die vor Ansprung angegebene Adresse.
Rekursives Unterprogramm: Aus dem Unterprogramm wird die Anfangsadresse angesprungen. Dann muss die Rückkehradrese im Stack verwaltet werden.
Beim offenen Einbau eines Unterprogramms (insbesondere beim Einsatz von Makroexpansion) kann Rekursion nicht behandelt werden. Die MI sieht spezielle Befehle für Unterprogrammaufrufe vor, die jedoch auch durch Befehle ersetzt werden
können, die den Stack manipulieren und durch Sprungbefehle:
• CALL speichert den aktuellen Stand des Befehlszählers (Register PC) im Stack
ab und setzt den Befehlszähler auf die angegebene Adresse.
• RET bewirkt die Rückkehr an die Adresse hinter der Aufrufstelle. Diese wird
aus dem Stack in den PC geladen und der Stack wird zurückgesetzt.
Beispiel: Aufruf eines Unterprogramms zur Berechnung der Fakultät
MOVE W ..., R0 (Argument in R0 ablegen)
CALL fac (Unterprogramm aufrufen)
MOVE W R2, ... (Resultat abspeicher)
Wichtig: Durch Unterprogrammaufrufe werden Teile des Speichers und der Register
verändert. Für die Nutzung eines Unterprogramms ist es wichtig zu wissen:
• wie Parameter / Resultat übergeben werden
• welche Teile des Speichers / der Register durch das Unterprogramm verändert
werden
Bei einer strukturierten Programmierung auf Maschinenebene kommt der systematischen Nutzung von Unterprogrammen eine entscheidende Bedeutung zu. Unterprogramme sind sorgfältig zu dokumentieren.
152
KAPITEL 4. MASCHINENNAHE PROGRAMMIERUNG
Im allgemeinen Fall arbeitet das Hauptprogramm mit allen Registern. Das gleiche
gilt für das Unterprogramm. Deshalb ist es naheliegend, vor dem Aufruf des Unterprogramms alle Register zu retten (im Speicher zwischenzuspeichern). Dann
kann das Unterprogramm alle Register frei nutzen. Nach Rücksprung werden die
alten Registerwerte restauriert. Bei der MI gibt es einen speziellen Befehl zum Retten der Register im Stack:
PUSHR bewirkt ein Abspeichern der Register R14 bis R0 auf dem Stack entsprechend
der folgenden Befehlssequenz:
1
2
4
SUB W I4, R14
MOVE W R14, !R14
SUB W I4, R14
MOVE W R13, !R14
...
Der Stackpointer wird insgesamt um 60 dekrementiert und die Registerinhalte werden im Stack gespeichert.
Der Befehl POPR bewirkt das Restaurieren der Register aus dem Stack. Der Stackpointer wird um 60 erhöht.
Ratsam ist es, bei der Verwendung von Unterprogrammen eine einheitliche Standardschnittstelle zu nutzen. Prinzipien:
• Im aufrufenden Programm werden die Parameter des Unterprogramms in der
Reihenfolge pn , . . . , p1 auf dem Stack abgelegt. Für ein Ergebnis wird ebenfalls Speicherplatz im Stack reserviert. Dann erfolgt der Aufruf des Unterprogramms. Die Rückkehradresse steht im Stack.
• Im Unterprogramm werden die Registerwerte gesichert. Der nun erreichte
Stackpointer-Stand wird in R13 abgelegt. R13 dient also als Basisadresse für
den Stackpointer-Stand zur Aufrufzeit. In R12 tragen wir die Basisadresse der
Parameter ein. Bei der Rückkehr aus dem Unterprogramm können wir diese
Basisadresse verwenden, um den alten Stack-Pegel wiederherzustellen.
• Nach der Ausführung des Unterprogramms wird über POPR der Registerzustand wiederhergestellt und über RET erfolgt der Rücksprung.
• Nach Rückkehr stehen die Parameter und das Resultat im Stack. Nach Zurücksetzen des Stacks um die Parameter steht das Resultat zu Beginn des Stacks
und kann abgespeichert werden.
Also:
1
2
4
6
...
MOVE kn pn, -!SP
...
MOVE k1 p1, -!SP
CALL up
ADD W I..., SP
...
8
10
12
14
up: PUSHR
MOVE W SP, R13
MOVEA 64+!R13, R12
...
MOVE W R13, SP
POPR
RET
4.2. ADRESSIERTECHNIKEN UND SPEICHERVERWALTUNG
153
Beispiel: ggt
1
2
MOVE W I0, -!SP
MOVE W ..., -!SP
MOVE W ..., -!SP
CALL ggt
ADD W I8, SP
MOVE W !SP+, ...
4
6
8
10
12
14
16
18
20
22
24
26
ggt:
PUSHR
MOVE W SP, R13
MOVEA 64+!R13, R12
CMP W 4+!R12, !R12
JNE else
then:
MOVE W !R12, 8+!R12
JUMP rueck
else:
JLT op1lop2
SUB W 4+!R12, !R12
JUMP reccall
op1lop2: SUB W !R12, 4+!R12
reccall: MOVE W I0, -!SP
MOVE W !R12, -!SP
MOVE W 4+!R12, -!SP
CALL ggt
ADD W I8, SP
MOVE W !SP+, 8+!R12
rueck:
MOVE W R13, SP
POPR
RET
4.2
Adressiertechniken und Speicherverwaltung
In höheren Programmiersprachen werden Datenstrukturen zur strukturierten Darstellung von Informationen verwendet:
• Grunddatentypen
• Enumerationssorten
• Records
• Union
• Array
• rekursive Datensorte
• Referenzen
Im Folgenden interessiert uns die Frage, wie wir diese Datenstrukturen auf Maschinenebene darstellen und darauf zugreifen können.
4.2.1
Konstante
Eine Konstante entspricht einer Operandenspezifikation, die einen Wert repräsentiert, ohne dabei auf Register oder den Speicher Bezug zu nemen. In der MI schreiben wir z.B. I 2735. Zur Ausführungszeit ist der Wert 2735 in Binärdarstellung im
154
KAPITEL 4. MASCHINENNAHE PROGRAMMIERUNG
Hauptspeicher abgelegt. Zur Ausführungszeit des entsprechenden Befehls zeigt der
Befehlszähler in der Adressrechnung auf die Adresse im Hauptspeicher, unter der
der Wert abgelegt ist.
4.2.2
Operandenversorgung über Register
Bei der Operandenversorgung über Register sind die Zugriffszeiten in der Regel
geringer als beim Zugriff auf den Hauptspeicher. In der MI: Rn, 0 ≤ n ≤ 15.
4.2.3
Absolute Adressierung
Im Befehl wird die Adresse als Wert angegeben. Dies bedeutet, dass das Programm
bzw. seine Daten im Hauptspeicher fixiert sind, d.h. die Lage im Hauptspeicher
festgelegt ist. Dies bedeutet eine Inflexibilität, die zu vermeiden ist. Absolute Adressierung macht höchstens für eine Reihe von Systemprogrammen Sinn.
4.2.4
Relative Adressierung
Idee: Wir adressieren relativ zu einer frei festlegbaren Anfangsadresse für
• den Programmcode
• die Daten
Ein Maschinenprogramm heißt relativ adressiert, wenn es keine Adressteile bzw.
zur Verwendung als Adressteile vorgesehene Operandenteile enthält, die auf eine
absolute Lage des Programms oder seiner Daten im Speicher Bezug nehmen. Nur
dann können wir das Programm und seine Daten an beliebigen Stellen im Speicher
ansiedeln.
Für die Methodik wichtige Frage: Ist die Tatsache, dass ein Maschinenprogramm
relativ adressiert ist, einfach zu erkennen und zu überprüfen (Codierstandards)?
In der MI ist natürlich syntaktisch erkennbar, ob ausschließlich mit relativer Adressierung in der Operandenspezifikation gearbeitet wird. Dies garantiert allerdings
noch nicht, dass das Programm relativ adressiert ist. (Achtung: Unterschied Syntax
/ Semantik!)
4.2.5
Indizierung, Zugriff auf Felder
In höheren Programmiersprachen verwenden wir Felder ([n:m] array m a). Wir
behandeln nun die Frage, wie solche Felder in einer Maschine (im Hauptspeicher)
abgelegt werden und wie systematisch darauf zugegriffen wird. Wir legen die Elemente eines Felds konsekutiv im Speicher ab. Um den Zugriff zu organisieren, verwenden wir folgende Darstellung:
Adresse
Inhalt
4711
4711 + 1 − n fiktive Anfangsadresse a0 für a[0]
4712
a[n]
4713
a[n+1]
..
..
.
.
4711+m
a[m]
Für den Index k, n ≤ k ≤ m, erhalten wir durch k + a0 die Adresse des Feldelements. Sei α die Anfangsadresse des Felds. Dann schreiben wir in die Speicherzelle
die fiktive Adresse α + 1 − n.
Vorteil: Wenig Speicher für Verwaltungsinformationen zum Feld, uniformer Zugriff.
Nachteil: keine vollständigen Informationen zur Überprüfung, ob der Index außerhalb der Indexgrenzen liegt.
4.2. ADRESSIERTECHNIKEN UND SPEICHERVERWALTUNG
155
In der MI: Enthält R0 die fiktive Anfangsadresse des Felds und R1 den Index k, so
wird auf a[k] durch !R0/R1/ zugegriffen. Enthält R0 hingegen die effektive Anfangsadresse des Felds, so erfolgt der Zugriff auf das k-te Feldelement durch !!R0/R1/.
Diese Zugriffstechnik ist problemlos mit der relativen Adressierung kombinierbar.
Die Zugriffstechnik setzt voraus, dass zur Ausführungszeit beim Anlegen des
Felds (auf dem Stack) die Feldgrenzen festliegen und sich nicht mehr ändern.
Mehrstufige Felder können nach dem selben Prinzip behandelt werden: Sei [n1:m1,
..., ni:mi] array m a mit nq ≤ mq für 1 ≤ q ≤ i gegeben. Wir verwenden folgendes Schema:
Adresse
Inhalt
4711
a0
fiktive Adresse von a[0, ..., 0]
4712
s1
Spanne m1 − n1
..
..
..
.
.
.
4711+i-1
si−1
4711+i
a[n1, ..., ni]
Fiktive Anfangsadresse:
Spanne mi−1 − ni−1
erstes Feldelement
a0 = α + i − (n1 + s1 (n2 + s2 (. . . )))
Adresse des Elements a[k1, ..., ki] (mit nq ≤ kq ≤ mk für 1 ≤ q ≤ i):
a0 + k1 + s1 (k2 + s2 (. . . ))
Beispiel:
1
2
4
Einfacher Algorithmus auf Feldern (alle Feldelemente addieren)
v := 0;
for i := 1 to n do
v := v + a[i]
od
Werte in der MI:
R0 Anfangsadresse der Feldelemente (zeigt auf das erste Feldelement a[1])
R1 Zähler
R2 Abbruchswert
R3 Aufnahme des Resultats
1
2
4
6
8
MOVE W I n, R2
CLEAR W R3
CLEAR W R1
for: CMP W R2, R1
JLT ende
ADD W !R0/R1/, R3
ADD W I 1, R1
JUMP for
ende: HALT
4.2.6
Symbolische Adressierung
Auch die relative Adressierung ist noch sehr fehleranfällig, da der Programmierer
in Registern und Zahlen sein Programm verstehen muss. Bequemer und weniger
fehleranfällig ist das Arbeiten in Programmen mit frei gewählten Bezeichnungen. Bei
Assemblerprogrammen sprechen wir von symbolischen Adressen. Bei frei gewählen
Bezeichnungen sind folgende Prinzipien zu beachten:
•
sprechende Bezeichnungen
156
KAPITEL 4. MASCHINENNAHE PROGRAMMIERUNG
•
data dictionary anlegen: ein Verzeichnis aller Begriffe und deren Bedeutung
• Codierungsstandards für die Wahl von Bezeichnungen
Vor der Ausführung des Programms werden die Bezeichnungen vom Assemblierer
durch Adressen ersetzt. Dazu benutzt man eine Datenstruktur Adressbuch, in
der die Beziehung Bezeichnung – relative Adresse festgelegt ist. Dabei ist darauf zu
achten, dass diese Zuordnung so gewählt ist, dass das Programm effizient ist:
• wenig Speicher verbraucht
• kurze Ausführungszeiten hat
Dazu werden Programme gezielt optimiert (unter Einsatz von Pipelining und Caching).
4.2.7
Geflechtsstrukturen und indirekte Adressierung
Bei problemorientierten Programmiersprachen findet sich das Konzept Referenz /
Verweis / Zeiger / Pointer. Auf der MI-Ebene bilden wir Referenzen auf Adressen
ab. Die Referenz auf einen Wert oder eine Variable wird zu der Adresse der Speicherzelle, die diesen Wert enthält oder diese Variable repräsentiet. Bei umfangreichen
Datenpaketen / -strukturen (beispielsweise Feldern) entsprechen die Referenzen den
Anfangsadressen des Datenpakets.
Referenzen entsprechen den Konzept der indirekten Adressierung. Referenzen /
Verweise bieten große Vorteile für die Effizienz von Programmen:
• Statt großer Datenpakete können wir in Prozedur- / Unterprogrammaufrufen
die Verweise / Anfangsadresse übergeben.
• In Geflechtsstrukturen können wir gewisse Daten, die für eine Reihe von Programmelementen bedeutsam sind, einheitlich verwalten (data sharing).
Beispiel: Eine Typvereinbarung für Pointer / Records, aus der wir verkettete
Listen (aber auch Bäume) aufbauen können, lautet wie folgt:
1
2
4
6
type
rlist = ^slist;
list = record
inhalt: s;
naechster, letzter: rlist
end;
Die Abbildung auf den Speicher (MI) könnte - unter der Annahme, dass der Typ s
der Kennung W entspricht - so aussehen, dass ein Knoten aus 3 * 4 Bytes besteht,
von denen je ein 4-Byte-Block die Adresse naechster, letzter und den abgelegten Wert enthält.
Wir nehmen an, dass durch die Listenelemente eine zweifach verkettete Liste aufgebaut wird, wobei im ersten Element der Verweis letzter nil ist und im letzten
Listenelement der Verweis naechster nil ist.
Durchlaufen der Liste:
1
2
4
zeiger := listenanfang;
while zeiger <> nil do
if zeiger^.inhalt = suchmuster then
goto gefunden
fi;
4.2. ADRESSIERTECHNIKEN UND SPEICHERVERWALTUNG
6
8
zeiger := zeiger^.naechster
od
goto nicht_gefunden
R0
Darstellung in der MI: R1
R2
1
2
4
6
Zeiger
Listenanfang
Suchmusteradresse
MOVE W R1, R0
loop: CMP W I 0, R0
JEQ nicht_gefunden
CMP W !R2, 8+!R0
JEQ gefunden
MOVE W !R0, R0
JUMP loop
Programm für das Anhängen eines Listenelements am Ende der Liste:
1
2
4
6
8
10
12
14
if listenanfang <> nil then
zeiger := listenanfang;
while zeiger^.naechster <> nil do
zeiger := zeiger^.naechster
od;
zeiger^.naechster := neu;
neu^.letzter := zeiger;
neu^.inhalt := ...;
neu^.naechster := nil
else
listenanfang := neu;
neu^.letzter := zeiger;
neu^.inhalt := ...;
neu^.naechster := nil
fi
R0
R2
R3
R4
1
2
4
6
8
10
12
14
16
Zeiger
Listenanfang
neu
inhalt
MOVE W R2, R0
CMP W I 0, R0
JNE loop
CLEAR W !R3
CLEAR W 4+!R3
MOVE W R4, 8+!R3
MOVE W R3, R2
JUMP ende
loop:
CMP W I 0, !R0
JEQ listenende
MOVE W !R0, R0
JUMP loop
listenende: MOVE W R3, !R0
CLEAR W !!R0
MOVE W R0, 4+!!R0
MOVE W R4, 8+!!R0
157
158
4.2.8
KAPITEL 4. MASCHINENNAHE PROGRAMMIERUNG
Speicherverwaltung
Für die Ausführung eines Maschinenprogramms mit komplexen Daten ist eine Speicherorganisation erforderlich. Darunter verstehen wir die Systematik der Ablage von
Programm und Daten im Speicher. Ziele dabei sind:
• effiziente Nutzung des Speichers
• einfache, gut handhabbare Strukturen
Beobachtungen:
• Praktisch alle Programme benötigen während ihrer Ausführung zusätzlichen
Speicherplatz, der stückweise belegt und wieder freigegeben wird.
• Oft werden Speicherplätze in der umgekehrten Reihenfolge, in der sie belegt
werden, wieder freigegeben (Stack-Prinzip) - der zuletztbelegte Platz wird
zuerst wieder freigegeben.
• Für gewisse Datenstrukturen eignet sich das Stack-Prinzip nicht (Zeiger- /
Referenzstrukturen).
Damit nutzen wir drei Arten der Verwaltung von Speicher:
• statischer Speicher: Hier werden alle Daten abgelegt, die über die gesamte
Laufzeit des Programms benötigt werden.
• Stack: Hier werden alle Daten abgelegt, die nach dem Stack-Prinzip benötigt
und freigegeben werden. Bei der Blockstruktur wird beim Betreten eines
Blocks der Speicher für die lokalen Variablen (bei Prozeduraufrufen für Parameter) bereitgestellt und beim Verlassen des Blocks wieder freigegeben.
• Listenspeicher (Heap): Hier werden alle Daten abgelegt, deren Speicherplätze
nicht im Stack verwaltet werden können. Dabei wird im Quellprogramm in der
Regel Speicherplatz explizit angefordert (Kreieren von Objekten, new/allocate
zum Bereitstellen von Speicher für Zeiger). Die Freigabe von Speicher erfolgt:
– entweder explizit (deallocate)
Vorteil: Der Programmierer kann die Speicherverwaltung kontrollieren.
Nachteil: Erhöhte Komplexität der Programme, Gefahr ins Leere zeigender Referenzen (dangling references).
– oder implizit durch Speicherbereinigung (garbage collection). Dabei
wird durch Erreichbarkeitsanalyse festgestellt, auf welche Zeigerelemente / Objekte noch zugegriffen werden kann; die nicht mehr erreichbaren
werden entfernt.
Vorteil: erheblich einfacher, keine Fehlergefahr durch Zeiger ins Leere.
Nachteil: weniger effizient, Echtzeitverhalten der Programme kaum vorhersagbar.
4.2.9
Stackverwaltung von blockstrukturierten Sprachen
Eine Programmiersprache heißt blockstrukturiert, wenn lokale Variablen /
Konstanten verwendet werden, die in abgegrenzten Bindungsbereichen deklariert
werden, soweit keine Referenzen / Zeiger auftreten. Für lokale Bezeichner / Variablen unterscheiden wir:
• Deklaration / Bindung (genau ein Mal)
• benutzendes Auftreten
4.2. ADRESSIERTECHNIKEN UND SPEICHERVERWALTUNG
159
Da die gleiche Bezeichnung in unterschiedlichen Bindungsbereichen gebunden werden kann, ist die Zuordnung zwischen Bindung und Nutzung wichtig (vgl. statische gegen dynamische Bindung). Blockstrukturen entsprechen Klammerstrukturen.
Durch Durchnummerieren der Blockklammern kann jeder Block eindeutig gekennzeichnet werden. Durch dieses Verfahren können wir Blöcke eindeutig kennzeichnen.
Blockschachtelungstiefe (BST): Anzahl der einen Block umfassenden Blöcke + 1.
Die Blöcke definieren einen Baum.
Beispiel:
1
2
4
6
8
10
Die Blockstruktur
begin
begin
begin
end
begin
end
end
begin
end
end
entspricht dem Baum
1
1.1
1.2
1.1.1 1.1.2
In diesem Fall bestimmt die statische Struktur der Blöcke die Struktur des Stacks.
Beispiel:
1
2
4
Blockorientiertes Programm mit Prozeduraufruf
| // 1
proc p =:
| // 1.1
...
| // 1.1
6
8
| // 1.2
...
| // 1.2
10
12
14
16
| // 1.3
...
p;
...
| // 1.3
| // 1
Hier unterscheiden wir für jeden Abschnitt im Stack (der den lokalen Variablen
eines Blocks entspricht) den statischen Vorgänger (in der Aufschreibung der nächste
umfassende Block) und den dynamischen Vorgänger (in der Ausführung der nächste
umfassende Block) (s. Abb 4.1).
In der Speicherorganisation für blockorientierte Sprachen verwenden wir einen
Stack, in dem jeweils auch der Verweis auf den statischen und dynamischen Vorgänger sowie die Blockschachtelungstiefe abgelegt wird. Ein Prozeduraufruf entspricht
160
KAPITEL 4. MASCHINENNAHE PROGRAMMIERUNG
1.1
1
1
...
1.3
1.3
1.3
1
1
1
1
Abbildung 4.1: Blöcke auf dem Stack
imer dem Betreten eines Blocks.
Jeder Speicherabschnitt auf dem Stack enthält schematisch folgende Informationen:
• statischer Vorgänger
• dynamischer Vorgänger
• Blockschachtelungstiefe
• Rückkehradresse (bei Prozeduraufrufen)
• Parameter (bei Prozeduraufrufen)
• Resultat (bei Prozeduraufrufen)
• lokale Variablen
4.3
Techniken maschinennaher Programmierung
Ein komplexes maschinennahes Programm kann kaum in einem Anlauf gleich als
Maschinenprogramm geschrieben werden. Geschickter ist es, zunächst ein problemorientiertes Programm zu schreiben, dies zu analysieren und zu verifizieren und
dann in ein Maschinenprogramm umzusetzen. Die Umsetzung erfolgt
• entweder per Hand
• oder durch ein Übersetzungsprogramm (Compiler)
Typische Bestandteile höherer Programmiersprachen:
• Datenstrukturen
• Blockstrukturen
• Ablaufstrukturen
– Ausdrücke
– Zuweisungen
– if - then - else
– Wiederholungsanweisungen
– Funktionen, Prozeduren
Maschinennahe Umsetzung:
• Speicherung
• Umsetzung in Konzepte maschinennaher Sprachen (s.u.)
4.3. TECHNIKEN MASCHINENNAHER PROGRAMMIERUNG
4.3.1
161
Auswertung von Ausdrücken / Termen
Ein Ausdruck besteht aus einem Gebirge von Funktionsaufrufen.
abs(ab − cd) + ((e − f )(g − j))
+
abs
-
*
-
-
* efgj
*
a bcd
Zugehöriges Single-Assignment-Programm:
1
2
4
6
8
h1
h2
h3
h4
h5
h6
h7
h8
:=
:=
:=
:=
:=
:=
:=
:=
a * b;
c * d;
h1 - h2;
abs (h3);
e - f;
g - j;
h5 * h6;
h4 + h7;
:=
:=
:=
:=
:=
:=
:=
:=
a * b;
c * d;
h1 - h2;
abs (h1);
e - f;
g - j;
h2 * h3;
h1 + h2;
oder:
1
2
4
6
8
h1
h2
h1
h1
h2
h3
h2
h1
Zur Optimierung des Speicherbedarfs beim Auswerten von Ausdrücken verwenden
wir statt Hilfsvariablen einen Stack. Operationen finden immer auf dem ersten Element des Stacks statt. Deshalb kann das erste Element auch in einer speziellen
Hilfsvariable AC gehalten werden.
Operationen:
• lege
Wert auf den Stack
• führe ein- oder zweistellige Operation auf dem Stack aus
Das Arbeiten auf dem Stack zur Auswertung eines Ausdrucks kann einfach durch
die Postfix-Darstellung des Operatorbaums erreicht werden.
im Beispiel:
1
2
4
a b * c d * - abs e f - g j - * +
AC := a
push AC; AC := b
AC := top * AC; pop
push AC; AC := c
162
6
8
KAPITEL 4. MASCHINENNAHE PROGRAMMIERUNG
push AC; AD := d
AC := top * AC; pop
AC := top - AC; pop
AC := abs (AC)
...
In der MI können wir ohne Verwendung eines Registers direkt auf dem Stack arbeiten:
1
2
MOVE W a, -!SP
MULT W !SP+, !SP
...
Wir schreiben nun ein Programm, das einem kleinen Ausschnitt aus einem Compiler
entspricht. Es nimmt die Umsetzung von Ausdrücken in eine Sequenz von Operationen auf dem Stack vor. (Das erzeugte Programm legt schließlich das Ergebnis in
AC ab, der Stack ist nach Ausführung des Programms wieder im Anfangszustand.)
Wir benötigen eine Datenstruktur zur Darstellung von Operatorbäumen.
1
2
sort expr = nullary (symbol sym)
| mono (op op, expr e0)
| duo (expr e1, op op, expr e2)
Dabei nehmen wir an, dass die Sorten symbol (Menge der Identifikatoren für Werte)
und op (Operationssymbole) gegeben sind.
Im Folgenden wird ein Programm angegeben, das einen Operatorbaum als Eingang nimmt und ein Stackprogramm zur Auswertung des durch den Operatorbaum
gegebenen Ausdrucks ausdruckt:
1
2
4
6
8
10
12
proc eaf = (expr e):
if e in nullary then
print ("AC := ", sym(e))
elif e in mono then
eaf (e0(e));
print ("AC := ", op(e), "AC")
else
eaf (e1(e));
print ("push AC");
eaf (e2(e));
print ("AC := top ", op(e), "AC");
print ("pop")
fi
4.3.2
Maschinennahe Realisierung von Ablaufstrukturen
Eine Zuweisung
1
x := E
wird umgesetzt in (sei e der Operatorbaum zu E):
1
2
eaf (e);
x := AC
Die bedingte Anweisung
1
if E = 0 then S1 else S2 fi
wird wie folgt umgesetzt (sei e der Operatorbaum zu E):
4.3. TECHNIKEN MASCHINENNAHER PROGRAMMIERUNG
1
2
4
6
8
t:
10
12
163
eaf (E);
if AC = 0 then
goto t
fi
.....
(Maschinencode fuer S2)
.....
goto ende
.....
(Maschinencode fuer S1)
.....
ende:
Die Wiederholungsanweisung
1
while E <> 0 do S od
wird zu:
1
2
4
6
8
while: eaf (e);
if AC = 0 then
goto ende
fi
....
(Maschinencode fuer S)
....
goto while
ende:
Im Folgenden schreiben wir ein Programm, das die Umsetzung (Übersetzung) eines
while / if - Programms in ein maschinennahes Programm vornimmt. Wir benötigen
eine Sorte zur Repräsentation von Programmen:
1
2
4
sort statement = assign (symbol x, expr e)
| if (expr cond, statement st, statement se)
| while (expr e, statement s)
| seq (statement s1, statement s2)
Im Übersetzungsvorgang werden zusätzlich Marken für Sprünge benötigt. Wir nehmen an, dass wir zu einer gegebenen Marke m in der Sorte label Über eine Funktion
1
fct next = (label) label
verfügen, so dass m, next(m), next(next(m)), . . . eine Folge unterschiedlicher Marken liefert (Generator für Marken).
Programm für das Erzeugen eines maschinennahen Programms:
1
2
4
6
8
10
proc gen = (statement z, var label m):
if z in assign then
eaf (e(z));
print (x(z), " := AC")
elif z in seq then
gen (s1(z), m);
gen (s2(z), m)
elif z in if then
label me := m;
label mt := next (m);
164
12
14
16
18
20
22
24
26
28
KAPITEL 4. MASCHINENNAHE PROGRAMMIERUNG
m := next (mt);
eaf (cont(z));
print ("if AC = 0 then goto ", mt, " fi");
gen (se(z), m);
print ("goto ", me);
print (mt, ":");
gen (st(z), m);
print (me, ":")
else
label mw := m;
label ma := next (m);
m := next (ma);
print (mw, ":");
eaf (c(z));
print ("if AC = 0 then goto ", ma, "fi");
gen (s(z), m);
print ("goto ", mw);
print (ma, ":")
fi
Teil III
Systeme und systemnahe
Programmierung
165
167
Hintergrund:
• Systemprogramme (Betriebssysteme)
• Telekommunikation
• Rechnernetze
• eingebettete Systeme (∼ Software zur Steuerung technischer Systeme)
Weiteres Anliegen:
• Beschreibung von allgemeinen diskreten (digitalen) Systemen (Bsp.: Geschäftsprozesse)
168
Kapitel 1
Prozesse, Interaktion,
Koordination in verteilten
Systemen
Wir behandeln verteilte, parallel ablaufende, interaktive (diskrete) Systeme.
Definition: System
Unter einem System verstehen wir eine von ihrer Umgebung abgegrenzte Anordnung von aufeinander einwirkenden Komponenten.
Wichtige Begriffe:
Systeme: führen Aktionen aus
Parallelität: Aktionen finden möglicherweise nebeneinander (gleichzeitig) statt (Nebenläufigkeit)
Prozess / Ablauf: Die Anordnung der Aktionen im Ablauf eines Systems und ihre
kausalen Abhängigkeiten
Interaktion / Kommunikation: (Koordination, Synchronisation etc.)
Zustand: Eine Sicht auf Systeme ist die Zuordnung eines Zustands und von Zustandsübergängen
Struktur / Verteilung: Aufgliederung eines Systems in Komponenten (Subsysteme)
Zeit: Oftmals als diskrete Zeit verstanden
Schnittstellen / Interfaces: Markieren die Interaktion (Wirkung) an Systemgrenzen
(am Übergang von zwei oder mehreren Systemen)
1.1
Prozesse
Wir betrachten diskrete Prozesse, aufgebaut aus Aktionen, die in kausalen Beziehungen stehen. Etwas enger wird der Begriff Prozess in der Systemprogrammierung
verwendet. Dort ist ein Prozess der Vorgang eines Programms in Ausführung.
1.1.1
Aktionsstrukturen als Prozesse
Ein Prozess besteht aus einer Reihe von Aktionen.
169
170
KAPITEL 1. PROZESSE, INTERAKTION, KOORDINATION . . .
off_hook(3)
off_hook(2)
free(3)
free(2)
dial(3, 2)
busy(3)
on_hook(3)
ring(3)
off_hook(3)
connected(3)
busy(3)
dial(2, 3)
ringtone(2)
connected(2)
on_hook(2)
on_hook(3)
Abbildung 1.1: Aktionsstruktur
Beispiel: Telefonanlage
• Telefone: t ∈ {1, 2, . . . }
• Aktionen:
off hook(t)
on hook(t)
dial(t, t’)
ring(t)
stopRing(t)
free(t)
busy(t)
ringtone(t)
connected(t)
abheben des Hörers an Telefon t
auflegen
wählen der Nummer t0 an t
läuten
aufhören zu läuten
Freizeichen
Partner besetzt
beim Partner klingelts
Telefon ist verbunden
Aktionsstruktur (Abb. 1.1): Definiert einen Ablauf im Telefonsystem
Ein Prozess ist ein gerichteter azyklischer Graph, dessen Knoten mit Aktionen
markiert sind (Aktionsgraphen). Typischerweise kommen in Prozessen gewisse
Aktionen mehrfach vor. Deshalb verwenden wir den Begriff des Ereignisses. Ein
Ereignis ist ein innerhalb eines Prozesses einmaliger Vorgang, der der Ausführung
(Instanz) einer Aktion entspricht. Ein Prozess entspricht einer Ablaufstruktur p0 =
(E0 , ≤0 , α0 ), wobei:
E0 Menge von Ereignissen
≤0 partielle Ordnung auf E0
α0 Abbildung E0 → A, wobei A eine Menge von Aktionen ist
Wir nennen zwei Ereignisse e0 , e1 ∈ E0 in p0 parallel bzw. kausal unabhängig, wenn
gilt
¬(e0 ≤0 e1 ) ∧ ¬(e1 ≤0 e0 )
Anderenfalls nennen wir die Ereignisse zueinander sequentiell.
Sequentieller Prozess: Aktionsstruktur, in der alle paarweisen Ereignisse sequentiell sind.
Beobachtung: Programme mit vorgegebener Eingabe definieren Prozesse.
Aktionen ∼ atomare Befehle, Anweisungen
1.1. PROZESSE
171
Ereignisse ∼ Instanz: Ausführung eines Befehls
Sequentielle Programme entsprechen sequentiellen Prozessen.
Forderung: Jeder Prozess ist endlich fundiert, d.h. zu jedem Ereignis ist die
Menge der Vorgänger endlich.
Deutungsmöglichkeiten der Relation ≤:
• echt kausale Deutung (Wirkursache)
• zeitliche Reihenfolge
• nicht-parallele Ereignisse
1.1.2
Strukturierung von Prozessen
Ein Prozess p1 = (E1 , ≤1 , α1 ) heißt Teilprozess von p2 = (E2 , ≤2 , α2 ), wenn:
E1 ⊆ E 2
≤1 =≤2 |E1 ×E1
α1 = α2 |E1
p1 heißt Präfix von p2 , wenn darüber hinaus gilt
∀e ∈ E2 , d ∈ E1 : e ≤2 d ⇒ e ∈ E1
Wir schreiben dann p1 v p2 (ist partielle Ordnung).
Lemma: Für jeden endlich fundierten Prozess p1 = (E1 , ≤1 , α1 ) gilt: Die Menge
seiner endlichen Präfixe M = {p : p v p1 ∧ p endlich} bestimmt den Prozess p1
eindeutig. Insbesondere gilt p1 = sup M .
Beweis: Sei p2 = (E2 , ≤2 , α2 ) ebenfalls obere Schranke von M . Dann gilt E1 ⊆ E2 ,
denn
[
E1 =
{E0 : (E0 , ≤0 , α0 ) ∈ M }
Da ≤1 und α1 auf E1 mit ≤2 und α2 übereinstimmen, gilt p2 v p1 .
Sequentialisierung von Prozessen: p1 heißt Sequentialisierung von p2 , falls
gilt:
E1 = E2
∀e, d ∈ E1 : e ≤2 d ⇒ e ≤1 d
α1 = α2
Eine Sequentialisierung heißt vollständig, wenn p1 sequentiell ist, d.h. wenn E1
hinsichtlich ≤1 eine Kette bildet.
Satz: Jeder endliche fundierte Prozess p0 ist duch die Menge seiner vollständigen
Sequentialisierungen eindeutig bestimmt.
Ein Prozess p1 heißt Verfeinerung von p0 , wenn es eine injektive Abbildung
γ : E1 → E0 gibt mit
∀e, d ∈ E1 : γ(e) 6= γ(d) ⇒ e ≤1 d ⇒ γ(e) ≤0 γ(d)
In der Praxis treten Prozesse auf, die sehr umfangreich und unübersichtlich sind.
Beispiel:
• Workflow in betriebswirtschaftlichen Anwendungen
• Rechenvorgänge in Rechenanlagen
172
KAPITEL 1. PROZESSE, INTERAKTION, KOORDINATION . . .
• Verkehrsgeschehen
• Datenübertragungen (Telekommunikation)
Ein Ziel der Prozessmodellierung ist eine übersichtliche und strukturierte Darstellung. Eine Methode: Vereinfachung und Abstraktion.
• nur sequentielle Prozesse betrachten
• nur endliche Prozesse betrachten
• Prozesse als komponiert aus Teilprozessen betrachten
1.1.3
Sequentielle Prozesse und Spuren
Sequentieller Prozess: Kausalordnung ist lineare Ordnung.
Sequentielle Prozesse lassen sich durch endliche und unendliche Sequenzen von Aktionen darstellen - die explizite Betrachtung von Ereignismengen erübrigt sich. Wir
sprechen von Spuren oder Strömen von Aktionen: Sei A Aktionsmenge; die Menge
der Ströme über A ist definiert durch
Aω = A∗ ∪ A∞
A∞ entspricht der Menge der Sequenzen der Form ha1 a2 a3 . . . i, genauer: der Abbildung a : N \ {0} → A. Durch Aω können wir jeden sequentiellen Prozess als Spur
darstellen. Wir können die Konkatenation auf Aω erweitern:
ha1 . . . an i ◦ hb1 . . . i = ha1 . . . an b1 . . . i
ha1 . . . i ◦ hb1 . . . i = ha1 . . . i
Präfixordnung v auf Aω :
s1 v s2 ⇔ ∃s3 ∈ Aω : s1 ◦ s3 = s2
Für einen sequentiellen Prozess
a1 → a2 → a3 → . . .
ist die Spur einfach zu definieren:
spur(p) = ha1 a2 a3 . . . i
Für einen nicht sequentiellen Prozess definieren wir die Menge seiner Spuren:
spuren(p) := {spur(p0 ) : p0 ist vollständige Sequentialisierung von p}
Achtung:
• Durch den Übergang von Prozessen zu Spuren verlieren wir Informationen
über die Nebenläufigkeit.
• Systeme können wir durch die Menge ihrer Prozesse P definieren. Die Menge
der Spuren von P ist definiert durch
[
Spuren(P ) :=
spuren(p)
p∈P
Eine andere Vereinfachung erhalten wir, wenn wir nur endliche Prozesse betrachten.
Sei P eine Menge von Prozessen (Beschreibung eines Systems). Die Menge der
endlichen Anfangsprozesse ist definiert durch
[
Mea(P ) =
{p0 : p0 v p ∧ p0 endlich}
p∈P
Beide Vereinfachungen lassen sich kombinieren:
Spuren(Mea(P ))
Menge der Spuren der endlichen Anfangsprozesse
1.1. PROZESSE
173
send
recieve
send
recieve
send
recieve
Abbildung 1.2: Prozess
Beispiel: (s. Abb. 1.2)
Spur: < send send send recieve recieve . . . >
Achtung: Durch die Menge Spuren(Mea(P )) können wir insbesondere nicht charakterisieren, welche Aktionen unendlich oft auftreten müssen (zumindest in bestimmten Fällen). Dies führt auf den Begriff der Fairness:
Eine Spur s ∈ Aω heißt fair für eine Menge von Prozessen P , wenn es einen Prozess
p ∈ P gibt mit
spur(p0 ) = s ∧ p0 ist Sequentialisierung von p
Die Spur s ∈ Aω heißt unfair, wenn es einen unendlichen Prozess p ∈ P und ein
unendliches Anfangsstück p0 v p gibt, so dass s ∈ spuren(p0 ) und s nicht fair für P
ist.
1.1.4
Zerlegen von Prozessen in Teilprozesse
Wir wollen Prozesse strukturieren, indem wir sie in zusammengehörige Teilprozesse
zerlegen oder umgekehrt aus Teilprozessen zusammensetzen. Wir betrachten Porzesse
pi = (Ei , ≤i , αi )
i = 0, 1, 2
Wir sagen, p0 ist sequentiell zusammengesetzt aus p1 gefolgt von p2 , falls gilt:
∧
∧
∧
∧
∧
E0 = E1 ∪ E2
∀e1 ∈ E1 ∀e2 ∈ E2 : e1 ≤0 e2
α0 |E1 = α1
α0 |E2 = α2
≤0 |E1 ×E1 =≤1
≤0 |E2 ×E2 =≤2
Wir schreiben dann isseq(p0 , p1 , p2 ).
Wir können auch Prozesse parallel zerlegen bzw. parallel zusammensetzen. Dabei verwenden wir eine gewisse Menge gemeinsamer Aktionen S ⊆ A. Dies sind
Aktionen, die in beiden Teilprozessen auftreten (die quasi gemeinsam ausgeführt
werden). Wir sagen, Prozess p0 ist aus p1 und p2 über die gemeinsamen Aktionen
174
KAPITEL 1. PROZESSE, INTERAKTION, KOORDINATION . . .
S ⊆ A parallel zusammengesetzt, wenn gilt:
∧
∧
∧
∧
E0 = E1 ∪ E2
E1 ∩ E2 = {e ∈ E0 : α0 (e) ∈ S}
α0 |E1 = α1
α0 |E2 = α2
≤0 = (≤1 ∪ ≤2 )∗ Transitive Hülle
(d.h. ≤0 |E1 ×E1 =≤1 ∧ ≤0 |E2 ×E2 =≤2 ). Wir schreiben dann ispar(p0 , p1 , p2 , S).
1.1.5
Aktionen als Zustandsübergänge
Neben der Beschreibung von Systemen durch die Prozesse, die deren Abläufe bilden,
können wir Systeme auch durch Zustände und ihr Verhalten durch Zustandsänderungen modellieren. Wir geben für ein System einen Zustandsraum an und deuten
Aktionen in Prozessen als Zustandsänderungen.
Wir definieren hierfür nichtdeterministische Zustandsautomaten mit Transitionsaktionen:
• S Menge von Zuständen (Zustandsraum)
• A Menge von Transitionsaktionen
• R ⊆ S × A × S eine Zustandsübergangsrelation
• S0 Menge von möglichen Anfangszuständen
Seien σ0 , σ1 ∈ S und a ∈ A gegeben. (σ0 , a, σ1 ) drückt aus, dass im Zustand σ0 Aktion a ausgeführt werden kann und dies zum Nachfolgezustand σ1 führen kann. Das
heißt, in einem Zustand können mehrere Transitionsaktionen zu unterschiedlichen
Nachfolgezuständen führen. Der Automat ist nichtdeterministisch.
a
Wir schreiben σ0 → σ1 für (σ0 , a, σ1 ) ∈ R.
Jedem Zustandsautomaten lassen sich Spuren zuordnen. Eine Folge ai , 1 ≤ i ≤ k
mit k ∈ N ∪ {∞} ist eine endliche oder unendliche Aktionsspur des Zustandsautoa
maten, falls eine Folge von Zuständen σi existiert mit σ0 ∈ S0 und σi−1 →i σi für
alle 1 ≤ i ≤ k.
• Eine Aktion a ∈ A heißt deterministisch, wenn für jeden Zustand σ ∈ S
a
höchstens ein Nachfolgezustand σ1 ∈ S existiert mit σ → σ1 .
• Eine Aktion a ∈ A heißt total, wenn für jeden Zustand σ ∈ S ein Nachfola
gezustand σ1 ∈ S existiert mit σ → σ1 .
Totalen, deterministischen Aktionen können wir (totale) Zustandsänderungsabbildungen zuordnen.
Sei S eine Menge von Zuständen, A eine Menge von Aktionen. Wir ordnen jeder
Aktion a ∈ A eine Zustandsänderung zu:
% : A → (S → S)
Jede Aktion a ∈ A definiert durch %(a) eine Abbildung auf Zuständen. Endlichen
sequentiellen Prozessen lassen sich Zustandsänderungsabbildungen zuordnen.
1.1. PROZESSE
175
Beispiel:
1
2
4
6
x := 10;
y := 0;
while x > 0 do
y := y + x;
x := x - 1
od
Ereignisse
Aktionen
a0
b0
a1
b1
c1
..
.
x := 10;
y := 0;
(x > 0) ?
y := y+x;
x := x-1;
..
.
Zustand
x
y
10
?
10
0
10
0
10 10
9
10
..
..
.
.
0
55
%(y := 10)(n, m) = (n, 0)
%(x := 10)(n, m) = (10, m)
%(y := y + x)(n, m) = (n, n + m)
%(x := x − 1)(n, m) = (n − 1, m)
Frage: Welche Zustandsübergänge können ohne Schwierigkeit zeitlich parallel
ausgeführt werden?
Wir sagen, Aktionen a, b ∈ A seien im Konflikt, wenn sie nicht problemlos nebeneinander ausgeführt werden können.
Beispiel: Konflikt bei parallelen Zuweisungen
(1) x := x+1
(2) x := x*2
Aktionen (1) und (2) sind im Konflikt. Bei paralleler Ausführung kann dem Prozess
keine Zustandsänderung eindeutig zugewiesen werden.
Modellierung: Die Relation Conflict ⊆ A × A gibt für jedes Paar von Aktionen
a1 , a2 ∈ A an, ob Konflikt bestehen. Im Falle von Zuweisungen ist Conflict wie folgt
definiert (Bernstein – Bedingung):
Die Zuweisungen
(1) x1, ..., xn := E1, ..., En
(2) y1, ..., ym := F1, ..., Fm
sind konfliktfrei, falls
• jede Programmvariable xi nicht in der Anweisung (2) vorkommt und
• jede Programmvariable yi nicht in der Anweisung (1) vorkommt.
Gilt die Bernsteinbedingung, dann ist die Reihenfolge der Ausführung der beiden
Zuweisungen ohne Einfluss auf den Endzustand.
Für konfliktfreie Aktionen a1 , a2 ∈ A setzen wir voraus:
176
KAPITEL 1. PROZESSE, INTERAKTION, KOORDINATION . . .
• dass die zugehörigen Zustandsübergänge unabhängig sind, d.h. die Reihenfolge
der Ausführung spielt keine Rolle:
%(a1 ) ◦ %(a2 ) = %(a2 ) ◦ %(a1 )
• a1 , a2 können problemlos zeitlich nebenläufig (parallel) ausgeführt werden.
Wir schreiben a1 k a2 für die Aktion der Parallelausführung der Aktionen a1 und
a2 .
Unter der Voraussetzung der Konfliktfreiheit:
%(a1 k a2 ) =def %(a1 ) ◦ %(a2 )
Parallele Komposition konfliktfreier Aktionen a1 , a2 ∈ A führt keine neuen Konflikte
ein, d.h.
¬((a1 , a3 ) ∈ Conflict) ∧ ¬((a2 , a3 ) ∈ Conflict) ⇒ ¬((a1 k a2 , a3 ) ∈ Conflict)
Damit gilt bei paralleler Komposition konfliktfreier Aktionen für die induzierte Zustandsänderung:
• a1 k a2 = a2 k a1
• (a1 k a2 ) k a3 = a1 k (a2 k a3 )
Ist die Konfliktfreiheit verletzt, so kann über die Wirkung der Parallelausführung
keine Aussage gemacht werden.
p = (E0 , ≤0 , α) heißt konfliktfrei, falls alle konfliktträchtigen Aktionen nur
bei nicht parallelen Paaren von Ereignissen auftreten, d.h.
∀e, d ∈ E0 : (α(e), α(d)) ∈ Conflict ⇒ e ≤0 d ∨ d ≤0 e
Gegeben sei eine Zustandsmenge S, Aktionen A und eine Zustandsübergangsrelation R. R lässt sich auf endliche Prozesse und Spuren erweitern.
Spuren: Seien σ0 , σ1 , σ2 ∈ S, a ∈ A und s1 , s2 ∈ S ∗ Sequenzen von Aktionen. Induktive Definition der Zustandsübergänge für Spuren über die Länge der Sequenzen:
ε
σ 0 → σ0
a
hai
σ 0 → σ1 ⇒ σ 0 → σ 1
s
s
s ◦s
1
2
σ0 →
σ1 ∧ σ 1 →
σ2 ⇒ σ0 1→ 2 σ2
Wir erweitern die Zustandsübergangssicht auf endliche Prozesse. Wieder definieren
wir eine Zustandsübergangsrelation für Prozesse. Sei P ein Prozess; wir schreiben
p
σ0 → σ1 , um auszudrücken, dass bei Ausführung der Aktionen in P ausgehend vom
Zustand σ0 der Zustand σ1 erreicht werden kann.
Sei s eine Sequenz von Aktionen, die einer Sequentialisierung des endlichen Prozesses
p
s
P entspricht und es gelte σ0 → σ1 . Dann und nur dann gelte auch σ0 → σ1 .
Die Reihenfolge der Aktionen in der Sequentialisierung kann bewirken, dass
unterschiedliche Zustände erreicht werden. Wir sprechen von Nichtdeterminismus,
der sich aus der Zufälligkeit der Hintereinanderausführung paralleler Ereignisse bzw.
der zugeordneten Aktionen ergibt.
1.2
Systembeschreibungen durch Mengen von Prozessen
Ein Prozess beschreibt einen einzelnen Ablauf eines Systems. In der Regel besitzt
ein System viele Abläufe (unter Umständen unendlich viele). Damit wird ein System
durch eine Menge von Abläufen beschrieben.
1.2. SYSTEMBESCHREIBUNGEN DURCH MENGEN VON PROZESSEN 177
PN 1
PN 2
1
2
b
a
e
d
c
f
Abbildung 1.3: Petri-Netze
Beispiel: Programme besitzen i.d.R. für unterschiedliche Eingaben unterschiedliche Abläufe.
Wir betrachten exemplarisch drei Möglichkeiten zur Beschreibung von Systemen:
• Petri-Netze
• formale Beschreibungssprachen
• Prädikatenlogik
Alle drei Ansätze beschreiben Mengen von Abläufen.
1.2.1
Petri-Netze
(eingeführt 1962 von C. A. Petri in seiner Dissertation)
Beispiel: (s. Abb. (1.3))
Abläufe in PN1 mit Anfangsmarkierung 1 auf 1, 0 auf 2:
b → a → c → a → c → a → b → ...
c → a → c → a → b → a → b → ...
Abläufe in PN2 :
e
f
→ d → e
%
&
f
→ d → e → ...
%
&
%
f
Petri-Netz: gegeben durch:
T0
endliche Menge von Transitionen
P0
endliche Menge von Plätzen
R ⊆ (T0 × P0 ) ∪ (P0 × T0 ) Flussrelation
Das Tripel (T0 , P0 , R) definiert ein Petri-Netz.
Die Belegung eines Petri-Netzes entspricht einer Abbildung:
b : P0 → N0 natürlichzahlige Belegung
Stellen- / Transitionsnetze
b : P0 → B boolesche Belegung
Bedingungs- / Ereignisnetze
Eine Belegung definiert einen Zustand des Petri-Netzes. Das Schalten einer Transition definiert einen Zustandsübergang
1 b 0
→
1
0
Eine Menge K ⊆ T0 von Transitionen heißt für eine natürlichzahlige Belegung
schaltbereit, wenn gilt:
|{k ∈ K : (p, k) ∈ R}| ≤ b(p)∀p ∈ P0
178
KAPITEL 1. PROZESSE, INTERAKTION, KOORDINATION . . .
v
h
p
l
Abbildung 1.4: Erzeuger – Verbraucher – Modell
Falls K schaltbereit für die Belegung b ist, so führt das Schalten von K auf die
Belegung b0 mit
b0 (p) = b(p) − |{k ∈ K : (p, k) ∈ R}| + |{k ∈ K : (k, p) ∈ R}|
Bei booleschen Petri-Netzen definiert sich die Schaltbereitschaft ganz analog, nur,
dass jetzt alle Zielplätze für jede Transition K mit false belegt sein müsen. Ansonsten entspricht true der 1 und false der 0.
Lemma: Sequentialisierung
Ist K schaltbereit für eine Belegung b und gilt
K = K1 ∪ K2
K1 ∩ K 2 = ∅
K
so sind auch K1 und K2 schaltbereit für b. Wir schreiben b → b0 , falls durch Schalten
von K die Belegung b in b0 übergeführt wird (Zustandsübergang).
Feststellung: Die Übergänge sind deterministisch.
K
K
K
b0 →1 b1 ∧ b1 →2 b2 ⇒ b0 → b2
(mit den Bezeichnungen wie oben)
Beispiel: Petri-Netz des Erzeuger – Verbraucher – Systems
(1.4))
Ablauf (ohne die strichlierte Erweiterung):
(vgl. Abb.
p → l → p → l → p → l → ...
&
&
&
h → v → h → v → h → ...
Die Erweiterung des Netzes stellt sicher, dass hole (h) und liefere (l) nie
parallel ausgeführt werden.
Jedes Petri-Netz mit einer gegebenen Anfangsbelegung definiert eine Menge von
Prozessen. Die Aktionen dieser Prozesse entsprechen den Transitionen.
Seien b und b0 Belegungen eines Petri-Netzes und sei p ein Prozess. Wir schreiben
p
b → b0
wenn das Netz mit Belegung b den endlichen Prozess p ausführen kann und dies in
die Belegung b0 führt. Diese Relation definieren wir induktiv (sozusagen Induktion
über die Anzahl der Ereignisse im Prozess p):
(1) Der leere Prozess p (d.h. der Prozess mit leerer Ereignismenge) hat folgenden
p
Übergang: b → b.
1.2. SYSTEMBESCHREIBUNGEN DURCH MENGEN VON PROZESSEN 179
b
a
y
x
c
Abbildung 1.5: Petri-Netz
(2) Sei p ein Prozess mit trivialer Kausalitätsordnung (jedes Ereignis ist nur für
sich selbst kausal) und alle Ereignisse sind unterschiedlich markiert. Dann
p
gilt b → b0 genau dann, wenn die Menge {α(e) : e ∈ Ereignismenge zu p}
schaltbereit ist und von b zu b0 führt.
Für jeden Prozess p = (E0 , ≤0 , α0 ), auf den nicht die Bedingungen (1) und (2)
p
zutreffen, gilt b0 → b1 genau dann, wenn für jede nichttriviale Zerlegung von p in
ein Präfix p1 v p und einen Restprozess p2 = p|E0 \E1 eine Belegung b0 existiert, so
p1
p2
dass gilt b0 → b0 ∧ b0 → b1 .
p
Beobachtungen: Gilt b0 → b1 , dann gilt für jede Sequentialisierung p0 von p
p0
ebenfalls b0 → b1 .
Achtung: Die Umkehrung gilt in der Regel nicht!
Auch für Petri-Netze ist der Begriff der Fairness von Interesse: Für ein PetriNetz mit gegebener Anfangsbelegung heißt ein unendlicher Ablauf unfair, wenn
eine Transition nur endlich oft schaltet, obwohl sie unendlich oft schaltbereit ist.
Die Menge der Belegungen eines Petri-Netzes N bezeichnen wir mit BE(N ). Für
eine gegebene Anfangsbelegung (Anfangszustand) sind bestimmte Belegungen
durch Schalten von Transitionen erreichbar, andere nicht.
Die Menge der im Netz N vom Anfangszustand b0 aus erreichbaren Belegungen
definieren wir wie folgt:
n
o
p
R(N, b0 ) = b ∈ BE(N ) : ∃p : p endlich ∧ b0 → b
Beispiel:
Fairness (s. Abb. (1.5)) Mögliche Schaltvorgänge bei Anfangszustand
x
0
=
y
1 :
• a c a b a b a c . . . → fair
• a b a b a b a b . . . → unfair
• a c a b a b a b . . . → unfair
Menge der erreichbaren Zustände: 01 , 10
Es gilt: x + y = 1 (Systeminvariante).
Mengen von Zuständen lassen sich durch Prädikate q : BE(N ) → B charakterisieren. Eine Systeminvariante für das Netz N mit Anfangsbelegung b0 ist ein Prädikat
q, für das gilt:
∀b ∈ BE(N ) : b ∈ R(N, b0 ) ⇒ q(b)
Idee: Systeminvarianten charakterisieren, welche kritischen Zustände nicht eingenommen werden.
Die Gültigkeit einer Invariante für ein Petri-Netz beweisen wir wie folgt:
180
KAPITEL 1. PROZESSE, INTERAKTION, KOORDINATION . . .
Menge aller Zustaende
q
Menge der erreichbaren
Zustaende
Abbildung 1.6: Zustände und Invarianten
(1) q(b0 )
t
(2) ∀b, b0 ∈ BE(N )∀t ∈ T0 : q(b) ∧ b → b0 ⇒ q(b0 )
Achtung: Diese Beweismethode funktioniert für bestimmte Invarianten nicht, wenn
für nicht erreichbare Zustände b ∈ BE(N )\R(N, b0 ) die Aussage (2) nicht beweisbar
ist (vgl. Abb. (1.6)).
Ausweg: Wir suchen eine Invariante q 0 mit der Eigenschaft
∀b ∈ BE(N ) : q 0 (b) ⇒ q(b)
(wobei für q 0 die Beweismethode funktioniert). Im Zweifel können wir immer q 0 (b) =
b ∈ R(N, b0 ) wählen.
Beispiel: Erzeuger – Verbraucher – Problem Können l und h gleichzeitig
schalten? Ist t ≤ 1 Invariante?
Hilfsinvariante: t + 1 ≤ 1
Im Anfangszustand: t + 1 = 1
Beobachtung: Jede Transition lässt t + 1 unverändert ⇒ Hilfsinvariante ist ok ⇒
t ≤ 1.
Petri-Netze sind in der Regel nicht deterministisch! Für eine gegebene Belegung
b gibt es in der Regel viele unterschiedliche Belegungen, die über die einzelnen
Transaktionen erreicht werden können. Unterschiedliche Transitionen (insbesondere
bei Konflikten) können schalten.
Petri-Netze sind der vielleicht einfachste Formalismus, in dem wir die typischen
Probleme verteilter Systeme studieren können:
• Struktur der Systeme
• Prozesse, die auf dem System ablaufen (Prozesssicht)
• Zustandsraum, Zustandsübergänge, Menge der erreichbaren Zustände
• Fairness
Typische Fragestellungen für ein System (u.U. modelliert durch ein Petri-Netz):
(1) Welche Eigenschaften haben erreichbare Zustände? (Invarianten)
Ist die Menge der erreichbaren Zustände endlich?
(2) Ist Nebenläufigkeit (gleichzeitiges Schalten) für bestimmte Transitionen ausgeschlossen?
(3) Können Belegungen erreicht werden, von denen aus gewisse oder alle Transitionen nie wieder schalten werden (Verklemmung, Deadlock)
(4) Sind alle Abläufe fair?
1.2. SYSTEMBESCHREIBUNGEN DURCH MENGEN VON PROZESSEN 181
Beobachtung: Jedes Petri-Netz mit Anfangszustand definiert einen Zustandsübergangsautomaten für die Menge der erreichbaren Belegungen / Zustände. Die Übergänge entsprechen dem Schalten von Transitionen.
Jedem Petri-Netz mit Anfangsbelegung kann solch ein Automat zugeordnet werden.
Allerdings drücken diese Automaten die Nebenläufigkeit in Prozessen nicht mehr
aus. Wir beschränken uns auf sequentielle Prozesse.
1.2.2
Prozessalgebra
Wir können Prozesse und damit verteilte, parallel ablaufende Systeme auch durch
Sprachen (Terme) beschreiben. Ein Spezialfall sind Programmiersprachen. Wir betrachten eine vereinfachte Programmiersprache zur Beschreibung von Prozessen.
Wir sprechen von Prozessalgebra (Nache Ideen von C.A.R. Hoare: CSP, Communicating Sequential Processes, R. Milner: CCS, Calculus of Communicating Systems).
Wir definieren die Sprache der CSP-Agenten:
hagenti ::=skip
| hactioni
| hagenti ; hagenti
| hagenti or hagenti
| hagenti k hagenti
| hagent idi
| hagent idi :: hagenti
Beispiel: Ampel mit Aktionen rot, gelb, grün:
Agentenidentifikatoren: ampel:: (rot; gelb; grün; ampel)
Dies entspricht dem Agenten mit Identifikator ampel, für den die Gleichung
ampel = rot; gelb; grün; ampel (Rekursion)
gilt. ampel beschreibt den folgenden Prozess:
rot → gelb → grün → rot → . . .
Beispiel: Fahrkartenautomat
Aktionen:
• Zielwahl
• Preisanzeige
• Geldeinwurf
• Kartenausgabe
• Rückgeldausgabe
• Abbruch
Agent, der den Automaten beschreibt:
1
2
4
automat :: Zielwahl;
Preisanzeige;
((Geldeinwurf;
((Kartenausgabe || Rueckgeldausgabe) or Abbruch))
or Abbruch);
182
KAPITEL 1. PROZESSE, INTERAKTION, KOORDINATION . . .
Prozesse für den Automaten:
Zielwahl → Preisanzeige → Abbruch → . . .
Zielwahl → Preisanzeige → Geldeinwurf → Abbruch → . . .
...
Die Sprache der Agenten ist eine Prozessbeschreibungssprache. Da wir aus gegebenen Termen der Spache Agent durch or, ; und || neue Agenten bilden
können, sprechen wir von einer Prozessalgebra.
Agenten beschreiben Mengen von Prozessen. Um genau festzulegen, welche Prozesse durch einen gegebenen Agenten beschrieben werden, führen wir (ähnlich wie für
Belegungen bei Petri-Netzen) eine Relation
p
t → t0
ein, die ausdrückt, dass Agent t den Prozess p ausführen kann und sich dadurch
verhält wie durch den Agenten t0 beschrieben. Dies wird durch folgende Regeln
festgelegt:
(1) Jeder Agent kann den leeren Prozess p∅ ausführen:
p∅
t→t
(2) Sei a eine Aktion.
pa
a → skip
wobei pa der Prozess mit nur einem Ereignis e sei mit α(e) = a.
(3) Auswahl:
p
p
p
p
t1 → t0 ⇒ t1 or t2 → t0
t2 → t0 ⇒ t1 or t2 → t0
(4) Sequentielle Komposition:
p
p
t1 → t 0 ⇒ t 1 ; t 2 → t 0 ; t2
p1
p2
p
t1 → skip ∧ t2 → t ∧ isset(p, p1 , p2 ) ⇒ t1 ; t2 → t
(5) Parallele Komposition:
p1
p2
p
t1 → t01 ∧ t2 → t02 ∧ ispar(p, p1 , p2 , ∅) ⇒ t1 k t2 → t01 k t02
p1
p2
p
t1 → skip ∧ t2 → skip ∧ ispar(p, p1 , p2 , ∅) ⇒ t1 k t2 → skip k skip
Algebraisch:
1
2
4
skip || skip = skip
skip; t = t; skip = t => skip; skip = skip
t1 || t2 = t2 || t1
(t1 || t2) || t3 = t1 || (t2 || t3)
skip || t = t
o
n
p
P (t) = p : ∃t0 : t → t0
Menge der Prozesse, die der Agent t ausführen kann.
[
S(t) =
spuren(p)
p∈P (t)
Zwei Aktionen t, t0 sind spuräquivalent, falls S(t) = S(t0 ) oder prozessäquivalent, falls P (t) = P (t0 ).
1.2. SYSTEMBESCHREIBUNGEN DURCH MENGEN VON PROZESSEN 183
a1
b1
b2
c
a2
b3
Abbildung 1.7: Ablauf des Beispielagenten
6
a || b = (a; b) or (b; a)
Achtung: Bestimmte Agenten führen nie nach skip!
(6) Rekursion
p
p
t[x :: t/x] → t0 ⇒ x :: t → t0
Alle Fragen, die wir für Petri-Netze gestellt haben, können wir analog für Agenten
p
stellen. Wir sagen, von t ist t’ erreichbar, wenn t → t’ für einen Prozess p gilt.
Beispiele:
• Ampel
ampel :: rot; gelb; grün; ampel
prot
rot −→ skip
pgelb
gelb −→ skip
pgrün
grün −→ skip
prot
prot
rot −→ skip ⇒ rot; gelb −→ skip; gelb
pgelb
prot
p
rot −→ skip ∧ gelb −→ skip ∧ isseq(p, prot , pgelb ) ⇒ rot; gelb −→ skip
Menge der erreichbaren Agenten (Zustände):
prot
ampel −→ gelb; grün; ampel
prot ;pgelb
ampel −→ grün; ampel
prot ;pgelb ;pgrün
ampel
−→
ampel
• q :: a; q
p0
p0
a; (q :: q; q) −→ q :: a; q ⇒ q :: a; q −→ q :: a; q
1.2.3
Synchronisation und Koordination von Agenten
Für Agenten t1 und t2 gilt, dass diese in t1 || t2 ihre Aktionen / Prozesse unabhängig voneinander ausführen. Damit wir in der Lage sind, kausale Beziehungen
zwischen den Prozessen von t1 und t2 herzustellen, verwenden wir eine Menge
S ⊆ A von gemeinsamen Aktionen (englisch: handshake).
Wir definieren nun eine parallele Komposition mit gemeinsamen Ereignissen,
gekennzeichnet durch die Aktionen in der Synchronisationsmenge S. Wir schreiben
t1 kS t2 für den Agengen, der Prozesse ausführt, die sich parallel aus den Prozessen
von t1 und t2 zusammensetzen, wobei jedoch alle Ereignisse in diesen Prozessen
für t1 und t2, die mit Aktionen aus S markiert sind, gemeinsame Ereignisse sind.
Damit unterscheiden wir in den Prozessen p1 für Agent t1 und p2 für t2 zwischen
den Ereignissen, die beide unabhängig parallel ausführen (das sind die Ereignisse
mit Aktionen aus A \ S) und den Ereignissen, die die Agenten gemeinsam ausführen
(die Ereignisse mit Aktionen aus S).
184
KAPITEL 1. PROZESSE, INTERAKTION, KOORDINATION . . .
a1
b1
p
a2
v
p
a1
v
b2 a2
b1
p
v
p
v
p
b2 a2
Abbildung 1.8: Ablauf mit gegenseitigem Ausschluss von Aktionen
Beispiel: (a1; c; a2) k{c} ((b1 || b2); c; b3)
Ablauf: s. Abb. (1.7)
Definition der parallelen Komposition:
p1
p2
p
t1 → t01 ∧ t2 → t02 ∧ ispar(p, p1 , p2 , S) ⇒ t1 kS t2 → t01 kS t02
p1
p2
p
t1 → skip ∧ t2 → skip ∧ ispar(p, p1 , p2 , S) ⇒ t1 kS t2 → skip
Beispiel: Gegenseitiger Ausschluss für Aktionen b1, b2:
[(t1 :: a1; p; b1; v; t1) || (t2 :: a2; p; b2; v; t2)]
k{p,v} sema :: p; v; sema Ablauf: s. Abb. (1.8)
Probleme:
• Es kann passieren, dass ein Agent im obigen Beispiel nie ein p ausführt, da
ihm der Partnerprozess ständig zuvorkommt.
• Verklemmung: (a; c) k{c,d} (b; d)
Die Agenten stimmen in den synchronisierten Aktionen nicht überein, es ist
kein Fortschritt nach Ausfühung der Aktionen a und b mehr möglich. Wir
sprechen von einer Verklemmung.
1.2.4
Beschreibung von Prozessen mit Prädikaten
Wir beschreiben ein System durch eine Menge von Prozessen. Eine gängige Beschreibung von Mengen erhalten wir durch Prädikate.
q : Prozesse → B
(wobei Prozesse die Menge der Prozesse über einer gegebenen Menge A von Aktionen sei).
Wesentliche Frage: Wie Prädikate auf Prozessen definieren?
(1) Wir nutzen die Präfixordnung.
(2) Wir definieren für einen Prozess p ∈ Prozesse und für eine Aktion a ∈ A die
Zahl #(a, p) (Anzahl der Aktionen a in p):
#(a, p) = |{e ∈ E0 : α0 (e) = a}|
wobei p = (E0 , ≤0 , α0 ). Es ist #(a, p) ∈ N0 ∪ {∞}.
Erweiterung auf Teilmengen K ⊆ A:
X
#(K, p) =
#(a, p)
a∈K
Oft ist es von Interesse, nicht über den unendlichen Prozess p, sondern über Eigenschaften seiner endlichen Präfixe p0 v p zu reden:
0 ≤ #(a1 , p0 ) − #(a2 , p0 ) ≤ 1∀p0 v p, p0 endlich
⇒ a1 und a2 bilden eine kausale Kette
1.2. SYSTEMBESCHREIBUNGEN DURCH MENGEN VON PROZESSEN 185
Weiteres Prädikat: Gegenseitiger Ausschluss von Aktionen b1 und b2 :
ggA(b1 , b2 , p) = ∀e, e0 ∈ E0 : α0 (e) = b1 ∧ α0 (e0 ) = b2 ⇒ (e ≤0 e0 ) ∨ (e0 ≤ e)
Wichtiges Mittel, um Prädikate über sequentiellen Prozessen zu definieren: Temporale Logik.
Betrachten wir Prädikate über Prozessen (und damit über Systemen) mit unendlichen Abläufen, so können wir zwei Klassen von Aussagen unterscheiden:
➥ Sicherheitseigenschaften: Dies sind Eigenschaften, bei denen ein Verstoß stets
durch eine endliche Beobachtung nach endlich vielen Schritten festgestellt
werden kann.
➥ Lebendigkeitseigenschaften: Dies sind Eigenschaften, bei denen wir einen Verstoß nicht durch endliche Beobachtung feststellen können, sondern nur durch
Betrachtung des unendlichen Gesamtprozesses.
Beobachtung: Jede Systemeigenschaft (in der Regel eine Mischung aus Sicherheitsund Lebendigkeitseigenschaft) lässt sich zerlegen in eine reine Sicherheits- und eine
reine Lebendigkeitseigenschaft.
Beispiel:
Sicherheits- und Lebendigkeitseigenschaften für einen Lift:
• Sicherheitseigenschaften:
– Solange der Lift fährt, sind alle Türen geschlossen.
– Stockwerktüren sind geschlossen, wenn der Lift nicht in diesem Stockwerk
hält.
– Lift hält nur auf Anforderung.
– ...
• Lebendigkeitseigenschaften:
– Bei Anforderung hält der Lift irgendwann im entsprechenden Stockwerk,
falls er nicht blockiert wird.
– Bei Fahranforderung schließt die Tür irgendwann, falls sie nicht blockiert
wird.
– ...
Wie formal ausdrücken?
• Sicherheitseigenschaften:
– Invarianten des Systems auf Zuständen
– bei Prozessen: Prädikate, die für alle endlichen Präfixe gelten. q gilt als
Sicherheitseigenschaft für den Prozess p, wenn ∀p0 v p, p0 endlich q(p0 )
gilt.
• Lebendigkeitseigenschaften: Wir definieren für Zusicherungen auf Zuständen
(oder Prozessen) folgende Aussage: Seien Q, P Zusicherungen. Q leads to P
heißt für unser System: Ist das System in einem Zustand, in dem Q gilt, dann
ist es irgendwann (nach endlich vielen Schritten) in einem Zustand, in dem P
gilt.
true leads to P steht für: Das System erreicht immer irgendwann einen Zustand, in dem P gilt.
186
KAPITEL 1. PROZESSE, INTERAKTION, KOORDINATION . . .
1.3
Programmiersprachen zur Beschreibung kooperierender / kommunizierender Systeme / Prozesse
Programme, insbesondere anweisungsorientierte, beschreiben Prozesse.
Beispiel:
1
2
4
var nat x := 10;
while true do
x := x+1
od
Invariante: x ≥ 10.
Lebendigkeitseigenschaften:
• true leads to x = 100
• ∀n ∈ N : x = n leads to x > n
Allerdings sind diese Prozesse sequentiell. Wir führen nun Sprachmittel ein, um
parallele Prozesse durch Programme beschreiben zu können.
1.3.1
Parallelität in anweisungsorientierten Programmiersprachen
Voneinander logisch unabhängige Anweisungen können wir ohne Probleme parallel
ausführen. Wir schreiben
k S1 k · · · k Sn k
für eine parallele Anweisung. Dies steht für:
parallel aus.
führe die Anweisungen S1 , . . . , Sn
Beispiel: k x := x + 1; z := x2 ky := y − 1 k Solche Anweisungen können unabhängig voneinander ausgeführt werden, da sie keine gemeinsamen Variablen verwenden (Bernstein-Bedingung).
{x = 0 ∧ z = 0}
k x := x + 1kz := x2 k
{x = 1 ∧ (z = 0 ∨ z = 1)}
Feststellung: Falls in S1 , . . . , Sn gemeinsame Programmvariablen auftreten (dies
sind Programmvariablen, die in mindestens einer Anweisung geschrieben und in
mindestens einer weiteren Anweisung gelesen oder geschrieben werden), so lässt
sich über den Endzustand der Ausführung nichts aussagen, wenn man nichts über
die Granularität (Atomizität) der Anweisungen weiß.
Fazit: Zusätzliche Hilfsmittel (Konstrukte der Programmiersprache) sind erforderlich, um Parallelität zu kontrollieren, zu steuern und zu koordinieren. Dabei sind
wir an folgenden Ausdrucksmöglichkeiten interessiert:
(1) Sicherstellung der Ausführung bestimmter Anweisungen ohne unerwartete
Einflüsse durch parallel ablaufende Programme (ohne Unterbrechung) — Konzept der unteilbaren Anweisung / Aktion (atomare Anweisung): gegenseitiger
Ausschluss.
1.3. PROGRAMMIERSPRACHEN ZUR BESCHREIBUNG. . .
187
Nutzereingabe
Sensoren
Controller
reaktives Programm
gesteuertes System
("Regelstrecke")
Aktuatoren
Ausgabe fuer
den Nutzer
Abbildung 1.9: reaktives System
(2) Warten auf das Eintreten gewisser Bedingungen (kann nicht einfach durch if
erreicht werden, da if nicht zu Warten führt, sondern zu sofortiger Fortsetzung der Berechnung abhängig vom Wert der Bedingung).
(3) Nachrichtenaustausch: Zwischen den parallel ablaufenden Anwendungen wollen wir Informationen austauschen.
Achtung: Auch beim Arbeiten mit atomaren Anweisungen kann durch Parallelität Nichtdeterminismus auftreten, durch die Zufälligkeit der relativen Geschwindigkeit der Ausführung.
Durch die Konzepte paralleler Prozesse können wir auch Anweisungen schreiben,
die Vorgänge in der Umgebung eines Rechners steuern und überwachen. Wir sprechen von reaktiven Systemen / Programmen (z.B. Steuerung technischer Vorgänge
durch Rechner (Controller) und ihre Software: mobile Telefone, Haushaltsgeräte,
Automobiltechnik, Avionik, Robotik, . . . ).
Eine Zahl: 98% aller programmierbaren Prozessoren laufen eingebettet / reaktiv.
Definition: reaktiv
Ein Program heißt reaktiv, wenn es auf Ereignisse seiner Umgebung reagiert
und somit technische oder organisatorische Vorgänge steuert. Dies erfordert:
• Sensoren: Vorrichtungen, die Ereignisse erkennen und in digitale Signale
umsetzen
• Aktuatoren: Geräte, die digitale Signale in Steueranweisungen umsetzen
Für solche Systeme sind Echtzeitanforderungen typisch. Das heißt, dass die Systeme
zeitbezogen reagieren. Wir unterscheiden zwei Kategorien:
• Weiche Echtzeitanforderungen: keine scharfen Zeitschranken (z.B. Telefon)
• Harte Echtzeitanforderungen: Wenn diese nicht eingehalten werden, treten
katastrophale Fehler auf. (z.B. Motorsteuerung, Airbag, ABS, Robotik)
1
2
4
||- x := 0;
x := 1;
|| while x <> 0 do
y := y+1
od
-||
➥ Terminiert dieses Programm?
➥ Wenn ja, welchen Wert haben x und y?
188
KAPITEL 1. PROZESSE, INTERAKTION, KOORDINATION . . .
Die parallele Anweisung terminiert, falls alle parallel laufenden Programme terminieren.
Zwei Fälle für unser Beispiel:
(1) Die while – Bedingung wird nicht ausgewertet, solange x = 0 gilt. Dann terminiert die Wiederholungsanweisung und damit das ganze parallele Programm
nicht.
(2) Die while – Bedingung wird ausgewertet, wenn x = 1 gilt. Das Programm
terminiert. Dann hat x den Wert 1 und y hat einen beliebigen Wert.
Frage: Wie Information zwischen parallel ablaufenden Programmen austauschen?
Wir unterscheiden grundsätzlich zwei Möglichkeiten:
• Nachrichtenaustausch / Austausch von Signalen
• gemeinsame Variablen
1.3.2
Kommunikation durch Nachrichtenaustausch
Eine grundlegende Form der Kommunikation besteht im Austausch von Nachrichten. Dabei unterscheiden wir:
• den Sender (Nachrichtenquelle)
• den / die Empfänger
der Nachricht. In der Regel haben wir
• einen Sender
• einen (
point – to – point), viele oder alle (
Wir konzentrieren uns auf den Fall
Broadcasting) Empfänger.
ein Sender, ein Empfänger.
Wichtige Frage: Wie Nachrichten adressieren?
➥ direkt Empfänger adressieren
➥ über Nachrichtenmedien (Briefkästen, Kanäle etc.)
Wir verwenden Nachrichtenkanäle. Deklaration / Vereinbarung eines Kanals:
channel m c
mit:
m Kanalsorte ∼ Sorte der Nachrichten, die über den Kanal gesendet werden
c Kanalidentifikator
Operationen / Anweisungen für Kanäle:
send E on c
Senden einer Nachricht
receive v on c Empfangen einer Nachricht und Ablegen in v
mit:
c Kanalidentifikator
E Ausdruck von der Sorte des Kanals
v Programmvariable von der Sorte des Kanals
1.3. PROGRAMMIERSPRACHEN ZUR BESCHREIBUNG. . .
Sender
Beispiel:
1
2
4
6
8
10
189
Empfaenger
Erzeuger / Verbraucher
|- channel m c;
var m x, z := x0, z0;
||- while not B(x) do
receive x on c;
consume(x)
od
|| while not B(x) do
producenext(z);
send z on c
od
-||
-|
1
2
4
6
8
proc consume = (m x):
if x <> 0 then
r := x * r
fi
proc producenext = (var m x):
if x <> 0 then
x := x-1
fi
10
B(x) = (x = 0)
12
m = nat
Sender: schickt die Zahlen x0-1, x0-2, ..., 0
Empfänger: multipliziert die Zahlen in r auf.
Wichtige Fragen:
(1) Was passiert, wenn receive v on c ausgeführt wird und keine Nachricht
vorliegt?
Prinzipielle Möglichkeiten:
(a) Es wird eine
leere Nachricht zurückgegeben.
(b) Es wird gewartet, bis eine Nachricht vorliegt.
Wir wählen den Fall (b).
(2) Was passiert, wenn send E on c ausgeführt wird, aber kein Empfänger bereit
ist, die Nachricht zu empfangen?
(a) Die Nachricht geht verloren.
(b) Es wird gewartet, bis ein Empfänger bereit ist und dann wird die Nachricht übergeben (Telefonprinzip, Handshaking, synchroner Nachrichtenaustausch).
190
KAPITEL 1. PROZESSE, INTERAKTION, KOORDINATION . . .
(c) Die Nachricht wird gesendet, gegebenenfalls zwischengepuffert (d.h. in
einem Puffer / einer Warteschlange zwischengespeichert), bis ein Empfänger empfangsbereit ist (d.h. eine receive – Anweisung ausführt)
(asynchroner Nachrichtenaustausch).
Für (c) gibt es Varianten, falls die Puffergröße endlich beschränkt ist.
Konvention: receive v on c und send E on c stehen für asynchronen, v ? c
und E ! c für nachrichtensynchronen Nachrichtenaustausch.
Achtung: Bei synchroner Arbeitsweise treten leicht Verklemmungen auf!
1
2
4
||- send E1
receive
|| send E2
receive
on
x1
on
x2
c1;
on c2
c2;
on c1 -||
keine Verklemmung
1
2
4
||- E1
x1
|| E2
x2
!
?
!
?
c1;
c2
c2;
c1 -||
Verklemmung
Auch bei Regelung, wie viele der parallel ablaufenden Programme einen Kanal
nutzen dürfen, können wir unterschiedliche Festlegungen treffen.
(1) Pro Kanal gibt es genau ein Programm, das sendet, und ein Programm, das
empfängt (exklusive Nutzung des Kanals).
kein Nichtdeterminismus
(2) Mehrere Sender:
||- send 1 on c || send 2 on c || receive x on c -||
Empfänger kann zuerst 1 oder zuerst 2 empfangen.
Nichtdeterminismus
(3) Mehrere Empfänger:
||- send 1 on c || receive x on c || receive y on c -||
Einer der Empfänger erhalt die Nachricht — welcher, ist nicht festgelegt.
Nichtdeterminismus
Fazit: Beim asynchronen wie beim synchronen Austausch von Nachrichten entsteht Nichtdeterminismus, wenn parallel gesendet oder parallel empfangen wird.
Beim asynchronen Nachrichtenaustausch findet ein Informationsfluss nur in eine Richtung statt — vom Sender zum Empfänger. Anders beim synchronen Nachrichtenaustausch: Die Tatsache, dass eine Nachricht synchron übergeben wurde,
liefert dem Sender die Information, dass der Empfänger eine entsprechende Anweisung ausgeführt hat. Im asynchronen Fall ist diese Form der Rückkopplung durch
Rückkanäle zu übermitteln.
Beispiel:
1
2
4
6
Asynchrone Kommunikation mit Rückkopplung
sort ack = {ack};
proc sender = (channel m c, channel ack a):
|- var m z = z0;
var ack y = ack;
while not B(z) do
1.3. PROGRAMMIERSPRACHEN ZUR BESCHREIBUNG. . .
c
Sender
a
191
c’
Medium
Empfaenger
a’
Abbildung 1.10: Entkopplung von Sender und Empfänger durch ein Medium
Sender
Medium
ack on a
m on c
Empfaenger
ack on a’
m on c’
Abbildung 1.11: Message Sequence Chart
8
10
12
14
16
18
20
producenext(z);
receive y on a;
send z on c
od
-|
proc receiver = (channel m c, channel ack a):
|- var m x := z0;
while not B(x) do
send ack on a;
receive x on c;
consume(x)
od
-|
channel m c, channel ack a;
||- sender (c, a) || receiver (c, a) -||
Frage: Wie können wir Sender und Empfänger besser entkoppeln?
1
2
4
6
8
proc medium = (channel m c, c’, channel ack a, a’):
|- var m t := t0;
var ack test = ack;
while true do
send ack on a;
||- receive test on a || receive t on c -||;
send t on c’
od
-|
Frage: Wie muss das Medium formuliert werden, um Sender und Empfänger
noch stärker zu entkoppeln?
Diese Muster der Zusammenarbeit können anschaulich in Interaktionsdiagrammen
(Message Sequence Charts, MSC) beschrieben werden (s. Abb. (1.11)).
Zusammenarbeit:
||- sender (c, a) || medium (c, c’, a, a’) || receiver (c’, a’) -||
Auch das Steuern und Regeln von Systemen (Stichwort: eingebettete Systeme) kann über Nachrichtenaustausch modelliert werden.
192
KAPITEL 1. PROZESSE, INTERAKTION, KOORDINATION . . .
Crashsensor
Speedsensor
c
s
a
Airbag−
Controller
p
Zuend−
mechanismus
Panel
Abbildung 1.12: Airbagsteuerung
Beispiel:
1
2
4
6
8
10
12
14
16
18
20
22
Airbagsteuerung (ohne Echtzeitbetrachtung)
sort crashsignal = {ok, crash, error};
proc airbagcontroller = (channel crashsignal c, p, a,
channel nat s):
|- var nat speed := 0;
var bool on := true;
var crashsignal cs := ok;
while on do
||- receive speed on s || receive cs on c -||;
if cs = ok then
send ok on p
elif cs = crash then
if speed > 20 then
send crash on a
else
send crash on p;
on := false
fi
else
send error on p;
on := false
fi
od
-|
Obwohl in diesem Programm das wichtige Thema Echtzeit ausgespart ist, wird die
Logik des Auslöseverhaltens doch sichtbar.
Mit den soweit verfügbaren Sprachmitteln können wir nicht alternativ auf Nachrichten von unterschiedlichen Kanälen warten. Dafür benötigen wir ein weiteres
Sprachmittel.
Dieses Konzept der Guarded Communication Commands geht zurück auf die
Guarded Commands von Edsger Dijkstra (+ 2002) und CSP von Tony Hoare.
do higcomi od
if higcomi fi
hgcci ::=do
od|if
higcomi ::= hgcomi { hgcomi}∗
hgcomi ::= hexpi : send hexpi on hidi then hstatementi
| hexpi : receive hidi on hidi then hstatementi
| hexpi : hexpi! hidi then hstatementi
| hexpi : hidi? hidi then hstatementi
Beispiel:
1
Nachrichtenweiche
sort weiche = {links, rechts, alle};
1.3. PROGRAMMIERSPRACHEN ZUR BESCHREIBUNG. . .
2
4
6
8
193
var weiche w := alle;
var m v := ...;
do
w <> links: receive v on y then send v on r
[] w <> rechts: receive v on x then send v on r
[] true: receive w on z then nop
od
1.3.3
Nachrichtenaustausch über gemeinsame Variablen
Tritt in parallel ablaufenden Programmen eine Variable in einem Programm auf
der linken Seite einer Zuweisung (wird geschrieben) und in mindestens einem
weiteren Programm auf (gelesen oder geschrieben), dann sprechen wir von einer
gemeinsamen Variablen.
Bei gemeinsamen Variablen ist es entscheidend, festzulegen, welche Anweisungen
atomar (in einem Stück, ununterbrochen, unteilbar) ausgeführt werden, da
die Verzahnung der Anweisungen Auswirkungen auf das Ergebnis hat. Darüber hinaus wollen wir die Ausführung bestimmter Anweisungen zurückstellen, bis gewisse
Bedingungen gelten.
Diese Forderungen erreichen wir durch folgendes Sprachkonstrukt:
await E then S endwait (bewachter kritischer Bereich)
mit:
E boolescher Ausdruck, Wächter
S Anweisung, kritischer Bereich
Beispiel:
1
2
var nat x, y := 0, 0;
||- await y > 0 then x := x+1 endwait
|| await true then y := y+1 endwait -||
Hier ist y gemeinsame Programmvariable.
Die Anweisung wird wie folgt ausgeführt: Sobald die Bedingung E wahr ist, wird
die Anweisung S als unteilbare Aktion ausgeführt. Falls E nicht wahr ist (dabei
erfolgt auch die Auswertung von E als atomarer Schritt), wird gewartet, bis E zutrifft.
Achtung:
• Die Auswertung von E auf wahr und die Ausführung von S wird ebenfalls als
unteilbare Aktion gesehen. ⇒ wenn mit der Ausführung von S begonnen wird,
ist E sicher wahr.
• Falls E nie wahr wird, ist die Ausführung des Programms für immer blockiert.
Beispiel:
1
2
4
6
8
Parallele Berechung der Fakultät
var bool taken, var nat y, x, z := true, 1, n, 0;
||- while n > 0 do
await taken then taken, z := false, n endwait;
n := n-1
od
|| while x > 1 do
await not taken then taken, x := true, z endwait;
y := y*x
od
-||
194
KAPITEL 1. PROZESSE, INTERAKTION, KOORDINATION . . .
Gemeinsame Variablen (shared variables): taken, z
Wichtig:
• Hier kommen gemeinsame Variablen nur innerhalb von await – Anweisungen
vor.
• Die Feststellung, ob eine Variable als gemeinsame Variable benutzt wird, ist
syntaktisch überprüfbar.
• Ob alle gemeinsamen Variablen nur innerhalb von await – Anweisungen auftreten, ist auch syntaktisch überprüfbar.
Auch wenn der Zugriff auf gemeinsame Variablen durch await geschützt wird,
kann Nichtdeterminismus auftreten:
1
2
4
6
8
10
12
var bool taken, var nat x, y := false, 0, 0;
||- await true then
if not taken then
taken := true;
x := 1
fi
endwait
|| await true then
if not taken then
taken, y := true, 1
fi
endwait
-||
Nachbedingung: {taken ∧ ((x = 1 ∧ y = 0) ∨ (x = 0 ∧ y = 1))}
Zusammenhang zum Nachrichtenaustausch: Asynchroner Nachrichtenaustausch
kann durch eine Warteschlange als gemeinsame Variable in Programmen modelliert
werden.
channel m c
steht für var queue m c := emptyqueue
send E on c
steht für await true then c := stock(c, E) endwait
receive v on c steht für await c <> emptyqueue then
v, c := first(c), rest(c) endwait
Eines der frühen Mittel zur Synchronisation von parallel ablaufenden Programmen
sind Semaphore. Ein ganzzahliges (genau natürlichzahliges) Semaphor entspricht
einer Variablen der Sorte nat, die nur in einer genau festgelegen Weise verändert
und gelesen werden darf.
Deklaration eines Semaphors: sema nat x := n;
Operationen:
1
2
proc P = (sema nat x):
await x > 0 then x := x-1 endwait
4
proc V = (sema nat x):
await true then x := x+1 endwait
Beispiel:
1
2
4
6
Erzeuger / Verbraucher durch Semaphore
sema nat s1, s2 := 0, 1;
var m x, y, z := x0, y0, z0;
||- while not B(x) do
P(s1);
x := y;
V(s2);
1.3. PROGRAMMIERSPRACHEN ZUR BESCHREIBUNG. . .
195
p0
p1
p4
p2
p3
Abbildung 1.13: Fünf Philosophen
8
||
10
12
14
consume(x)
od
while not B(z) do
producenext(z);
P(s2);
y := z;
V(s1);
od
-||
Gemeinsame Variablen: s1, s2, y
Jetzt ist die Frage, ob der Zugriff auf gemeinsame Variablen geschützt ausgeführt wird, keine syntaktische Eigenschaft mehr, sondern Teil der Logik des Programms.
Boolesche Semaphore:
1
2
4
6
sema bool s := b;
proc P = (sema bool s):
await s then s := false endwait
proc V = (sema bool s):
await true then s := true endwait
Beispiel: Problem der fünf dinierenden Philosophen (englisch: dining philosophs)
Jeder Philosoph braucht zwei Gabeln, um zu essen. Jeder Philosoph isst und denkt
abwechselnd.
1. Lösung: Wir verwenden await
1
2
var array [0:4] bool gabeln;
gabeln[0], ..., gabeln[4] := true, ..., true;
4
proc philo = (nat i):
while true do
think;
await gabeln[i] and gabeln[i+1 mod 5] then
gabeln[i], gabeln[i+1 mod 5] := false, false
endwait;
eat;
await true then
gabeln[i], gabeln[i+1 mod 5] := true, true
endwait
6
8
10
12
196
14
16
KAPITEL 1. PROZESSE, INTERAKTION, KOORDINATION . . .
od
||- philo(0) || ... || philo(4) -||
Ziel des parallelen Programms zur Synchronisation der Philosophen:
• Deadlock (zyklische Wartezustände) vermeiden!
• Starvation vermeiden! Kein Programm
kommt.
verhungert, indem es nie zum Zug
• Sind die Synchronisationsmittel fair?
Schwache Fairness: Wenn jemand unbeschränkt lange wartet und die Fortsetzungsbedingung immer wahr ist, dann kommt er schließlich zum Zug.
Starke Fairness: Wenn jemand unbeschränkt lange wartet und die Fortsetzungsbedingung immer wieder wahr wird, dann kommt er schließlich zum
Zug.
• Kritischer Bereich wird geschützt.
Stehen nur Semaphore zur Verfügung (kein await), so wird es schwierig, die Wartebedingung auszudrücken.
2. Lösung (1 Semaphor)
1
2
4
6
8
10
12
14
16
18
sema nat s := 1;
proc philo = (nat i):
|- var bool ready := false;
while true do
think;
ready := false;
while not ready do
P(s);
if gabeln[i] and gabeln[i+1 mod 5] then
gabeln[i], gabeln[i+1 mod 5] := false, false;
ready := true
fi;
V(s)
od;
eat;
P(s);
g[i], g[i+1 mod 5] := true, true;
od
-|
Lösung ohne
1
2
4
6
8
10
busy wait: Jede Gabel entspricht einem Semaphor.
array[0:4] sema nat g;
g[0], ..., g[4] := 1, ..., 1;
proc philo = (nat i):
|- while true do
think;
P(g[i]);
P(g[i+1 mod 5]);
eat;
V(g[i]);
V(g[i+1 mod 5]);
od
-|
1.3. PROGRAMMIERSPRACHEN ZUR BESCHREIBUNG. . .
197
Achtung: Deadlock, falls alle Philosophen gleichzeitig die linke Gabel aufnehmen!
Lösungsidee: Bei Philosoph 4 die Anweisungen P(g[0]) und P(g[4]) vertauschen.
Beobachtungen:
(1) Gabel 4 ist frei oder einer der Philosophen 3 oder 4 hat beide Gabeln.
(2) Gabel 3 ist frei oder Philosoph 2 hat beide Gabeln oder Philosoph 3 hat sie.
Konsequenz: Einer der Philosophen ist immer essbereit oder die Gabeln für ihn sind
frei. ⇒ kein Deadlock!
(Präzise Argumentation: über Invarianten)
3. Lösung: Jeder hungrige Philosoph trägt sich in eine Warteschlange ein.
1
2
4
6
8
10
12
14
16
18
20
22
24
26
28
30
32
34
array[0:4] sema nat k;
k[0], ..., k[4] := 0, ..., 0;
var queue nat q := emptyqueue;
proc philo = (nat i):
|- while true do
think;
P(s);
q := stock (q, i);
V(s);
P(k[i]);
P(s);
g[i], g[i+1 mod 5] := false, false;
V(s);
eat;
P(s);
g[i], g[i+1 mod 5] := true, true;
V(s);
od
-|
proc manage =:
|- var bool busy = false;
var nat i := 0;
while true do
P(s);
if q <> emptyqueue then
i, q, busy := first(q), rest(q), true
fi;
V(s);
P(s);
if g[i] and g[i+1 mod 5] then
V(h[i]);
busy := false;
fi
V(s)
od
-|
Frage: Behindern sich die Philosophen mehr als unbedingt erforderlich?
Ein Sprachelement, das die synchronisierte Arbeitsweise mit gemeinsamen Variablen regelt (ohne Semaphoren oder await) sind Monitore (nach C.A.R. Hoare,
Per Brinch-Hansen).
Idee: In einem Monitor werden wie in einem Objekt in der objektorientierten Programmierung gemeinsame Variablen gekoppelt. Zugriff auf gekapselte Variablen erfolgt nur über Prozeduren (in der Objektorientierung Methoden).
198
KAPITEL 1. PROZESSE, INTERAKTION, KOORDINATION . . .
Regel: Jeweils eine Prozedur kann gleichzeitig ausgeführt werden. Es wird jeweils
höchstens ein Aufruf ausgeführt, bis er regulär terminiert oder in einen Wartezustand geht. Wartezustände werden (ähnlich wie P und V bei Semaphoren) über
Operationen auf sogenannten Signalen erreicht.
Beispiel:
1
2
monitor EV =
|- var queue m q := emptyqueue;
signal nonempty;
4
proc send = (m x):
|- q := stock (q, x);
nonempty.signal
-|
6
8
10
12
proc receive = (var m x):
|- if q = emptyqueue then
nonempty.wait
fi
x, q := first(q), rest(q) -|
14
16
18
20
22
24
-|
var m x, z := x0, z0;
||- while not B(x) do
EV.receive(x);
consume(x);
od
|| while not B(z) do
producenext(z);
EV.send(z)
od
-||
Beobachtungen: Alle gemeinsamen Variablen sind im Monitor gekapselt! Koordination über Signale; ein Signal entspricht einer booleschen Variablen, vorbesetzt mit
false.
nonempty.signal steht für nonempty := true.
nonempty.wait steht für
1
2
nonempty := false;
await nonempty then nop endwait
Hinweis: Monitore dienen für Java als Vorlage für Nebenläufigkeit.
1.3.4
Sprachmittel zum Erzeugen paralleler Abläufe
Bisher haben wir Parallelität explizit über ||- p1 || ... || pn -|| eingeführt.
Daneben gibt es noch ein weiteres Mittel zur Einführung von Parallelität: Das Starten einer weiteren, parallel ablaufenden Aktivität durch einen Prozeduraufruf.
(s. Abb. (1.14))
Fork: Aufspalten von einer Aktivität in zwei parallele.
Join: Aufsammeln zweier Aktivitäten in eine.
Beispiel: Die Anweisung ||- s1 || s2 || ... || sn -|| kann wie in Abbildung (1.15) verstanden werden.
1.3. PROGRAMMIERSPRACHEN ZUR BESCHREIBUNG. . .
(a)
fork
199
(b)
join
Abbildung 1.14: Parallele Abläufe durch fork und join
s1
Abbildung 1.15:
s2
...
sn
Parallele Anweisung durch fork und join
Varianten von fork und join finden sich in zahlreichen (modernen) Programmiersprachen (bzw. deren Bibliotheken). Motivation für die explizite Erzeugung
paralleler Prozesse:
• Erzeugung von Familien paralleler Abläufe
• Möglichkeit der Identifikation verschiedener Prozesse
Hinweis: Mit ||- p1 || ... || pn -|| werden n anonyme Aktivitäten erzeugt, welche die Programme p1 bis pn ausführen.
• Ausführung von Operationen auf Prozessen via deren Identifikator, z.B. Anhalten, Fortsetzen, Beenden und Priorisierung
• Modularisierung paralleler Abläufe
Sprachmittel:
• process x deklariert den Bezeichner x, mit dem ein Programmablauf identifiziert werden kann.
• start p name x erzeugt einen Prozess entsprechend der durch p gegebenen
Anweisung und weist x eine Referenz auf diesen neuen Prozess zu.
• terminated x: boolescher Ausdruck; true: der durch x bezeichnete Prozess
ist beendet; false: der durch x bezeichnete Prozess läuft.
• terminate x: der durch x bezeichnete Prozess und alle durch ihn initiierten
Prozesse werden beendet.
1
2
4
6
process x;
proc p =: ...;
...
start p name x;
...
if terminated x then
200
8
10
KAPITEL 1. PROZESSE, INTERAKTION, KOORDINATION . . .
...
else
terminate x
fi
Die Ausführung von start (bzw. fork) ähnelst der Ausführung einer Prozedur. Daraus resultieren häufig Missverständnisse, da zwischen den Ereignessen des erzeugten
und denen des erzeugenden Prozesses - im Gegensatz zu Prozeduren - bereits bei
der Initiierung der start – Operation kein kausaler Zusammenhang besteht (sofern
nicht zusätzliche Synchronisationskonzepte eingesetzt werden).
Beispiel: Sei x1 ein Prozess, der mit start p name x2 einen zweien Prozess x2
erzeugt. Unmittelbar nach der Ausführung von start p name x2 ist es möglich,
dass x2:
• noch nicht mit der Ausführung von p begonnen hat
• p ausführt
• bereits beendet ist
Erläuterung der verschiedenen Konzepte zur Spezifikation von Parallelität:
Beispiel: Binäre Suche in einem Feld
Gegeben sei ein Feld von n natürlichen Zahlen:
var array[1:n] nat a
Zu entwickeln: proc bsuche = (int x, int low, int high, var bool b)
1
2
4
6
8
10
12
14
proc bsuche = (int x, int low, int high, var bool b):
|- var int m := (low + high) / 2;
if low <= high then
if a[m] = x then
b := true
else
var bool bl, bh;
||- bsuche (x, low, m-1, hl)
|| bsuche (x, m+1, high, bh) -||;
b := bl or bh
fi
else
b := false
fi
-|
Analyse: Kaskade von maximal 2n − 1 parallel ablaufenden Prozessen bsuche.
b enthält das Resultat, nachdem alle Aufrufe terminiert sind.
Ineffizient: Es wird weitergesucht, selbst wenn die gesuchte Zahl bereits gefunden
wurde.
Abhilfe: Austausch von Nachrichten, ob die Zahl bereits gefunden ist, mit gemeinsamer Programmvariable b.
1
2
4
proc bsuche = (int x, int low, int high, var bool b):
|- var int m := (low + high) / 2;
if low <= high then
if a[m] = x then
await true then b := true endwait
1.3. PROGRAMMIERSPRACHEN ZUR BESCHREIBUNG. . .
6
201
else
var bool bn;
await true then bn := b endwait;
if not bn then
||- bsuche (x, low, m-1, b)
|| bsuche (x, m+1, high, b) -||
fi
fi
fi
-|
8
10
12
14
Analyse: Es werden keine neuen Prozesse mehr erzeugt, sobald einer der Prozesse
x gefunden hat.
Alternative ohne gemeinsame Programmvariable mittels terminate:
1
2
4
6
8
10
12
14
16
18
20
proc bsuche = (int x, int low, int high, var bool b):
|- var int m := (low + high) / 2;
if low <= high then
if a[m] <> x then
process pl, ph;
var bool bl, bh := false, false;
start bsuche (x, low, m-1, bl) name pl;
start bsuche (x, m+1, high, bh) name ph;
await bl or bh or (terminated pl and terminated ph) then
b := bl or bh
endwait;
if not terminated pl then
terminate pl
fi
if not terminated ph then
terminate ph
fi
else
b := true
fi
fi
-|
Erzeugung eines Baumes von Prozessen. Findet ein Kind x, so wird das Ergebnis
nach oben propagiert. Alle noch nicht beendeten Prozesse werden dann zwangsweise
terminiert.
Bemerkungen: Es gibt eine Vielzahl von Beschreibungstechniken für Systeme
parallel ablaufender Prozesse, die sich im wesentlichen auf die bisher beschriebenen
Konstrukte zurückführen lassen.
Für parallele Programmsysteme ist der Nachweis von Eigenschaften und die Sicherstellung der Korrektheit besonders schwierig. Auf die Beschreibung quantitativer Zeitaspekte wurde hier verzichtet. Zeitkritische Programme erfordern weitere
Konzepte und werden in der Echtzeitverarbeitung zur Steuerung realer Prozesse
(reaktive Systeme) verwendet.
1.3.5
Nebenläufigkeit in Java
Java bietet mit einem Threadkonzept eine technische Realisierung für parallele
Aktivitäten an. Unter einem Thread (= Ausführungsfaden) versteht man eine eigenständige Aktivität in einem Rechensystem, die eine Folge von Anweisungen
ausführt. Jedes sequentielle Programm kann als Ausführung eines Threads verstanden werden.
202
KAPITEL 1. PROZESSE, INTERAKTION, KOORDINATION . . .
new Thread()
start
running
yield
runnable
I/O, sync, ...
not running
run terminiert
dead
Abbildung 1.16: Zustände eines Threads
Thread-Erzeugung in Java
In Java ist ein Thread stets ein Objekt der Klasse Thread oder einer Unterklasse
hiervon.
Sei MyThread eine Unterklasse von Thread. Ein Objekt m der Klasse MyThread wird
mit
1
2
MyThread m;
// vereinbart, mit
m = new MyThread(); // instantiiert und mit
m.start();
// gestartet
Durch die ererbte Methode start() wird eine neue Aktivität p2 gestartet, die parallel zur Aktivität p1, die start() aufgerufen hat, abläuft. In dieser neuen Aktivität
p2 wird die Methode run() des Objektes m ausgeführt. Der Thread ist so lange lebendig, bis die Methode run() endet. Daraus geregeben sich für einen Thread die
Zustände gemäß Abbildung (1.16).
Die Methode run() der Klasse Thread führt lediglich nop aus und kehrt zurück,
d.h. für jeden nichttrivialen Thread muss eine Unterklasse der Klasse Thread erstellt
und run() überschrieben werden.
Weitere Operationen:
• m.suspend() unterbricht den Thread und versetzt ihn in einen Wartezustand
• m.resume(): fortsetzen aus dem Wartezustand
• m.join(): warten auf das Terminieren des Threads
• m.stop(): bricht den Thread ab. Achtung: nichtdeterministischer Zwischenzustand am Ende!
Synchronisation in Java
Soweit können mehrere Threads (parallel ablaufende Aktivitäten) nebeneinander
in einem Objekt aktiv sein. Dabei ist in der Regel die Bernsteinbedingung verletzt
und es können schwer beherrschbare Effekte entstehen. Abhilfe schafft eine Synchronisation der Methoden. Dort gilt für die Ausführung der so gekennzeichneten
Methoden das Monitorprinzip:
public synchronized void method (...) ...
um festzulegen, dass die Methode synchronisiert ausgeführt wird, d.h. während
der Ausführung der Methode durch einen Thread (die Ausführung umfasst auch
Methodenunteraufrufe!) darf kein anderer Thread in dem Objekt aktiv sein.
Bemerkung: Zusätzlich kann man durch synchronized(x) ... ein beliebiges
Objekt x für andere Threads sperren.
1.3. PROGRAMMIERSPRACHEN ZUR BESCHREIBUNG. . .
203
Typisch für Monitore stehen folgende Signaloperationen zur Verfügung:
• x.wait(): Anhalten eines Threads und Entblockieren der Synchronisation
• x.notify(): Aufwecken eines Threads, der auf ein Signal wartet
• x.notifyall(): Aufwecken aller Threads, die auf ein Signal warten
204
KAPITEL 1. PROZESSE, INTERAKTION, KOORDINATION . . .
Kapitel 2
Betriebssysteme und
systemnahe Programmierung
Rechenanlage: Hardware, Gesamtheit aller Geräte:
• Rechnerkern
• Busse
• Speicher
• Peripherie
• Kommunikationseinrichtungen
• ...
Wir erhalten ein maschinennahes Programmierkonzept (Maschinensprache).
Um einen Rechner für Anwender bequemer nutzbar zu machen, verwendet man
eine Familie von Programmen (Systemsoftware), die die Nutzung erleichtern. Wir
sprechen auch von dem Betriebssystem. Dazu zählen Anwendungsprogramme in der
Regel nicht.
Der Nutzer und der Programmierer greifen über die vom Betriebssystem zur
Verfügung gestellten Funktionen auf den Rechner zu.
Ein Betriebssystem realisiert eine Nutzer- und Programmierschnittstelle. Dabei
nutzt das Betriebssystem die Hardware – Schnittstelle der Rechenanlage. Die Hardware kann als eine Familie von Betriebsmitteln gesehen werden (Speicher, Rechenkapazität, Übertragungskapazität, Druckmöglichkeiten etc.).
Das Betriebssystem unterstützt den Nutzer beim Zugriff auf diese Betriebsmittel. Dabei nutzt das Betriebssystem selbst die Betriebsmittel für seine eigenen Zwecke. Das Betriebssystem ist also stets durch zwei Schnittstellen gekennzeichnet:
➥ Hardware – Schnittstelle
➥ Nutzerschnittstelle
Wir sprechen deshalb auch von einer Betriebssystem – Schicht (s. Abb. 2.1).
Wichtige Aspekte eines Betriebssystems:
• Nutzerschnittstelle und Leistungsumfang
• Vorgesehene Hardware – Schnittstelle
• Aufbau, Struktur und Realisierung des Betriebssystems
205
206KAPITEL 2. BETRIEBSSYSTEME, SYSTEMNAHE PROGRAMMIERUNG
Nutzerschnittstelle
Betriebssystem
Hardware
Abbildung 2.1: Betriebssystem – Schicht
Die Entwicklung eines Betriebssystems ist eine Software-Entwicklungsaufgabe. Die
Programmier – Teilaufgaben sind in der Regel aus dem Bereich der Systemprogrammierung.
Aufgaben der Systemprogrammierung sind dadurch gekennzeichnet, dass:
• technische Eigenschaften der Hardware eine (oft dominierende) Rolle spielen
• Echtzeit (Zeitverhalten) von Bedeutung ist
• maschinennahe Sprachelemente verwendet werden
• Effizienz oft entscheidend ist
2.1
Grundlegende Betriebssystem – Aspekte
Ein Betriebssystem hat bestimmte Aufgaben zu erfüllen und bestimmte Funktionen
(Dienste) anzubieten. Welche Aufgaben und Dienste im Vordergrund stehen, hängt
stark von der Betriebssituation der Rechenanlage ab.
2.1.1
Aufgaben eines Betriebssystems
Betriebsmittel: alle Hardware- und Softwareanteile eines Rechensystems.
Betriebssystem: Familie von Programmen zur Verwaltung der Betriebsmittel und
zur Unterstützung des Nutzers beim bequemen Zugriff.
Wichtige Teilaufgabe des Betriebssystems: Steuerung und Überwachung der Ausführung von Anwendungsprogrammen. Dabei ist eine wesentliche Frage, wie viele
Programme gleichzeitig (nebeneinander) zur Ausführung kommen.
Aufgaben des Betriebssystems:
• Verwaltung der Betriebsmittel für die Ausführung von Anwendungsprogrammen (Mittel: Prozessor, Hauptspeicher)
• langfristige Datenerhaltung, Ablage und Zugriff auf Datenträger
• Nutzung von Ein-/Ausgabegeräten (Bildschirm, Tastatur, Maus, Drucker etc.)
einschließlich Datenfernübertragungseinrichtungen und Sensorik/Aktuatorik.
Zweiter Aufgabenbereich:
• Benutzerverwaltung
– Benutzeridentifikation: Im System ist die Unterscheidung von Benutzern
vorgesehen.
– Benutzerauthentifikation: Überprüfung, ob der Benutzer der ist, der er
vorgibt zu sein (Passwörter, Biometrie, Zugangsschlüssel, . . . ).
2.1. GRUNDLEGENDE BETRIEBSSYSTEM – ASPEKTE
207
– Authorisierung, Benutzerrechte: Jeder Nutzer hat gewisse Nutzungsrechte, deren Einhaltung vom Betriebssystem überprüft wird.
• Prozessverwaltung
Sind mehrere Programme im System gleichzeitig bereit zur Ausführung, so
muss das Betriebssystem regeln, welches Programm als nächstes an die Reihe
kommt und dabei sicherstellen, dass alle benötigten Betriebsmittel verfügbar
sind.
Achtung: Im Zusammenhang mit Betriebssystemen verwenden wir den Begriff
Prozess in einer leicht anderen Bedeutung:
• Prozess im Betriebssystem: Programm in Ausführung im Rechner
• Bisher: Familie von Ereignissen / Aktionen mit Kausalitätsrelation
• Zusammenhang: Jedes Programm wird ausgeführt, indem eine Menge von
Ereignissen / Aktionen stattfinden mit einer Kausalitätsrelation, die sich aus
der Logik des Programms ergibt.
Für das Betriebssystem gibt es eine Fülle von Informationen über den Betriebszustand eines Rechensystems, die das Betriebssystem für die Erfüllung seiner Aufgaben benötigt:
• Benutzerdaten
• Daten für langfristige Datenerhaltung
• Daten über Hardware / Geräte
• aktuelle Betriebsdaten:
– Zustand des Prozessors und der Prozesse
– Zustand und Aufteilung des Hauptspeichers
– Belegung der Geräte (Geräteverwaltung)
Die Aufgaben eines Betriebssystems hängen eng von der Betriebsart ab:
➥ Anzahl der interaktiven Nutzer:
• Einplatzsysteme
• Mehrplatzsysteme
➥ eigentliche Betriebsart:
• Stapelbetrieb
• Dialogbetrieb
• Prozesssteuerbetrieb
➥ Anzahl der Prozesse im System:
• Einprogrammbetrieb
• Mehrprogrammbetrieb
Mehrprogrammbetrieb führt in der Regel auf Multiplexbetrieb: Mehrere Programme
werden zeitlich verzahnt ausgeführt. Die Organisation eines Betriebssystems hängt
natürlich stark von der Hardware – Struktur ab. Prinzipielle Unterschiede ergeben
sich aus der Zahl der verfügbaren Prozessoren:
208KAPITEL 2. BETRIEBSSYSTEME, SYSTEMNAHE PROGRAMMIERUNG
• Einprozessorsystem
• Mehrprozessorsystem
Auch in Einprozessorsystemen existieren über Gerätetreiber oft weitere, (eingeschränkt) programmierbare Hardwareeinheiten. Dadurch liegt nicht nur bei Mehrprozessorsystemen, sondern auch bei Einprozessorsystemen ein gewisser Grad echter Parallelität vor.
Beispiel: Algorithmus des Bankiers
Sinn: Problematik der Betriebsmittelvergabe erläutern.
Betrachtete Problemsituation: Eine Bank B hat n Kunden. Jeder Kunde k besitzt
eine maximale Kreditzusage mk und einen augenblicklichen Kredit ak . Es existieren
folgende Vereinbarungen zwischen Bank und Kunden (Kontrakt):
(1) Garantie der Bank für jeden Kunden k: Falls k mehr Kredit benötigt, wird
die Bank im Rahmen der Kreditzusage den Kredit irgendwann auszuzahlen.
(2) Garantie des Kunden: Der Kunde zahlt seinen Kredit, nachdem er ihn bis zu
einer bestimmten Höhe (maximal mk ) in Anspruch genommen hat, irgendwann zurück.
Achtung: Die Garantien bestimmen sich wechselseitig.
Nebenforderung: Die Bank sollte Anfragen nur zurückweisen, wenn das zwingend
erforderlich ist, um die Garantie der Bank einhalten zu können.
Frage: Wann ist ein Zustand der Bank sicher?
Zustand der Bank:
K
Menge der Kunden
mk Kreditlimit des Kunden k ∈ K
ak
aktueller Kredit des Kunden k, ak ≤ mk
r
Restkapital der Bank
Definition sicher: Der Zustand der Bank ist für die Menge K sicher, wenn gilt:
Es existiert ein Kunde k ∈ K:
(1) k kann ausgezahlt werden: mk − ak ≤ r
(2) Die Stellung mit K \ {k} Kunden und Restkapital r + ak ist sicher.
Beobachtung: Die Bank ist genau dann sicher, wenn es eine Reihenfolge für die
Bedienung der Kunden gibt (scheduling), die funktioniert. Die Bank kann (in
einer sicheren Stellung) eine Anfrage eines Kunden nach mehr Kreditauszahlung
ohne Probleme sofort befriedigen, falls die dadurch entstehende Situation für die
Bank wieder sicher ist. Falls die Bank unvorsichtig ist, kann sie in eine unsichere
Stellung geraten. Eine unsichere Stellung für die Bank nennen wir auch instabil.
Falls das Verhalten der Kunden das erlaubt, kann die Bank aus einer unsicheren
Stellung in einen sicheren Zustand kommen (sich stabilisieren).
In einem unsicheren Zustand können die Kunden die Bank in eine Verklemmung
führen (d.h. zu einem Nichteinhalten der Garantie).
Moral von der Geschichte: Das Betriebssystem muss die Betriebsmittelvergabe so
vornehmen, dass Verklemmungen (unsichere, instabile Zustände) vermieden werden.
Dabei ist aber gleichzeitig dafür zu sorgen, dass die Betriebsmittel im Sinne der
Anforderungen optimal ausgenutzt werden.
Die Qualität eines Betriebssystems zeigt sich insbesondere unter Überlast.
Hauptziele eines Betriebssystems:
• Zuteilung der Betriebsmittel unter optimaler Befriedigung der Nutzanforderungen und Maximierung des Durchsatzes (Anzahl der gerechneten Programme / Zeiteinheit)
2.1. GRUNDLEGENDE BETRIEBSSYSTEM – ASPEKTE
209
• Geschickte Verlagerung von Programmen und Daten zwischen Haupt- und
Hintergrundspeicher
• Steuerung und optimale Ausnutzung der E/A – Geräte
2.1.2
Betriebsarten
(a) Stapelbetrieb:
Es werden Ströme von Auftragspaketen verarbeitet. Dabei müssen alle Bestandteile eines Auftragspakets vollständig beschrieben sein, damit dieses eigenständig (ohne Rückfrage) bearbeitet werden kann. Nachfragen unter der
Bearbeitung sind nicht vorgesehen. Dabei sind die Aufträge sorgfältig zu planen, damit keine Zwischenfehler mit Folgeproblemen auftreten.
Die Auftragspakete sind wieder strukturiert in Unteraufträge (Unterabschnitte).
Abschnitt: Ausführung einer in einer Programmiersprache abgefassten Programmeinheit.
Die Sprache des Betriebssystems (Kontrollsprache) dient der Formulierung der
Auftragspakete. Ein- und Ausgabe erfolgt über Dateisysteme (file systems)
oder Datenbanken. Besonders wichtig beim Stapelbetrieb (bei dem typischerweise umfangreiche Aufträge mit langer Rechendauer bearbeitet werden) ist
der Durchsatz (Anzahl bearbeiteter Aufträge pro Zeiteinheit). Bei Stapelbetrieb fließt durch das System ein Abschnittsstrom.
(b) Dialogbetrieb:
Der Systemnutzer erteilt dem System Aufträge im Dialog und ein Teil der Ein/Ausgabe erfolgt auch im Dialog. Es findet eine Interaktion statt zwischen dem
Nutzer und dem ablaufenden Programm. Die Aufgabe des Betriebssystems ist
die Steuerung des Dialogs und der Ein-/Ausgabe aus den Programmen.
Beherrschend ist dabei die Kommandosprache des Betriebssystems (die unter
Umständen über graphische Nutzerschnittstellen angesprochen wird). Typisch
für Dialogsysteme: Der Nutzer bewegt sich auf unterschiedlichen Dialogebenen. Jede Dialogebene stellt eine bestimmte Auswahl von Dialogbefehlen zur
Verfügung. Ein Teil dieser Befehle dient dazu, die Ebenen zu wechseln, andere erlauben die Ausführung bestimmter Anweisungen innerhalb einer Ebene.
Dies führt auf komplexe Nutzerschnittstellen, für die gute Hilfefunktionen und
Benutzerführung wichtige Aufgaben sind.
(c) Prozesssteuerung:
Werden durch Rechensysteme technische oder organisatorische Prozesse überwacht und gesteuert, so sprechen wir von Prozesssteuerung (auch von eingebetteten Softwaresystemen und Echtzeitverarbeitung). 98% aller programmierbaren Rechner laufen eingebettet.
Beispiele: Robotik, Verkehrstechnik, Produktionstechnik, Hausgeräte, Medizintechnik, Sonderfall: Kommunikationstechnik
Eine Sonderstellung nehmen transaktionsorientierte Systeme ein. Dies sind typischerweise Datenbankanwendungen, bei denen eine große Anzahl von Nutzern gleichzeitig auf eine Datenbank zugreifen (z.B. Platzbuchungssysteme). Hierbei bilden in
der Regel mehrere Interaktionen eine Einheit (Transaktion) (z.B. Anfrage, ob
ein Platz verfügbar ist und zu welchem Preis und Buchung oder Reservierung).
Oft sind bei Systemen alle drei Betriebsarten vorzufinden.
Beispiel: Recheneinsatz im Krankenhaus
Ein Großrechner verarbeitet:
210KAPITEL 2. BETRIEBSSYSTEME, SYSTEMNAHE PROGRAMMIERUNG
Prozessor
Speicher
Kanal
Eingabegeraet
(Lochkartenleser)
Ausgabegeraet
(Drucker)
Abbildung 2.2: Einfaches Rechensystem
• im Stapelbetrieb: Verbuchung der Daten, Drucken von Rechnungen, . . .
• im Dialogbetrieb: Eingabe neuer Daten am Bildschirm (Krankenaufnahme)
• Prozesssteuerung: Überwachung von Daten in der Intensivstation
Dabei wird auf die unterschiedlichen Aufgaben unterschiedliche Priorität gelegt.
Weiteres Thema: Verteilte Rechensysteme, Rechnernetze
2.1.3
Ein einfaches Betriebssystem für den Stapelbetrieb
Wir betrachten exemplarisch das einfache Rechensystem aus Abbildung (2.2).
1
2
4
6
8
10
12
14
16
proc reader =:
|- var string lochkarte;
while true do
readnext (lochkarte);
await l_free then
l_register, l_free := lochkarte, false
endwait
od
-|
proc printer =:
|- var string register;
while true do
await not d_free then
d_free, register := true, d_register
endwait;
print (register)
od
-|
18
20
22
24
26
28
30
proc processor =:
|- var nat k, var string sr;
while p_active do
for k := AR to ER do
await not l_free then
l_free, sr := true, l_register
endwait;
sp[k] := sr
od
{fuehre Programm in Speicherabschnitt AR bis ER aus und
lege das Resultat im Speicherabschnitt von AW bis EW ab}
for k := AW to EW do
2.1. GRUNDLEGENDE BETRIEBSSYSTEM – ASPEKTE
32
34
36
38
40
sr := sp[k];
await d_free then
d_free, d_register := false, sr
endwait
od
od
211
-|
|- [0:n] array string sp;
var string d_register, l_register;
var bool d_free, l_free, p_active := true, true, true;
||- processor || reader || printer -||
-|
Diese Programme liefern uns ein einfaches Betriebssystem.
Nachteil: Das Betriebssystem ist ineffizient, da in der Regel die Geschwindigkeit der
Ausführung der Befehle für den Drucker und das Lesegerät viel kleiner ist als für
den Prozessor.
1
2
4
6
8
10
12
14
16
18
20
22
24
26
28
30
32
34
36
38
proc channel =:
|- var string s, sr;
var nat k, i, j;
while p_active do
await c_active then
s, k, i := job, aa, ea
endwait;
if s = "read" then
for j := k to i do
await not l_free then
l_free, sr := true, l_register
endwait;
sp[j] := sr
od
else
for j := k to i do
sr := sp[j];
await d_free then
d_free, d_register := false, sr
endwait
od
fi
await true then
c_active := false
endwait
od
-|
proc processor =:
|- while p_active do
await not c_active then
c_active, job, aa, ea := true, "read", AR, ER
endwait
await not c_active then
nop
endwait
{Rechne Programm im Speicherabschnitt AR bis ER}
await not c_active then
c_acitve, job, aa, ea := true, "write", AW, EW
212KAPITEL 2. BETRIEBSSYSTEME, SYSTEMNAHE PROGRAMMIERUNG
endwait
await not c_active then
nop
endwait
od
40
42
Auch die Version mit Auftragsverteilung an den Kanal bringt keinen wesentlichen
Vorteil, da der Prozessor auf das Ende des Lesevorgangs bzw. des Druckvorgangs
warten muss.
Idee: Wir arbeiten gleichzeitig mit mehreren Programmen.
2.1.4
Ein einfaches Betriebssystem für Multiplexbetrieb
Idee: Wir arbeiten gleichzeitig mit drei Programmen:
(1) druckt
(2) rechnet
Pipeline
(3) wird eingelesen
1
2
4
6
8
10
12
14
16
proc processor =:
|- var nat i := 0;
await not cr_active then
cr_active, ar, er := true, AR(0), ER(0)
endwait;
while p_active do
i := (i+1) mod 3;
await not cr_active then
cr_active, ar, er := true, AR(i), ER(i)
endwait;
i := (i-1) mod 3;
{Rechne Programm im Speicherabschnitt i}
await not cp_active then
cp_acitve, ap, ep := true, AR(i), ER(i)
endwait;
i := (i+1) mod 3
od
-|
18
34
proc input_channel =:
|- var nat k, i;
while p.active do
await cr_active then
k, i := ar, er
endwait;
for j := k to i do
await not l_free then
l_free, sr := true, l_register
endwait;
sp[j] := sr
od
await true then
cr_active := false
endwait
od
-|
36
proc output_channel =:
20
22
24
26
28
30
32
2.1. GRUNDLEGENDE BETRIEBSSYSTEM – ASPEKTE
213
Speicher
E−Kanal
Prozessor
Leser
Unterbrechung
A−Kanal
Schreiber
Abbildung 2.3: Betriebssystem mit Warteschlangen
38
40
42
44
46
48
50
|- var nat k, i;
while p.active do
await cp_active then
k, i := ap, ep
endwait;
for j := k to i do
sr := sp[j];
await d_free then
d_free, d_register := false, sr
endwait
od
await true then
cp_active := false
endwait
od
-|
Dieses einfache Betriebssystem arbeitet effizient, wenn der Zeitaufwand für das Lesen, Drucken und Rechnen eines Programms etwa gleich groß ist — wenn nicht, gibt
es wieder Wartezeiten.
Durch die Einführung von Warteschlangen erreichen wir, dass die Geräte (im
Prinzip) immer voll ausgelastet sind. Zur Verwaltung der Geräte und ihrer Warteschlange muss der Prozessor immer wieder seine Arbeit unterbrechen, um Aufträge
zu behandeln und zu erteilen (s. Abb. (2.3)).
Dabei haben wir zwei Aufgaben zu lösen (Scheduling des Betriebssystems):
➥ Organisation des Wechsels zwischen der Ausführung von Anwenderprogrammen und Verwaltungsaufgaben (Ausführung von Betriebssystemprogrammen)
➥ Auswahl der Reihenfolge der Ausführung der rechen-/ druck-/ lesewilligen
Programme
Auswahl der Reihenfolge der Ausführung:
• Warteschlangenorganisation (auch andere Prinzipien wie Stack etc.)
• Prioritäten (Prioritätswarteschlange): Bei der Wahl eines Programms für den
nächsten Ausführungsschritt wird jeweil das mit der höchsten Priorität gewählt.
– statische Priorität: Die Priorität eines Programms ändert sich während
seiner Abarbeitung nicht.
214KAPITEL 2. BETRIEBSSYSTEME, SYSTEMNAHE PROGRAMMIERUNG
– dynamische Priorität: Die Priorität eines Programms wird während der
Abarbeitung geändert.
Für die Festlegung, wann die Ausführung eines Programms unterbrochen wird, gibt
es zwei prinzipielle Möglichkeiten:
• Zeitscheibenverfahren: Es wird eine Zeitdauer festgelegt, über die der Rechner an einem Prozess arbeitet. Spätestens nach Ablauf dieser Zeitspanne (oder
nach Terminierung des Prozesses) wird die Abarbeitung unterbrochen und anstehende Verwaltungsaufgaben ausgeführt.
Kritisch: Wie groß die Zeitscheibe wählen? Bei zu großen Zeitscheiben werden
die Geräte nicht ausgelastet, bei zu kleinen Zeitscheiben fällt zu viel Verwaltungsaufwand an.
• Unterbrechungskonzept: Die Ausführung von Anwenderprogrammen wird unterbrochen, sobald die Verwaltungsaufgaben dies erfordern. Beispielsweise melden sich die Gerätetreiber / Kanäle durch Signale beim Rechnerkern zurück.
Dabei ist es sinnvoll, dass der Rechnerkern seine augenblickliche Abarbeitung
noch so lange fortsetzt, bis ein Zeitpunkt erreicht wird, an dem die Arbeit
später sinnvoll fortgesetzt werden kann. Bedeutet die Unterbrechung, dass die
augenblickliche Arbeit nie fortgesetzt werden muss, so kann auch ein schneller
Abbruch erfolgen.
(Zusammenhang: unteilbare Aktion ∼ ununterbrechbare Aktion) Für Unterbrechungen können ganz unterschiedliche Gründe vorliegen:
– äußere: Eingaben des Operators
– verwaltungstechnische: Geräte benötigen Aufträge
– innere: Das auszuführende Programm läuft in eine Fehlersituation
Das System läuft oft in unterschiedlichen Unterbrechungsmodi, d.h. gewisse
Unterbrechungssignale führen nicht auf eine Unterbrechung, wenn Aufgaben
mit höherer Priorität ausgeführt werden.
Das Scheduling bestimmt bei komplexen Rechensystemen, die im Merprogrammbetrieb arbeiten, weitgehend den Wirkungsgrad eines Systems (= Grad der Nutzung
des Systems im Verhältnis zwischen Aufgaben und technischer (Rechen-)Leistung).
Um den Wirkungsgrad eines Systems empirisch einzuschätzen, werden Leistungsmessungen durchgeführt. Wichtige Kennzahlen:
• Verweilzeit: Wie lange ist ein Antrag bis zu seiner Abarbeitung im System
(auch von Interesse: mittlere Verweilzeit)?
• Ankunftsrate: Wie viele neue Aufträge pro Zeiteinheit?
• Durchsatz: Anzahl der pro Zeiteinheit abgearbeiteten Aufträge
2.2
Benutzerrelevante Aspekte eines Betriebssystems
Die Benutzerschnittstelle eines Betriebssystems besteht im Wesentlichen in den
Funktionen, die das Betriebssystem dem Nutzer zur Verfügung stellt, und aus den
Effekten, die sich ergeben und vom Nutzer bemerkt werden.
Typische Dienste:
• Rechnerleistung
2.2. BENUTZERRELEVANTE ASPEKTE EINES BETRIEBSSYSTEMS
215
• Speicherung von Daten (langfristig)
• Übertragung von Information (Kommunikationsdienste) und Zugriff auf entfernte Informationen (Internet)
• Zugriff auf Geräte
2.2.1
Kommandosprache des Betriebssystems
Das Betriebssystem stellt dem Nutzer bestimmte Operationen zur Verfügung. Diese
werden über Kommandos ausgelöst. Im Betriebssystem gibt es einen Kommandointerpreter, d.h. eine Zustandsmaschine, die die Kommandos als Eingabe nimmt und
in Folge entsprechende Betriebssystemprozeduren aufruft.
2.2.2
Benutzerverwaltung
Bei Betriebssystemen mit einer Vielzahl von Benutzern wird durch das Betriebssystem die Nutzerverwaltung bereitgestellt. Jeder Nutzer kat Kenndaten:
• Benutzer – ID
• Passwort
• Rechte
– Rechenzeit
– Speicher
– besondere Dienste
– Prioritäten
– ...
Dabei werden auch Betriebsdaten über den Nutzer gesammelt (beispielsweise für
eine Abrechnung).
2.2.3
Zugriff auf Rechenleistung
Der Zugriff auf Rechenleistung findet in Mehrprogrammsystemen in Konkurrenz
mit anderen statt. Abhängig vom Betriebssystem kann der Nutzer diesen Zugriff
unter Umständen steuern (Prioritäten, Rechenzeitschranken etc.).
2.2.4
Dateiorganisation und Verwaltung
Damit Nutzer Informationen über längere Zeiträume im Rechensystem speichern
können (Persistenz), stellt das Betriebssystem dem Nutzer eine Dateiorganisation
und -verwaltung zur Verfügung.
Wichtige Aspekte:
• Umfang und Größe des Speicherplatzes
• Art der Speichermedien (Platte, CD, Disc etc)
• Art des Zugriffs, Sicherung von unberechtigten Zugriffen oder Datenverlust /
-verfälschung
• Art der Organisation in Dateibäumen, Hierarchien, Namensgebung etc.
Interne Aufgaben des Betriebssystems:
216KAPITEL 2. BETRIEBSSYSTEME, SYSTEMNAHE PROGRAMMIERUNG
• Lokalisierung der Daten auf den Speichermedien
• Verwaltung der Speichermedien
• Transport zwischen den Speichermedien
Dateikonzept (UNIX, DOS, OS/2, Windows, . . . )
• Benutzersicht
– mit einem Namen versehene Zusammenfassungen eines strukturierten
Datenbestands
– Dienste: erzeugen, lesen, schreiben, Rechte vergeben, umbenennen, kopieren, löschen, . . .
• Realisierung durch das Betriebssystem
– Bytefolgen bzw. Folgen von Records (abhängig vom Medium / Betriebssystem)
– Speicherung in der Regel in Blöcken einheitlicher Größe auf dem Medium
Vgl. Daten ↔ Information: Die Bedeutung einer Datei wird vom Benutzer festgelegt;
das Betriebssystem verwaltet nur uniforme Container.
+ universell, für beliebige Datenbestände nutzbar, effizient
- Benutzerprogramme müssen Dateiinhalte selbst korrekt interpretieren → Aufwand, fehleranfällig
Beispiel:
ditor
Speicherung eines Java-Objekts als Datei und Bearbeitung mit Texte-
Dateisysteme
• Aus Gründen der Übersicht und der Effizienz ist es zweckmäßig, größere Mengen benannter Dateien hierarchisch (in Bäumen / Graphen) zu strukturieren.
• Betriebssysteme bieten hierfür spezielle Dateien — Verzeichnisse — an.
• Ein Verzeichnis fasst eine Menge von Dateien und Verzeichnissen zu einer
Einheit zusammen und enthält die erforderliche Verwaltungsinformation.
Beispiel:
Ausschnitt aus einem UNIX-Dateisystem
/
home usr lib var etc
Der Zugriff auf Dateien erfolgt über Zugriffspfade, die aus einer Folge von
Verzeichnisnamen, gefolgt vom Namen der Datei, bestehen.
Arten von Dateien in einem Dateisystem:
• gewöhnliche Dateien: Benutzerdaten, Programme, . . .
Blätter des Verzeichnisbaums
• Verzeichnisdateien (directories)
interne Knoten
• (UNIX): spezielle Dateien zur Repräsentation von Geräten (z.B. CD-ROM,
Audio, Scanner, . . . )
2.2. BENUTZERRELEVANTE ASPEKTE EINES BETRIEBSSYSTEMS
P1
abstrakt
P2
abstrakt
P3
real
real
Betriebssystem
217
Verbindungsnetz
Hardware
Betriebssystem
Hardware
Rechner 2
Rechner 1
Abbildung 2.4: Interprozesskommunikation (IPC)
2.2.5
Übertragungsdienste
Motivation: Kommunikation zwischen den verschiedenen Benutzern oder den verschiedenen Prozessen eines Benutzers, z.B.: E-Mail, Koodination paralleler Abläufe,
....
Da es sich bei den Kommunikationspartnern um Abläufen von Programmen
handeln, sprechen wir von Interprozesskommunikation (IPC).
Wir unterscheiden lokale IPC für Kommunikation innerhalb der Grenzen eines Rechners und verteilte IPC (vgl. Abb. (2.4)).
Gängige Betriebssysteme stellen für untershciedliche Kommunikationsanforderungen zahlreiche verschiedene IPC-Dienste zur Verfügung. Merkmale:
• lokal / verteilt
• Bandbreite (schmal- / breitbandig) ∼ maximal übertragbare Datenmenge je
Zeiteinheit
• Latenz ∼ minimale Dauer für die Übertragung einer Nachricht
• verbindungslos / verbindungsorientiert
• Anzahl der sendenden und empfangenden Prozesse (1:1, 1:n, n:m)
• (A)Synchronität von Sende- und Empfangsereignissen
• Niveau der Kontrolle durch das Betriebssystem (in- / offizielle Kanäle)
Beispiele:
• Signals / Interrupts: lokal, schmalbandig
• Pipes (UNIX): lokal, breitbandig, asynchron, verbindungsorientiert
Benutzersicht
• verbindungslose IPC: Kommandos für Übertragungsauftrag, Parameter: Empfänger (ggf. Netzadresse), Nachrichten, Lage, u.U. Diensteigenschaften (z.B.
Weg)
• verbindungsorientierte IPC: zusätzlich Kommandos für Verbindungsauf- und
-abbau
• in der Regel Erfolgs- / Fehlermeldung nach Beendigung der Übertragung
218KAPITEL 2. BETRIEBSSYSTEME, SYSTEMNAHE PROGRAMMIERUNG
request
Client−Prozess
Server−Prozess
reply
Betriebssystem
Betriebssystem
Netz
Das Client – Server – Modell
Mit den genannten IPC-Diensten können verschiedene Kommunikationsstrukturen
realisiert werden. Wichtige Kommunikationsstrukturen sind:
• Gruppenkommunikation
• Peer – to – Peer – Kommunikationssysteme; jeder Kommunikationspartner
hat die gleichen Rechte und Fähigkeiten
• Client – Server: asymmetrisch
Client-Server-artig strukturierte Systeme sind weit verbreitet. Sie bestehen aus zwei
Arten von Rechnern bzw. Prozessen:
• Clients: Dienstnutzer, erteilen Aufträge
• Server: Dienstanbieter, nehmen Aufträge entgegen und bearbeiten diese
Client-Server-artige Kommunikation ist nicht auf den verteilten Fall beschränkt.
Client- und Serverprozess können sich auf dem selben Rechner befinden.
Beispiel:
• Druckdienst, Dateiverwaltung
• UNIX X11 – Server und Clients (z.B. mozilla, gvim, . . . )
Middleware
Große, räumlich verteilt ablaufende Prozgrammsysteme (z.B. Flugbuchungssysteme) stellen auf Grund der großen Menge zu transportierender Nachrichten und ihrer
Heterogenität (Programmiersprachen, Datenformate, Hardware-Plattformen) hohe
Anforderungen an Übertragungsdienste.
Unter dem Begriff Middleware werden Betriebssystemerweiterungen verstanden, die auf den IPC-Primitiven vorhandener Betriebssysteme basieren und die
benötigten, höherwertigen, plattformübergreifenden Übertragungsdienste für verteilte Anwendungen zur Verfügung stellen.
Dienste (u.a.):
• Remote Procedure Call (RPC), Remote Method Invocation (RMI)
• verteilte Namens- und Verzeichnisdienste
• Konvertierung zwischen Repräsentationen (Programm, Hardware)
• Dienstlokalisierung
• Persistenz: verteilte Speicherung
• Lastverteilung
2.2. BENUTZERRELEVANTE ASPEKTE EINES BETRIEBSSYSTEMS
Rechner A
Rechner B
219
Rechner C
Verteilte Anwendung
Middleware
Betriebssystem
Betriebssystem
Betriebssystem
Netz
Abbildung 2.5: Middleware
Client−stub des Dienstes D
call
Parameter
packen
Ergebnis
entpacken
Server−stub des Dienstes D
Parameter
entpacken call
Ergebnis
entpacken
Betriebssystem
D
Betriebssystem
Netz
Abbildung 2.6: RPC
Beispiel: RPC
Idee: Analog zum gewöhnlichen Unterprogrammaufruf eine Prozedur / Funktion
auf einem entfernten Rechner aufrufen.
Einsatz: weit verbreitet; z.B. Network File System (NFS)
Probleme: heterogene Hardware, Hardware-Ausfälle, . . .
Anforderungen:
• transparenter Nachrichtenversand
• Transformation von Parametern, Ergebnissen
• Gewährleistung der Semantik bei partiellen Systemausfällen
Konzept:
• Indirektion des Prozeduraufrufs über zusätzliche Client-Server-stub-Funktionen (∼ Stummel), die den Nachrichtenversand organisieren.
• Für jede entfernt aufrufbare Prozedur wird ein Client-Server-stub-Paar benötigt.
• Stub-Funktionen werden auf der Basis einer RPC-Spezifikation mit einem
Werkzeug generiert.
Beispiel:
PRC einer Funktion D: s. Abb. (2.6)
220KAPITEL 2. BETRIEBSSYSTEME, SYSTEMNAHE PROGRAMMIERUNG
Nutzerebene
Verwaltung
Betriebsmittelspeicher
Tasks des Betriebssystems
Benutzerprozesse
Dateiverwaltung
Systemtask
Speicherverwaltung
Platten
Zeitgeber
...
Prozessverwaltung und −steuerung
Abbildung 2.7: Aufbau eines Betriebssystems
2.2.6
Zuverlässigkeit und Schutzaspekte
Betriebssysteme sind die Basis aller Benutzerpgramme, äußerst komplex und extremen Belastungen ausgesetzt. Hinzu kommen Eingabe- und Hardwarefehler. Betriebssystemausfälle können zu kostspieligen Informationsverlusten und Nutzungsausfällen mit Folgeschäden führen. Die Zuverlässigkeit und Betriebssicherheit von
Betriebssystemen ist daher entscheidend. Betriebssysteme sollten auf extreme Bedingungen, Hardware- und Nutzungsfehler möglichst unempfindlich reagieren, d.h.
robust und fehlertolerant.
Betriebssysteme verwalten umfangreiche Daten verschiedener Personen. Es muss
vermieden werden, dass Benutzer — unabsichtlich oder vorsätzlich — unbefugt Daten lesen, verfälschen, zerstören oder kopieren können. Wir sprechen von Datenschutz und Datensicherung.
• Sperrmechanismen
• Passwörter
• Verschlüsselung
Schematischer Aufbau eines vereinfachten Betriebssystems (angelehnt an UNIX,
nach Tanenbaum) in Schichten: s. Abb. (2.7).
Die einzelnen Teile des Betriebssystems sind Programme, die bestimmte Aufgaben wahrnehmen; wir sprechen auch von Tasks.
2.3
Betriebsmittelverteilung
Aufgabe des Betriebssystems: Zuteilung der Betriebsmittel unter dem Ziel der optimalen Auslastung der Betriebsmittel und der optimalen Versorgung der Tasks.
2.3.1
Prozessorvergabe
Im Multiplexbetrieb sind mehrere Rechenwillige Programme (Tasks) gleichzeitig
im System vorhanden. Das Betriebssystem regelt die Prozessorzuteilung und die
Verwaltung der Tasks.
Strategien für Prozessorvergabe (Scheduling):
• lifo
• fifo
• Prioritäten
• Mischformen
Frage, wie lange ein Prozess (Task) ausgeführt wird:
2.3. BETRIEBSMITTELVERTEILUNG
221
• unbeschränkte Ausführung
• Zeitscheibenverfahren
• Unterbrechungskonzept
Beim Unterbrechen von Programmen in Ausführung (Prozessen) müssen alle
Daten gerettet werden, die das Fortsetzen erlauben. Dieses Datenpaket, das für die
Fortsetzung entscheidend ist, beschreibt den Prozess eindeutig. Wir sprechen von
einer Prozesspräskription.
Auf vielen Rechensystemen laufen alle wichtigen Vorgänge auf nur einem Prozessor streng sequentiell ab. Trotzdem macht es Sinn, die Vorgänge gedanklich in
eine Form von parallel ablaufenden Programmen zu strukturieren. Wir sprechen
von quasi-paralleler Verarbeitung.
2.3.2
Hauptspeicherverwaltung
Aufgaben:
(1) Zuteilung des Hauptspeichers an Tasks / Prozesse: Der Hauptspeicher zerfällt
in die Datenbestände der rechenwilligen Programme.
(2) Verlagerung von Speicherinhalten in den Hintergrundspeicher. Dadurch wird
erreicht, dass der gedachte Hauptspeicher (virtueller Speicher) viel größer
ist als der wirklich vorhandene (näheres später).
(3) Schutz der einzelnen Speicherabschnitte vor unberechtigtem Zugriff
Beispiel: Speicherverwaltung
Der Speicher entspricht logisch einem Feld var [0:n] array m h.
Ziel: In h sollen k Untersequenzen (Teilfelder), genannt Segmente, der Länge li , 1 ≤
Pk
i ≤ k abgelegt werden. (Annahme: i=1 li ≤ n)
Die Zahl k der Segmente und ihre Ausdehnung li kann sich dynamisch ändern.
Folgende Prozeduren sind zu implementieren:
proc kreiere = (var nat i):
legt neues Segment der Länge 0 auf h mit Namen k an
proc erweitere = (nat i, m x):
erweitere Segment i um einen Wert und besetze den Speicherplatz mit x
proc lese = (nat i, nat j, var m x):
lies im i-ten Segment den Eintrag mit Index j und weise das Ergebnis x zu
proc laenge = (nat i, var nat j):
weise j die Länge li des Segments zu
proc loeschen = (nat i):
lösche i-tes Segment
...
Probleme:
(1) Die benötigten Speicherbereiche sind ungleichmäßig groß.
(2) Sie ändern ihren Umfang während des Betriebs.
(3) Der Hauptspeicher ist insgesamt zu klein (weil zu teuer).
(4) Inhaltlich zusammengehörige Datenbestände sollen auch organisatorisch zusammengefasst werden.
Lösungen:
222KAPITEL 2. BETRIEBSSYSTEME, SYSTEMNAHE PROGRAMMIERUNG
• Segmentierung – Unterteilen der Speicherelemente in Segmente
• Seitenaustauschverfahren – gedachter Hauptspeicher ist viel größer als echter,
Teile der Daten liegen auf dem Hintergrundspeicher.
2.3.3
Betriebsmittelvergabe im Mehrprogrammbetrieb
Die Bedürfnisse der Programme nach Betriebsmitteln variieren stark. Wir spechen
von:
• rechenintensiven Programmen
• speicherintensiven Programmen
• E/A- und kommunikationsintensiven Programmen
• verwaltungsintensiven Programmen
Aufgaben:
• Verzögerung der Ausführung einzelner Programme
• faire Zuteilung der Betriebsmittel
• Minimierung des Organisationsaufwands
• Minimierung der Umschaltzeiten bei Unterbrechungen
• Regelung der Zugriffsrechte
Betriebsarten:
• halbduplex: Es wird abwechselnd ein- und ausgegeben.
• vollduplex: Es wird gleichzeitig ein- und ausgegeben.
Im Dialogbetrieb führt jedes eingegebene Zeichen zu einer Unterbrechnung, um es
im Prozessor zu behandeln.
Aufgaben des Betriebssystems im Dialogbetrieb:
• Aufsammeln der eingegebenen Zeichen zu Zeichenfolgen (Kommandos)
• Zuordnen der E/A zu Benutzerprogrammen und den E/A – Geräten
• Ausgabe am Bildschirm (Echo der Eingabe)
• Verwaltung der dialogspezifischen Nutzerdaten
2.3.4
Zuteilung der E/A – Geräte
Das Betriebssystem verwaltet und steuert die E/A – Geräte. Dabei setzen Nutzerprogramme E/A – Aufträge an das Betriebssystem ab, die dann vom Betriebssystem
durchgeführt werden. (Details später)
2.4. TECHNIKEN DER SYSTEMPROGRAMMIERUNG
2.4
223
Techniken der Systemprogrammierung
Die Systemprogrammierung ist die Lehre
• vom Entwurf,
• der Realisierung und
• der Eigenschaften
der Familie von Programmen eines rechnersystems, welche die Annahme und Bearbeitung von Nutzeraufträgen unter gewissen Optimalitätsgesichtspunkten organisieren und teilweise durchführen.
Optimalitätsgesichtspunkte:
• Zuverlässigkeit (Verfügbarkeit, Robustheit)
• Komfort, Antwortzeiten
• Sicherheit (security)
• Änderungsfreundlichkeit, Wertbarkeit
Typische Aufgaben der Systemprogrammierung:
• Entwurf und Realisierung von Betriebssystemen
• Kommunikationsaufträge in Rechner- oder Telekommunikationsnetzen
• Übersetzung und Implementierung von Programmiersprachen
• Entwurf und Realisierung eingebetteter Software – Systeme
Dabei werden neben den üblichen Programmier- und Softwarekenntnissen (Algorithmen, Datenstrukturen, Softwarearchitekturen) auch Kenntnisse über Systemund Hardwareeigenschaften vorausgesetzt.
2.4.1
Unterbrechungskonzept
Unterbrechungen werden aus internen oder externen Gründen notwendig.
Beispiele:
• zugeteilte Prozessorzeit verbraucht
• Geräte oder Daten werden angefordert, sind aber nicht vorhanden
• ein E/A – Gerät meldet sich nach ausgeführtem Auftrag zurück
• der Operator unterbricht die Ausführung
• ein Fehler (Alarm) tritt auf
Zur Behandlung von Unterbrechungen könnte ein erweiterter Befehlszyklus wie folgt
aussehen:
224KAPITEL 2. BETRIEBSSYSTEME, SYSTEMNAHE PROGRAMMIERUNG
while true do
Befehlszähler hochzählen
Berechnung Befehlsadresse, Befehl laden
Speicherschutzüberprüfung, ggf. Speicherschutzalarm
falls Unterbrechung vorliegt, diese behandeln
Wird ein Programm unBerechnung der Operandenadressen
Speicherschutzüberprüfung, ggf. Speicherschutzalarm
Befehlsüberprüfung
Befehlsausführung
od
terbrochen, so sind alle Daten zu retten (abzuspeichern), die für eine nahtlose
Fortsetzung nötig sind.
Wichtige Informationen:
• Registerinhalte
• Angaben über die Lage des Programms und seiner Daten im Speicher
Nicht zu jedem Zeitpunkt macht eine Unterbrechung Sinn. Deshalb gibt es in einer
Reihe von Systemen das Konzept der Unterbrechungssperre. Ist die Unterbrechungssperre gesetzt, so kann das Programm nicht unterbrochen werden.
Das Setzen der Unterbrechungssperre ist ein privilegierter Befehl. Solche Befehle können nur im Systemmodus, nicht aber im Nutzermodus ausgeführt werden.
Das System arbeitet in folgenden Ausführungsmodi:
• Normalmodus: Ausführung von Nutzerprogrammen und unkritischen Systemprogrammen — keine privilegierten Befehle
• Systemmodus: Ausführung von Systemprogrammen, privilegierte Befehle, mit
oder ohne Unterbrechungssperre
Im technischen Detail wird dies am Beispiel der Maschine MI erläutert.
Um Systemprogramme, die im System vorgegeben sind, nutzen zu können, rufen
wir in der MI Systemdienste auf.
• CHMK n: (change mode to kernel): n ist eine Zahl, die einen Systemdienst
identifiziert.
• REI: (return from interrupt)
Wie Nutzerprogramme, verfügt auch das System über einen Stack (Systemstack).
CHMK bewirkt das Abspeichern der Informationen, die für die Fortsetzung benötigt
werden, im Systemstack:
• Systemdienstkennzahl (Adresse für n)
• aktueller Wert des Befehlszählers
• aktueller Wert des PSW (Prozessorstatusregister)
REI ist ein privilegierter Befehl, setzt die Register aus dem Stack in den alten
Zustand zurück.
• Systemaufruf:
– bereitstellen gewisser Parameter (im Stack)
– CHMK n
– nach Rückkehr löschen der Parameter
2.4. TECHNIKEN DER SYSTEMPROGRAMMIERUNG
225
• Systemunterprogramm (Systemdienst):
– PCB – Prozesskontrollblock
– Systemdienst n ausführen
– Auswählen eines Prozesses zur Fortsetzung
– PCB laden
– REI
Das PCB umfasst:
• Stackpegel im Systemmodus
• Stackpegel im Benutzermodus
• Registerinhalte
• Anfangsadresse und Lage der Seitentabelle (s. später) für Benutzerprogramme
und Benutzerstack
• PSW
Bedeutung einiger Bits im PSW:
CM 6, 7 current mode: 00 Systemmodus, LL Nutzermodus
PM 8, 9 previous mode
N
28
negative
Z
29
zero
V
30
overflow
C
31
carry
IV 26
integer overflow
Privilegierte Befehle zum Retten bestimmter Registerinhalte:
SPPCB speichern des PCB
Anfangsadresse im Register PCBADDR.
LPCB
laden des PCB
2.4.2
Koordination und Synchronisation
Um die Synchronisation in der MI zu bewerkstelligen, wird ein bestimmter Befehl
benötigt, in dem das Lesen und Schreiben bzw. Umsetzen des Befehlszählers als
unteilbare Aktion erfolgt.
JBSSI a b c (jump on bit set and set interlocked)
Die Ausführung von JBSSI a b c bewirkt:
• Setzen des Befehlszählers auf den durch c gekennzeichneten Wert, falls b[a]
= L.
• Setzen von b[a] = L, falls b[a] = 0.
Dies ist analog zu:
1
2
4
6
await true then
if b[a] = L then
goto c
else
b[a] = L
fi
endwait
Idee zur Nutzung: (busy wait)
226KAPITEL 2. BETRIEBSSYSTEME, SYSTEMNAHE PROGRAMMIERUNG
1
2
4
6
c: await true then
if b[a] = L then
goto c
else
b[a] = L
fi
endwait
JBCCI (jump on bit cleared and clear interlocked): analog zu JBSSI, aber Rolle
von 0 und L vertauscht.
Bei der obigen Realisierung von Semaphoren durch busy wait hängt es vom
Scheduler ab, ob die P- und V-Operationen fair ablaufen. Bei dieser Lösung
bleibt dem Betriebssystem verborgen, dass der Prozess vor einem Semaphor wartet. Geschickter ist es, dem Betriebssystem mitzuteilen, dass ein Prozess an einem
Semaphor wartet. Dazu werden Semaphore auf Betriebssystemebene verwaltet.
Wir geben ein einfaches Beispiel an, wie dann P und V realisiert werden.
1
2
4
6
8
10
12
14
16
18
20
22
24
26
proc P = (var bool b, var nat s):
|- a: await true then
if b = 0 then
goto a
else
b := 0
fi
endwait;
if s > 0 then
s := s-1;
await true then
if b = L then
goto error
else
b := L
fi
endwait
else
await true then
if b = L then
goto error
else
b := L
fi
endwait;
goto a
fi
-|
28
30
32
34
36
38
proc V = (var bool b, var nat s):
|- a: await true then
if b = 0 then
goto a
else
b := 0
fi
endwait;
s := s+1;
await true then
2.4. TECHNIKEN DER SYSTEMPROGRAMMIERUNG
if b = L then
goto error
else
b := L
fi
endwait
40
42
44
227
-|
Dies ist ein Beispiel dafür, wie selbst bei einem fairen Scheduling eine dynamische Verklemmung auftreten kann, falls s = 0 gilt, ein Prozess eine P-Operation
versucht und ein anderer eine V-Operation. Wird P immer in dem Zustand unterbrochen, in dem er b auf 0 gesetzt hat, so kann weder die P- noch die V-Operation
erfolgreich abgeschlossen werden.
Ausweg (wie oben angesprochen): Semaphore auf Betriebssystemebene.
• P(s) wird realisiert durch CHMK: Der Prozess geht in einen Wartezustand,
falls s = 0 gilt (er wird in eine Warteschlange für s eingereiht). Sobald ein
anderer Prozess eine V-Operation ausführt, wird der erste Prozess aus der
Warteschlange fortgesetzt (falls die Warteschlange nicht leer ist).
• V(S) wird realisiert durch CHMK: Weckt einen Prozess aus der Warteschlange
auf, falls diese nicht leer ist, bzw. erhöht s.
Bei dieser Technik behält das Betriebssystem den Überblick über die Zahl der wartenden Prozesse. Globale Verklemmungen (alle Prozesse warten) können erkannt
werden.
2.4.3
Segmentierung
Die Gesamtmenge der Speicherzellen (Adressen), auf die ein Benutzer(prozess)
während seiner Ausführung exklusiv zugreift, nennen wir Benutzeradressraum. Das
Betriebssystem trägt dafür sorge, dass dieser Benutzeradressraum dem Prozess ungestört und exklusiv zur Verfügung steht. In der Regel wird dieser Benutzeradressraum in Teilabschnitte, genannt Segmente, unterteilt. Aus Sicht des Nutzers ist
ein Segment durch eine Nummer und seine Länge charakterisiert. Es bietet sich an,
Inhalte (Speicherzellen) in Segmente über Anfangsadresse und relative Adresse im
Segment anzusteuern.
Vorteil: Die zufällige (vom Betriebssystem dynamisch festgelegte) Lage der Segmentinhalte im Speicher ist für den Benutzer unwichtig.
Typisch in Betriebssystemen:
• Der Adressraum eines Prozesses (Prozesspräskription) besteht aus einer Menge von Segmenten.
• Für jedes Programm und jeden Datenbereich (file) wird ein Segment angelegt,
für das Zugriffsrechte und -eigenschaften geregelt sind.
• Ansteuerung der Segmentinhalte über Paare (B, β) aus Segmentnamen B und
Relativadresse β.
• Programmstücke in Segmenten sind als Unterprogramme organisiert.
Technisch:
• Jeder Benutzer bekommt eine Segmenttabelle für seine Segmente.
• Für den Zugriff wird Hardwareunterstützung vorgegeben, durch spezielle Register für die Segmenttabelle und die Segmentnummer.
228KAPITEL 2. BETRIEBSSYSTEME, SYSTEMNAHE PROGRAMMIERUNG
virtuelle Adresse
Seitentabellenregister
S
W
Seitennummer
relative Adresse
K
Laengenregister
K
W
physikalische Adresse
Abbildung 2.8: virtuelle Adressierung
Vorteile der Segmentierung:
• einfache Zugrifssstruktur für den Benutzer
• übersichtliche Organisation der Zugriffsrechte für das Betriebssystem mit Hardwareunterstützung
Die Idee der Segmentierung lässt sich perfekt kombinieren mit der Idee des virtuellen
Speichers.
2.4.4
Seitenaustauschverfahren
Da der physikalische Hauptspeicher oft nicht ausreicht, alle Nutzeradressräume aufzunehmen, arbeitet man mit einem gedachten größeren Hauptspeicher (virtueller
Speicher) und bildet diesen auf den physikalischen Hauptspeicher und den Hintergrundspeicher ab. Dazu wird der gedachte Hauptspeicher in Abschnitte gleicher
Größe (sogenannte Seiten) zerschnitten. Gleichzeitig wird der physikalische Hauptspeicher (und ähnlich auch die Hintergrundspeicher) in gleich große Abschnitte (gleiche Größe wie die Seiten!) eingeteilt, die Kacheln.
Die Seiten werden auf die Kacheln abgebildet (jede Kachel entspricht einer Seite,
Seiten liegen entweder in Kacheln, auf dem Hintergrundspeicher oder sind nicht
belegt).
Beim gedachten Hauptspeicher arbeiten wir mit viruteller Adressierung. Die
Seitentabelle enthält für jede Seite (gekennzeichnet durch die Seitennummer) die
Kachelnummer. Vorteile des virtuellen Speichers:
• flexible Erweiterung der Segmente um weiteren Speicherplatz
• großer virtueller Adressraum
• Speicherüberlagerung: effiziente Nutzung des Speichers durch mehrere Prozesse
• Selbstregulierung der Ein-/Auslagerung von Seiten (Seiten nicht aktiver Prozesse werden schrittweise verdrängt)
• exzellente Ergänzung mit der Segmentierung
2.4.5
Verschiebbarkeit von Programmen
Bei der maschinennahen Programmierung wurde bereits betont, dass Programme
so geschrieben werden sollten, dass ihre absolute Lage im Speicher für das Programm keine Rolle spielt (Stichwort: relative Adressierung). Durch die Segmentierung können wir den (virtuellen) Speicher / Adressbereich festlegen, in dem ein
2.4. TECHNIKEN DER SYSTEMPROGRAMMIERUNG
Prozessadresse
Segmenttabellenregister
Laenge
229
SEG
Ort
S
W
Segmenttabelle
Laenge
Ort
Zugriffseigenschaften
Seitentabelle
K
fuer Organisationszwecke
und Zugriffsrechte
K
W
Speicheradresse
Abbildung 2.9: Kombination von Seitenaustauschverfahren und Segmentierung
Programm abgelegt ist. Dadurch kann überprüft werden, ob ein Programm unberechtigt auf den Speicher zugreift.
Technisch kann dies durch Hardware unterstützt werden, indem man Register
für Anfangs- und Endadresse vorsieht. Im Befehlszyklus wird überprüft, ob die
angesprochenen Adressen außerhalb des zulässigen Bereichs liegen.
Das Setzen dieser Register ist ein privilegierter Befehl.
2.4.6
Simultane Benutzbarkeit von Unterprogrammen
Bei Mehrprogrammbetrieb kann es vorkommen, dass mehrere Programm gleichzeitig (bzw. zeitlich verzahnt) das gleiche Programm arbeiten. (beispielsweise im
Rahmen des Aufrufs eines Systemdienstes). Es wäre ineffizient (Verschwendung von
Speicher), dieses Programm dann mehrfach (in Kopien) im Speicher zu halten.
Ein Programm (Unterprogramm), das im Durchschnitt unterschiedlicher Benutzeradressräume die korrekte Ausführung für mehrere Programme gleichzeitig ohne
Maßnahmen des gegenseitigen Ausschlusses garantiert, heißt simultan benutzbar.
Dies setzt natürlich voraus, dass in dem Bereich des Programms nichts während
der Ausführung abgespeichert wird.
Technisch erfordert das, dass die Operandenversorgung konsequent über Leitadressen erfolgt. Wesentlich:
• keine Abspeicherungen in das Segment, das das Programm enthält
• alle Speicherzellen, die sich ändern, werden über geeignete Register angesteuert
• unter Umständen ist der Speicherschutz eingeschränkt
• Laden des Registers ist ein privilegierter Befehl
• Ansteuern der Operanden nach festen Regeln
Wieder kann das hardwareseitig unterstützt werden (Translationsregister).
230KAPITEL 2. BETRIEBSSYSTEME, SYSTEMNAHE PROGRAMMIERUNG
2.4.7
Steuerung von E/A – Geräten
Die Zuteilung und Verwaltung von E/A – Geräten erfolgt durch das Betriebssystem.
Hintergrundspeicher und E/A – Geräte arbeiten elektromechanisch, Prozessor
und Hauptspeicher (unter Umständen auch Bildschirme) arbeiten rein elektronisch.
Es ist Aufgabe des Betriebssystems, die Brücke zwischen Elektronik und Mechanik herzustellen, den Nutzer zu entlasten (von zu detaillierten Kenntnisen über die
Gerätespezifikationen) und die Geräte von unsachgemäßer Ansteuerung zu schützen.
Bedingt durch die Unterschiede in der Verarbeitungsgeschwindigkeit arbeiten Prozessor und Geräte asynchron. Sie sind über Kanäle verbunden.
Hardwaretechnisch sind Kanäle einfache programmierbare Einheiten, die Kanalprogramme ausführen. Kanalprogramme bestehen aus Befehlsfolgen, die den Datentransport zwischen Speicher und Gerät steuern. Kanäle sind aktive Einheiten
mit stark eingeschränktem Befehlssatz.
Die Steuerung der Kanäle (Auftragserteilung) erfolgt über den Prozessor durch:
(a) Unterbrechungskonzept: Kanäle melden sich nach Auftragsabwicklung beim
Prozessor zurück, bewirken eine Unterbrechung, um vom Prozessor / Betriebssystem einen neuen Auftrag zu erhalten.
(b) wiederholtes Abfragen (polling): Der Prozessor frägt periodisch die Kanäle ab
und erteilt gegebenenfalls neue Aufträge.
Innerhalb des Betriebssystems gibt es bestimmte Programme / Prozesse / Tasks,
die für die Steuerung der E/A – Geräte mit ihren E/A – Programmen zuständig
sind.
Prinzipien:
• Jeder Gerätetyp benötigt im Prinzip einen entsprechenden Task.
• Aus Sicht des Nutzers und der übrigen Teile des Betriebssystems sind die
Besonderheiten eines Geräts (Gerätetyps) im Task gekapselt. Das übrige Betriebssystem hat einheitliche Zugriffsschnittstellen für den Task.
Der geräteabhängige Code ist im Gerätetreiber gekapselt. Aus Sicht des Betriebssystems sind Gerätetreiber Tasks / Prozesse wie andere auch.
Die Aufgaben des Gerätetreibers hängen stark von den Eigenheiten des Geräts
und des E/A – Prozessors ab.
• Ist der Registerumfang des Kanals / E/A – Kontrollers klein, so muss der
Gerätetreiber unter Umständen Kommando für Kommando an den Kontroller
übertragen.
• Ist der Registerumnfag groß, so können Kommandosequenzen abgelegt und
selbständig ausgeführt werden.
Abhängig vom Gerät muss der Gerätetreiber warten (Unterbrechung), bis das Gerät
das Kommando / die Kommandosequenz ausgeführt hat (z.B. Drucker), oder der
Kontroller reagiert so schnell, dass der Gerätetreiber nicht unterbrochen werden
muss, sondern direkt mit der Übertragung fortfahren kann (z.B. Bildschirm).
Neben den geräteabhängigen Anteilen gibt es geräteunabhängige Codeanteile
für Gerätesteuerung im Betriebssystems mit folgenden Aufgaben:
• einheitliche Schnittstelle für Nutzer zur Übergabe von E/A – Aufträgen
• Namensgebung
• Schutz der Geräte / Synchronisation
2.4. TECHNIKEN DER SYSTEMPROGRAMMIERUNG
Benutzerprozesse
231
E/A−Aufrufe
Daten zur E/A ablegen
E/A−Anforderung
gerateunabhaengige Software
fuer E/A im Betriebssystem
Geraetetreiber
Namen E/A zuordnen
Daten in Bloecke aufteilen und puffern
Speicher zuordnen
Geraeteregister setzen
Status pruefen
Unterbrechungskonzept
Treiber aufwecken, verwalten
E/A−Kontroller, Kanaele
Hardware
E/A−Geraet steuern
Abbildung 2.10: Geräteverwaltung
• Schaffung geräteunabhängiger Blockgrößen für E/A
• Pufferung von Aufträgen
• Speicherzuordnung
• Verwaltung exklusiv genutzter Geräte
• Fehlerberichte
Fazit: Nutzerprozesse dürfen und brauchen nicht direkt auf E/A – Geräte zugreifen.
Es ergibt sich das Bild (2.10).
Vorteile:
• Schutz der Geräte
• Betriebssystem hat volle Kontrolle
• Nutzer braucht technische Details nicht kennen
• Einbringen neuer Geräte, portieren der Lösung mit minimalem Aufwand möglich
Das ist ein wichtiges Beispiel für Schichtenarchitektur (layered architecture, Softwarearchitektur).
Vorteil der Schichtenarchitektur: Sauberes Trennen der unterschiedlichen Aspekte.
Vorteile:
• Experten für die unterschiedlichen Aspekte können sich auf diese konzentrieren.
• Teile können unabhängig erstellt und geändert werden.
• Test der Teile kann unabhängig erfolgen.
• Zusammenhang des Gesamtsystems nach klaren Vorlagen.
232KAPITEL 2. BETRIEBSSYSTEME, SYSTEMNAHE PROGRAMMIERUNG
Nutzerschnittstelle
Monitor
Anwendungsschicht
Zugriffsschicht
DB
Systemdienste
Abbildung 2.11: typische Schichtenarchitektur
Technisch essentiell: Festlegung der Schnittstellen der Bausteine / Schichten
(Komponenten).
Die Schnittstelle beschreibt, wie die Komponenten mit ihrer Umgebung zusammenarbeiten.
Literaturhinweis zu Betriebssystemen: Andrew S. Tanenbaum, Betriebssysteme:
Entwurf und Realisierung
2.4.8
Kommunikationsdienste
Die Kommunikation (Datenaustausch) zwischen Prozessen auf unterschiedlichen
Rechnern erfolgt in der Regel auch durch Unterstützung durch das Betriebssystem. Eine einfache Unterstützung der Kommunikation zwischen Prozessen sind sogenannte Sockets.
Socket: Kommunikationsendprodukt (vom Betriebssystem aufgebaut), an das
ein Nutzerprozess Daten übergeben kann (zum Senden) oder Daten übernehmen
kann (zum Empfangen). Diese Daten werden von Systemdiensten über das darunterliegende Netz übertragen.
Sockets sind im Umfeld von UNIX entstanden, finden sich heute aber in vielen
Betriebssystemen.
Sockets stellen ein API (application programming interface) zur Verfügung, über
das die Kommunikationsdienste des Sockets angesprochen werden. Typische Methoden / Prozeduren:
• socket: erzeugt ein neues Socket und schafft Protokoll – Port – Nummer
• bind: bindet ein Socket an eine Socketadresse (die Socketadresse erlaubt zu
bestimmen, welche Nachrichten für das Socket bestimmt sind)
• listen: zeigt an, dass eine Verbindung gewünscht wird und erzeugt (Verwaltungsinfrastruktur für) eine Warteschlange
• accept: warten auf eine Verbindungsanfrage und schaffen eines neuen Sockets
für eine Verbindung
• connect: Gegenstück zu accept, Verbindungsanfrage, baut Verbindung zum
Server auf
2.4. TECHNIKEN DER SYSTEMPROGRAMMIERUNG
server
socket
233
client
socket
bind
listen
accept
connect
send
receive
close
close
Abbildung 2.12: einfache Kommunikation über Sockets
sender A
receiver B
connection request A to B
connection confirmation A to B
Verbindungsaufbau
data transfer A to B
acknowledgement
iteriert, Datentransport
disconnection request A to B
disconnection indication A to B
Verbindungsabbau
Abbildung 2.13: Abracadabra – Protokoll
• send: Senden von Nachrichten
• receive: Empfangen von Nachrichten
• close: Auflösen der Verbindung
Die eigentliche Übertragung und das Ineinandergreifen der Methoden- / Prozeduraufrufe in einer sinnvollen Weise wird durch ein Protokoll geregelt. Ein
Protokoll legt fest, nach welchen Regeln und in welchen Formaten eine Kommunikation (einschließlich Verbindungsaufbau) abläuft.
Ein Beispiel für Protokolle ist das Abracadabra – Protokoll, das in Abbildung (2.13)
(stark vereinfacht) dargestellt wird.
Da bei der Übertragung von Nachrichten viele unterschiedliche Aspekte zu berücksichtigen sind, baut man die Protokolle in Schichten auf.
Das ISO/OSI – Schichtenmodell
ISO: International Standard Organisation
OSI: Open System Interconnection Protocol
• Physische Schicht: Übertragung von 0 und L auf Schaltungsebene
• Datenverbindungsschicht: Fehlerüberprüfung durch Paritätsbits
• Netzwerkschicht: Festlegung des Übertragungsweges (routing), Übertragung
in Paketen ohne direkte Verbindung (z.B. IP: internet protocol)
234KAPITEL 2. BETRIEBSSYSTEME, SYSTEMNAHE PROGRAMMIERUNG
SAP
application protocol
service access point
SAP
presentation protocol
session protocol
transport protocol
network protocol
data link protocol
physical protocol
network
Abbildung 2.14: ISO/OSI Schichtenmodell
• Transportschicht: Fehlerbehandlung bei verlorenen Paketen (z.B. TCP: transmition control protocol)
• Sessions- und Präsentationsschicht: Festlegung der Sender- und Empfängerrollen, Synchronisation der Kommunikation, Anlaufunterstützung bei Systemfehlern, Festlegung der Bedeutung der übertragenen Bits
• Anwendungsschicht: Übertragung auf Anwendungsebene (z.B. HTTP, FTP)
Routing von Datenpaketen:
• Statisches Routing nutzt statisch vorgegebene Routingtabellen. Der Router
empfängt das Datenpaket und leitet es an den Rechner weiter, an den anhand
seiner Tabelle das Paket gehen soll.
• Dynamisches Routing verwendet Tabellen, die über Informationen, die über
das Netz eintreffen, aktualisiert werden.
Probleme:
• keine garantierten Übertragungszeiten (QoS)
• Pakete nehmen unterschiedliche Routen und kommen unter Umständen nicht
in der Reihenfolge an, in der sie abgesendet werden
Jitter: Varianz bei der Verzögerung in der Übertragung
Unicast: Adressieren eines einzelnen Rechners
Multicast: Adressieren einer Gruppe von Rechnern
Anycast: Adressieren über Gemeinsamkeiten in der Adresse
2.5. BETRIEBSSYSTEM – STRUKTUREN
235
Beispiel: TCP – Nachrichtenformat
0
15 16
31
source port destination port
sequence number
acknowledge number
(technische Angaben)
checksum
urgent pointer
Optionen
Nutzerdaten
In Netzen können Pakete verloren gehen. Diese müssen dann erneut gesendet
werden. Problempunkte:
(1) Fehler (Paketverlust) feststellen: Grundsätzlich ist es bei asynchroner Übertragung nicht einfach, festzustellen, dass ein Paket nicht angekommen ist. Dies
kann geschehen, indem:
(a) Der Empfänger darüber informiert ist (Teil des Protokolls!), dass ein
Paket kommen sollte und nach einiger Zeit dem Sender mitteilt, dass
es nicht eingetroffen ist. (Problem: Auch diese Mitteilung kann verloren
gehen!)
(b) Der Empfänger bestätigt alle eingehenden Pakete dem Sender. Der Sender wiederholt das Senden von Paketen, falls nach einer bestimmten Zeitspanne keine Bestätigung eingetroffen ist.
Achtung: Bei (b) kann ein Paket beim Empfänger mehrfach eintreffen; deshalb
müssen Pakete so gekennzeichnet werden (Sequenznummer), dass Kopien erkannt
und aussortiert werden können.
Literatur:
• Tanenbaum, van Stean: Distributed Systems
• Comer: Computernetzwerke und Internets
2.5
Betriebssystem – Strukturen
Betriebssysteme sind heute oft sehr umfangreiche Softwaresysteme. Deshalb ist
es extrem wichtig, gute Softwarestrukturierungsprinzipien anzuwenden (Stichword:
Softwarearchitektur).
Beispiele:
OS/360
BS2000
UNIX
IBM
Siemens
Bell Labs
Großrechnerbetriebssystem für Mehrprozessbetrieb
Großrechnerbetriebssystem für Mehrprozessbetrieb
Timesharing – Betriebssystem für mittelgroße
Rechner, Arbeitsplatzrechner, Linux für PCs
PC – Betriebssystem
PC – Betriebssystem
MS-DOS
Microsoft
Windows
Microsoft
Mac-OS
Apple
Windows NT Microsoft netzwerkfähiges PC – Betriebssystem
Für eingebettete Softwaresysteme existieren eigenständige Betriebssystemfamilien.
236KAPITEL 2. BETRIEBSSYSTEME, SYSTEMNAHE PROGRAMMIERUNG
Applikation
OSEK Time
OSEK/VDX
application layer
message filter
Time
Service
fault ident. layer
Netzwerk−
management
comm. subsystem
CNI driver
Anwendungsmodule
OSEK
bus driver
I/O−System
Mikrocontroller
bus communication hardware
Abbildung 2.15: Struktur von OSEK
running
wait
preemt
start
waiting
release
terminate
suspended
activate
ready
Abbildung 2.16: Task State Modell in OSEK
Beispiel: OSEK im Automobilbereich (vgl. Abb. (2.15))
Die Echtzeitbetriebssysteme in mobilen Geräten (PDA, mobile Telefone etc.)
verwenden ebenfalls die Konzepte allgemeiner Betriebssysteme, allerdings oft in
eingeschränkter Form.
Abschließende Diskussion zu Betriebssystemen
Was sind
gute Modelle für Betriebssysteme?
➥ monolithischer Ansatz: Das Betriebssystem besteht aus einer Familie von Prozeduren, die sich gegenseitig aufrufen.
Nachteil: Software des Betriebssystems ist sehr unstrukturiert.
➥ Betriebssystem als Familie von
virtuellen Zustandsmaschinen.
➥ Schichtenmodell: Betriebssystem wird aus Betriebssystemkern in Schritten erweitert.
Vorteil: klare Strukturierung, einfache Portierung
Erweiterung:
• Prozesshierarchien
2.5. BETRIEBSSYSTEM – STRUKTUREN
• Auftraggeber / Auftragnehmer – Modell
237
238KAPITEL 2. BETRIEBSSYSTEME, SYSTEMNAHE PROGRAMMIERUNG
Kapitel 3
Interpretation und
Übersetzung von
Programmiersprachen
Arten von Programmiersprachen:
• höhere, problemorientierte Sprachen:
Struktur, Konzepte der Sprache sind ausgerichtet auf die Probleme, die durch
die Sprache beschrieben / gelöst werden sollen.
Beispiele: Java, Pascal, Ada, Lisp
• maschinennahe Sprachen:
Struktur und Konzepte sind von der Hardware bestimmt.
Vorteil: Maschinensprachen können unmittelbar auf der Hardware ausgeführt
werden.
Hardware definiert Maschinensprachen
Um eine höhere Programmiersprache auf einer Maschine zur Ausführung zu bringen,
bieten sich zwei Konzepte an:
➥ Interpretation: Durch ein Programm — den Interpreter (in der Maschinensprache geschrieben) — wird ein Programm der höheren Programmiersprache
interpretiert, d.h. schrittweise ausgeführt.
➥ Übersetzung Durch ein Programm (in der Maschinensprache geschrieben) —
Übersetzer / Compiler — wird ein Programm der höheren Programmiersprache in ein maschinennahes Programm übersetzt.
Bei der Übersetzung können drei Programmiersprachen eine Rolle spielen:
• Quellsprache (Sprache, in der das zu übersetzende Programm geschrieben ist)
• Zielsprache (Sprache, in die das Programm zu übersetzen ist)
• Sprache, in der der Compiler geschrieben ist
Eine Programmiersprache ist syntaktisch durch eine formale Sprache gegeben. Eine
formale Sprache ist eine (nicht notwendigerweise endliche) Menge von Zeichenfolgen
über einem Zeichensatz Z, L ∈ Z ∗ . Wie natürliche Sprachen sind Programmiersprachen in der Regel Folgen von Wörtern. Wir sprechen von einer lexikalischen
Struktur.
Programmiersprachen werden in der Regel syntaktisch wie oben angedeutet in
zwei Stufen beschrieben.
239
240
KAPITEL 3. INTERPRETATION UND ÜBERSETZUNG . . .
>
if a
<id>nat
then
b
<id>nat
a
else b fi
<id>nat
<id>nat
<op>nat
<exp> nat
<exp> nat
<exp> nat <exp> nat
<exp> bool
<if_exp>nat
<op>nat
Abbildung 3.1: Syntaxbaum mit Attributen
• Lexikalischer Anteil: Zugelassene Wörter werden teilweise über Lexika (endliche Menge von Wörtern) oder über einfache Beschreibungsmittel (endliche
Automaten, reguläre Grammatiken; siehe später) beschrieben.
• Grammatikalischer Anteil: Die Reihenfolgen, in denen die zugelassenen Wörter
aufeinander folgen dürfen.
Darüber hinaus verwenden wir zusätzliche Regeln (Kontextbedingungen), um die
Zusammensetzung weiter einzuschränken.
Syntaktische Behandlung:
Quellprogramm
Fehlermeldung, falls ↓ lexikalische Analyse (scanner)
inkonsistente Wörter auftreten
Symbol- und Konstantentabelle (Tabelle aller Wörter im Quellprogramm)
Indexliste1
Fehlermeldung, falls syntaktische ↓ grammatikalische / SyntaxanalyStruktur nicht korrekt
se (parser)
Syntaxbaum2
Fehlermeldung, falls ↓ Attributierung und Prüfung von
Nebenbedingungen nicht erfüllt
Kontextbedingungen
Syntaxbaum mit Attributen3
In der syntaktischen Behandlung eines Programms wird überprüft, ob der Text
den syntaktischen Anforderungen genügt; falls nicht, werden Fehlermeldungen erzeugt; falls doch, wird ein attributierter Syntaxbaum generiert.
Problem: Fehleranalyse und Hinweise auf mögliche Fehler geben.
Ausgehend von dem Ergebnis der syntaktischen Behandlung kann dann die
Übersetzung oder Interpretation erfolgen.
• Übersetzer
Syntaxbaum mit Attributen
↓ Zwischencodeerzeugung
Codeinterndarstellung
↓ Codeoptimierung
optimierter Code in Interndarstellung
↓
Zielprogramm
Dabei ist nur der letzte Schritt von der Maschinensprache abhängig.
1 Sequenz
der Wörter, codiert dargestellt durch die Indexwerte aus der Tabelle
Darstellung der grammatikalischen Struktur
3 strukturierte Darstellung der grammatikalischen Struktur mit zusätzlichen Angaben
2 strukturelle
3.1. LEXIKALISCHE ANALYSE
241
• Interpreter
Syntaxbaum mit Attributen
↓ Interpreterprogramm
Aufrufsequenz von Operationen
Beispiel:
1
2
4
Codeoptimierung
while x > y do
z := x + 1;
y := y + z
od
In dieser while – Anweisung bleibt der Wert von x konstant, d.h. z ändert sich
nach der ersten Anweisung nicht mehr.
Übersetzer- und Interpretertechniken sind kombinierbar (s. Java: Übersetzung in
Bytecode, der dann vom Bytecodeinterpreter interpretiert wird).
Sonderfall: inkrementelle Übersetzung: Der Übersetzungsvorgang benötigt nicht
das gesamte Quellprogramm (bzw. den vollständigen attributierten Codebaum),
sondern übersetzt bereits Teile (Anfangsstück des Quellprogramms), bevor das gesamte Quellprogramm vorliegt.
Inkrementelle Interpretation: Der Interpretationsvorgang beginnt bereits, ohne
dass das gesamte Quellprogramm vorliegt.
3.1
Lexikalische Analyse
Es existiert eine Menge W ⊆ V ∗ von Wörtern unserer Programmiersprache. Dabei
existieren in der Regel eine Reihe von Trennzeichen Z ⊆ V , die in Wörtern aus W
nicht vorkommen, d.h. W ⊆ (V \ Z)∗ .
Beispiele für Trennzeichen:
• Zeilenvorschübe
• Leerzeichen
Diese Trennzeichen dienen der Abspaltung von Wörtern.
haber nicht heutei ∈ V ∗
haberi , hnichti , hheutei ∈ (V \ Z)∗ , Z = { }
Frage: Sind Sonderzeichen (+, >, ≥, . . . ) in frei gewählten Bezeichnern (Identifikatoren) zulässig (z.B. a+, b-)? Wenn ja, dann müssen die Regeln erzwingen, dass
Formeln sauber in Wörter / Symbole aufgespalten werden.
Aufgabe:
(1) Beschreiben, welche Wörter in W liegen.
(2) Von gegebenem Text in V ∗ von links nach rechts durchgehend eine Folge von
Wörtern abspalten.
Die Beschreibung erfolgt in der Regel durch einfachste Mittel wie endliche Automaten oder reguläre Grammatiken (s. später).
Idee der lexikalischen Analyse: Der vorliegende Text wird zeichenweise von links
nach rechts gelesen; sobald ein Wort erkannt wurde, wird das in die Symboltabelle eingetragen und der Lesevorgang wird fortgesetzt. Trennzeichen werden unterdrückt.
242
KAPITEL 3. INTERPRETATION UND ÜBERSETZUNG . . .
Wenn Fehler auftreten, d.hh. wenn eine Zeichenfolge auftritt, die nicht im Wortschatz ist, wird eine Fehlermeldung abgesetzt.
Über Trennzeichen kann die lexikalische Analyse auch nach einem Fehler wieder
aufgesetzt werden.
Beispiel für Fehler: unzulässige Zeichenkombinationen
Die lexikalische Analyse ist eine in der Regel einfache Aufgabe. Scanner werden
meist nicht von Hand geschrieben, sondern aus standardisierten Beschreibungen des
Wortschatzes automatisch generiert.
3.2
Zerteilen von Programmen
Durch die lexikalische Analyse ist unser Programmtext in eine Folge von Wörtern /
Symbolen (technisch repräsentiert duch Indexwerte aus einer Tabelle) umgewandelt
worden. Diese Folge wird im Zerteilvorgang vom Parser (Zerteiler) in einen Baum
umgewandelt. Die Struktur des Baumes folgt der syntaktischen Beschreibung der
Programmiersprache.
hexpi ::= hif expi | hexpi hopi hexpi |(hexpi)| . . .
hif expi ::= if hexpi then hexpi else hexpi f i
Der Zerteilbaum zeigt an, wie sich das vorliegende Wort aus der BNF – Beschreibung
der Sprache ableiten lässt. Im Zerteilbaum sind die Blätter die Terminalzeichen
und die Markierungen im Inneren die Nichtterminalzeichen. Zerteilbäume können
in Programmiersprachen wie allgemein repräsentiert werden.
fct parser = (seq index s, table t) tree
Frage: Welche Datenstruktur für den Zerteilbaum? Zwei Mglichkeiten:
(1) universelle Baumstruktur
(2) spezielle Baumstruktur für die Sprache
Zu (2): Wir wandeln unsere BNF – Syntax in eine Folge von Sortenvereinbarungen
um:
1
2
4
sort exp =
|
|
|
|
ifexp (exp con, exp tb, exp eb)
opexp (exp le, op p, exp re)
klam (exp e)
idexp (id i)
...
Jedes Programm (d.h. jede Zeichenfolge, die der BNF – Beschreibung genügt) kann
in ein Datenelement der entsprechenden Sorte umgewandelt werden.
hexpi definiert eine formale Sprache, exp definiert eine Sorte, d.h. eine Menge von
Datenelementen (Bäume).
Wir können auch eine allgemeine Sorte definieren, die beliebige Bäume darzustellen erlaubt.
sort tree = paar (index k, seq tree t)
Beispiel:
1
2
if – Ausdruck
paar ("ifexp", <paar ("opexp", <paar ("id", <"a">),
paar ("op", <">">), paar ("id", <"b">)>),
paar ("id", <"a">), paar ("id", <"b">)>);
3.3. KONTEXTBEDINGUNGEN
3.3
243
Kontextbedingungen
Zerteilbäume sind Datenstrukturen. Diese erweitern wir um zusätzliche Attribute.
sort att exp = pair (exp e, expsort s)
Attributierung erfolgt durch das Ausrechnen der Werte der Attribute über den
Zerteilbäumen. Das Berechnen der Attribute auf den Zerteilbäumen erfolgt über
das Durchreichen von Informationen (Kontext) nach unten und das Berechnen
gewisser Werte von unten (also von den Blättern zur Wurzel hin). (Genaueres
in der Vorlesung Compilerbau).
3.4
Semantische Behandlung
Im Interpreter oder Compiler müssen wir die Semantik des Programms korrekt
wiedergeben.
244
KAPITEL 3. INTERPRETATION UND ÜBERSETZUNG . . .
Teil IV
245
247
• formale Sprache ↔ Beschreibungsmächtigkeit
• Berechenbarkeit ↔ Grenze der Algorithmik
• Komplexität ↔ Aufwand in Berechnungen für Probleme
• effiziente Algorithmen ↔ aufwändige Algorithmen optimieren
• (Logikprogrammierung)
• effiziente Datenstrukturen
• Datenbankmodelle
248
Kapitel 1
Formale Sprachen
Zeichensatz A
A∗ Menge der Wörter über A
L ⊆ A∗ formale Sprache
Frage: Mit welchen formalen Mitteln können wir Sprachen L ⊂ A∗ beschreiben?
1.1
1.1.1
Relation, Graphen
Zweistellige Relationen
Gegeben: Grundmenge M
zweistellige (binäre, dyadische) Relation: R ⊆ M × M
zur Notation:
(x, y) ∈ R gleichwertig xRy
Beispiel: ≤⊆ N × N
(1, 3) ∈≤
1≤3
Operationen auf Relationen: R, R1 , R2 ⊆ M × M
• Durchschnitt:
R1 ∩ R 2
• Vereinigung:
R1 ∪ R 2
• Komplement:
R− = (M × M ) \ R
• konverse Relation:
RT = {(x, y) ∈ M × M : (y, x) ∈ R}
• Relationenprodukt:
R1 ◦ R2 = {(x, z) ∈ M × M : ∃y ∈ M : (x, y) ∈ R1 ∧ (y, z) ∈ R2 }
Monotonie:
R1 ⊆ R10 ∧ R2 ⊆ R20 ⇒ R1 ◦ R2 ⊆ R10 ◦ R20
Frage: Wie Relationen definieren?
• durch Aufzählung: {(3, 7), (7, 25), . . . }
249
250
KAPITEL 1. FORMALE SPRACHEN
7
25
3
32
Abbildung 1.1: graphische Relationsdefinition
• logisch: {(x, y) : P (x, y)} (P . . . Prädikat)
• graphisch (vgl. Abb. 1.1)
• boolesche
3
3 0
7 0
25 0
32 0
Matrizen:
7 25 32
L 0
L
0 L
0
0
0
L
L 0
0
Spezielle Relationen über M :
• Nullrelation: 0M = ∅
• vollständige Relation: LM = (M × M )
• Identität: IM = {(x, x) ∈ M × M }
Eigenschaften von Relationen R ⊆ M × M :
• reflexiv: IM ⊆ R
• symmetrisch: R = RT
• antisymmetrisch: R ∩ RT ⊆ IM
• asymmetrisch: R ∩ RT = 0M
• transitiv: R ◦ R ⊆ R
• irreflexiv. IM ∩ R = 0M
• linkseindeutig R ◦ RT ⊆ IM (injektiv)
• rechtseindeutig RT ◦ R ⊆ IM
• linkstotal IM ⊆ R ◦ RT
• rechtstotal IM ⊆ RT ◦ R (surjektiv)
Relationsarten:
Quasiordnung, Präordnung transitiv, reflexiv
Striktordnung
transitiv, asymmetrisch
partielle Ordnung
transitiv, antisymmetrisch, reflexiv
lineare Ordnung
transitiv, antisymmetrisch, reflexiv, R ∪ RT = LM
Äquivalenzrelation
transitiv, reflexiv, symmetrisch
partielle Funktion
rechtseindeutig
totale Funktion
rechtseindeutig, linkstotal
Für eine gegebene Relation R ⊂ M × M heißt ein Element x ∈ M :
1.1. RELATION, GRAPHEN
251
• maximal ⇔ ∀y ∈ M : xRy ⇒ x = y
• größtes Element ⇔ ∀y ∈ M : yRx
Eine zweistellige Relation nennen wir auch gerichteten Graph, eine symmetrische
zweistellige Relation auch ungerichteten Graph.
Wir können für einen Graph R
• Kantenmarkierungen: β : R → S
• Knotenmarkierungen: α : M → S 0
einführen.
1.1.2
Wege in Graphen, Hüllenbildung
Iteriertes Relationenprodukt: R ⊆ M × M
R0 = IM
Ri+1 = Ri ◦ R
Ein Weg im Graph R von x0 nach xn der Länge n ist eine Folge von Knoten x0 , . . . , xn ∈ M mit ∀i, 1 ≤ i ≤ n : xi−1 Rxi
Satz: Ein Weg von x nach y der Länge n existiert genau dann, wenn xRn y.
Beweis: durch Induktion über n.
Ist R eine partielle Ordnung, dann heißen Wege auch Ketten. Wir lassen dann
auch unendliche Wege zu.
R heißt Noethersch, wenn es keine unendlichen Wege gibt, in denen sich Knoten
nicht wiederholen (streng aufsteigende Ketten).
Beispiel:
Graphen über N:
(a) 0 → 1 → 2 → 3 → . . . nicht Noethersch
(b) · · · → 3 → 2 → 1 → 0 Noethersch
Hüllenbildung
Gegeben sei eine Relation R ⊆ M × M .
• reflexive Hülle
Rrefl = R ∪ IM =
\
{T ⊆ M × M : R ⊆ T ∧ T reflexiv}
• transitive Hülle
R+ =
\
{T ⊆ M × M : R ⊆ T ∧ T transitiv}
• reflexiv – transitive Hülle
\
R∗ =
{T ⊂ M × M : R ⊆ T ∧ T transitv ∧ T reflexiv} = (R ∪ IM )+
• symmetrische Hülle
Rsym = R ∪ RT =
\
{T ⊆ M × M : R ⊆ T ∧ T symmetrisch}
• symmtrisch – reflexiv – transitive Hülle
\
R∗ =
{T ⊂ M × M : R ⊆ T ∧ T transitv ∧ T reflexiv ∧ T symmetrisch}
252
KAPITEL 1. FORMALE SPRACHEN
a
b
x
d
f
e
c
z
g
h
Abbildung 1.2: Berechnungswege
Zur Notation: Relation →, wir schreiben:
∗
→ für die transitiv – reflexive Hülle
∗
↔ für die symmetrisch – reflexiv – transitive Hülle.
Eine Relation → heißt konfluent, wenn gilt:
∗
∗
∗
∗
∀x, y1 , y2 ∈ M : (x → y1 ∧ x → y2 ) ⇒ ∃z ∈ M : y1 → z ∧ y2 → z
∗
∗
Relationenalgebra: RT ◦ R∗ ⊆ R∗ ◦ RT
Sei R konfluent und Noethersch, dann gilt: jeder Weg (jede Berechnung) ausgehend von x endet in einem Punkt z (vgl. Abb. 1.2).
1.2
Grammatiken
Formale Sprachen sind in der Regel unendlich und können deshalb nicht durch
einfaches Aufzählen beschrieben werden. Deshalb brauchen wir Regeln (endliches
Regelsystem), die die Wörter einer Sprache charakterisieren.
Wir verwenden Textersetzungsregeln x, y ∈ V ∗ × V ∗ (V Zeichenmenge).
x→y
→⊆ V ∗ × V ∗
→ ist eine endliche Menge von Textersetzungsregeln.
Wir gewinnen aus → eine in der Regel unendliche Relation durch Regelanwendung
(α, ω ∈ V ∗ ):
x→y ⇒α◦x◦ω α◦y◦ω
: algebraische Hülle, Halbgruppenhülle
∗
ω1 ω2 : ω1 wird in ω2 durch eine Folge von Ersetzungsschritten überführt.
(V ∗ , ) heißt Semi – Thue – System (ist → symmetrisch, dann auch Thue –
System).
Beispiel:
V = {a, b, c}
(1)
ba → ab
ca → ac
cb → bc
+
Noethersch? ja
+
konfluent? ja
Jedes Wort wird duch eine terminierende Berechnung auf die Form ai bk cj
gebracht.
∗
ist eine Äquivalenzrelation
1.2. GRAMMATIKEN
253
(2) zusätzliche Regeln zu (1):
aa → a
bb → b
cc → c
(3) zusätzliche Regeln zu (1):
aa → a
bc → a
Normalformen: abi
ack
a
bi
ck
ab → b
ac → c
Normalformen: a bi
ck
∗
Für jedes Semi – Thue – System (V ∗ , ) erhalten wir durch eine Äquivalenzrelation. [ω] bezeichnet die Äquivalenzklasse.
∗
V ∗ / bildet mit Konkatenation als Verknüpfung eine Halbgruppe mit neutralem
Element [ε], genannt Regelgrammatikmonoid.
1.2.1
Reduktive und generative Grammatiken
Eine Grammatik über einer (endlichen) Zeichenmenge M ist ein Tripel (M, →, z),
wobei →∈ M ∗ × M ∗ endlich ist. z ∈ M heißt Axiom (Wurzel).
G = (M, →, z) heißt auch Semi – Thue – Grammatik. Grammatiken beschreiben
formale Sprachen.
generativ: Ein Wort w ∈ M ∗ ist in der durch G beschriebenen formalen Sprache,
∗
wenn hzi w gilt.
Generativer Sprachschatz:
n
o
∗
Lg (G) = w ∈ M ∗ : hzi w
reduktiv: Ein Wort w ∈ M ∗ ist in der durch G beschriebenen formalen Sprache,
∗
wenn w hzi gilt.
Reduktiver Sprachschatz:
n
o
∗
Lr (G) = w ∈ M ∗ : w hzi
Sprechweise:
w ∈ Lg (G): w wird durch G generiert (erzeugt).
w ∈ Lr (G): w wird durch G erkannt.
Eine generative Grammatik heißt ε – frei oder ε – produktionsfrei, wenn die
rechte Seite jeder Regel nicht ε ist. Analog heißt eine reduktive Grammatik ε – frei
oder ε – produktionsfrei, wenn die linke Seite jeder Regel nicht ε ist.
Semi – Thue – Grammatiken haben enge Grenzen in der Ausdrucksmächtigkeit.
254
KAPITEL 1. FORMALE SPRACHEN
Beispiel: Mn= {L}
o
k
Sprache: S = L2 : k ∈ N ⊆ M ∗
Behauptung: Diese Sprache ist durch Semi – Thue – Grammatiken nicht beschreibbar.
Beweis: Jede Regel hat die Form Li → Lj . Wir betrachen den reduktiven Fall. Wir
benötigen mindestens eine Regel der Form Li → L mit i > 1. L muss also Axiom
sein.
S 63 Li Li−1 Li L
Chomsky – Grammatiken strukturieren den Zeichensatz: M = T ∪N mit T ∩N =
∅.
T : Terminalzeichen — Zeichen in Wörtern der formalen Sprache
N : Nichtterminalzeichen — Hilfszeichen für die Ableitung
Eine Chomsky – Grammatik ist ein Quadrupel G = (T, N, →, z) mit (T ∪ N, →
, z) Semi – Thue – Grammatik, z ∈ N und T ∩ N = ∅.
Lg (G) = Lg (T ∪ N, →, z) ∩ T ∗
Lr (G) = Lr (T ∪ N, →, z) ∩ T ∗
Beispiel: Reduktive Chomsky – Grammatik zur Beschreibung der Sprache S =
k
{L2 : k ∈ N}.
T = {L} N = {z, A, B, C} z Axiom
Regeln:
L→A
ε→B
AAB → CB
AAC → CA
BC → BA
BAB → z
Wir betrachen den Sprachschatz der Semi – Thue – Grammatik, die wir erhalten,
indem wir die ersten zwei Regeln weglassen. hBABi ist im Sprachschatz.
(1) Jedes Wort im Sprachschatz hat die Form hBi ◦ x ◦ hBi mit x ∈ {AC}∗ .
(2) In Rückwärtsableitung entstehen Cs immer am linken Rand. Um die Cs wieder
zu entfernen, müssen sie nach rechts geschoben werden und verdoppeln dabei
die Anzahl der As, d.h. die Wörter aus {AB}∗ im Sprachschatz haben die
k
Form BA2 B.
Zwei reduktive (bzw. generative) Grammatiken heißen äquivalent, wenn sie
die gleiche formale Sprache beschreiben.
Eine reduktive Grammatik heißt wortlängenmonoton, wenn für jede ihrer Regeln w → w0 gilt |w| ≥ |w0 |. Gilt sogar |w| > |w0 |, so heißt die Grammatik strikt
wortlängenmonoton. Konsequenz für Ableitungen:
w0
w1
. . . wn
|w0 | ≥ |w1 | ≥ . . . ≥ |wn |
Eine Ersetzungsregel heißt separiert, wenn w0 ∈ N + ist.
1.2.2
Die Sprachhierarchie nach Chomsky
Chomsky – Grammatiken lassen sich nach der äußeren Form ihrer Regeln klassifizieren.
1.2. GRAMMATIKEN
255
Chomsky – 0 – Sprachen sind formale Sprachen, die durch Chomsky – Grammatiken beschrieben werden können.
Wir beschränken uns im Folgenden auf reduktive Grammatiken (generative analog).
Eine Ersetzungsregel der Form u ◦ a ◦ v → u ◦ hbi ◦ v mit u, v ∈ (T ∪ N )∗ ,
a ∈ (T ∪ N )+ , b ∈ N heißt kontextsensitiv. Eine Chomsky – Grammatik heißt
kontextsensitiv oder ε – produktionsfreie Chomsky – 1 – Grammatik,
wenn alle ihrer Regeln kontextsensitiv sind.
Trivialerweise gilt für alle Chomsky – 1 – Grammatiken: Alle Regeln sind wortlängenmonoton.
Eine formale Sprache heißt kontextsensitiv oder Chomsky – 1 – Sprache,
wenn es eine Chomsky – 1 – Grammatik gibt, die die Sprache beschreibt. Ist eine
Sprache S kontextsensitiv, so wird S ∪ {ε} auch kontextsensitiv genannt.
Eine Regel a → hbi mit a ∈ (T ∪ N )∗ und b ∈ N heißt kontextfrei. Sind
alle Regeln der Grammatik kontextfrei, so heißt die Grammatik Chomsky – 2 –
Grammatik oder kontextfrei. Jede Sprache, die durch eine Chomsky – 2 –
Grammatik beschreibbar ist, heißt kontextfrei.
Hinweis: BNF entspricht Chomsky – 2 – Grammatiken.
Eine kontextfreie Regel der Form m ◦ hai ◦ n → hbi mit a, b ∈ N und m, n ∈ T +
heißt beidseitig linear. Gilt jedoch n = ε, so heißt die Regel rechtslinear bzw. für
m = ε linkslinear. Eine Regel der Form w → hbi mit w ∈ T + und b ∈ N heißt
terminal.
Eine Chomsky – 2 – Grammatik, deren sämtliche Regeln terminal oder rechtslinear
(bzw. terminal oder linkslinear) sind, heißt Chomsky – 3 – Grammatik oder
regulär. Eine Sprache heißt regulär oder Chomsky – 3 – Sprache, wenn sie
durch eine reguläre Grammatik beschreibbar ist.
C0 L
Menge der Chomsky – 0 – Sprachen
CSL Menge der Chomsky – 1 – Sprachen
CF L Menge der Chomsky – 2 – Sprachen
REG Menge der Chomsky – 3 – Sprachen
Satz: REG ( CF L ( CSL ( C0 L
1.2.3
Strukturelle Äquivalenz von Ableitungen
Ableitung über einer Grammatik:
w0 w1 w2 . . . wn
Fragen von Interesse:
∗
(1) Was ist ableitbar? ()
(2) Welche Struktur hat eine Ableitung?
hBAAAABi → hBAACBi → hBCABi → hBAABi → . . .
Die Anwendung von Regeln t1 → t01 und t2 → t02 in einem Wort
α ◦ t1 ◦ β ◦ t2 ◦ γ α ◦ t01 ◦ β ◦ t2 ◦ γ α ◦ t01 ◦ β ◦ t02 ◦ γ
mit α, β, γ ∈ (T ∪ N )∗ heißt vertauschbar.
Bemerkung: Formal ist jede Ableitung ein Wort aus (T ∪ N ∪ {})∗ . Vertauschbarkeit definiert eine Relation auf der Menge der Ableitungen. Wir können zu dieser
Relation die transitive symmetrische reflexive Hülle bilden. Stehen zwei Ableitungen
in dieser Relation, dann heißen sie strukturell äquivalent.
Bei Chomsky – 2 – Grammatiken können wir aufgrund der einfachen Struktur
ihrer Regeln jeder Ableitung auf das Axiom einen Strukturbaum zuordnen.
256
KAPITEL 1. FORMALE SPRACHEN
z
z
z
z z
z
z
z
(a * a) + a + a
Abbildung 1.3: Strukturbaum
Beispiel
Regeln:
N = {z}, T = {(, ), +, ∗, a}, Axiom z.
z+z →z
z∗z →z
a→z
(z) → z
Strukturbaum: s. Abb. (1.3).
Wichtig: Zwei Ableitungen sind genau dann strukturell äquivalent, wenn sie den
gleichen Strukturbaum darstellen, d.h. Strukturbäume sind Normalformen in der
Klasse der strukturell äquivalenten Ableitungen.
Ein Strukturbaum für eine Chomsky – 2 – Grammatik ist ein endlich verzweigender Baum:
(1) Die Wurzel ist das Axiom.
(2) Jeder Teilbaum hat folgende Eigenschaft:
• Er ist genau ein Terminalzeichen und hat keine Teilbäume
oder
• er hat als Wurzel ein Nichtterminalzeichen x; dann gilt: die Wurzeln
seiner Teilbäume können zu dem Wort w konkateniert werden und w → x
ist eine Regel der Grammatik.
∗
Eine Grammatik heißt eindeutig, wenn für jedes Wort w ∈ T ∗ mit w z (z
Wurzel) alle Ableitungen auf z strukturell äquivalent sind. Dann existiert genau ein
Strukturbaum für jedes Wort im Sprachschatz.
Feststellung: Die Grammatik aus obigem Beispiel ist nicht eindeutig.
1.2.4
Sackgassen, unendliche Ableitungen
Chomsky – 2 – Grammatiken werden praktisch eingesetzt, um:
(1) eine formale Sprache zu definieren
(2) jedem Wort im Sprachschatz einen Strukturbaum (am besten eindeutig) zuzuordnen.
1.2. GRAMMATIKEN
257
?
E
E
E
R
R
R
R
a * b + c
E
E
R
R
R
a * b + c
Abbildung 1.4: Ableitung mit Sackgasse
Ein Wort w heißt im Sprachschatz bzw.
∗
Grammatik akzeptiert, wenn w z gilt.
Aufgaben für eine gegebene Grammatik:
reduzierbar oder auch
von der
(1) Stelle fest, ob ein gegebenes Wort im Sprachschatz ist.
(2) Ermittle (falls es im Sprachschatz ist) den Strukturbaum.
Naive Idee: Wende auf das gegebene Wort Regeln an. Wir erhalten eine Ableitung
w → w1 → w2 → . . . .
Wir erhalten zwei Möglichkeiten:
(1) Die Ableitung führt zu einem Wort, auf das keine Regel mehr anwendbar ist.
(2) Die Ableitung bricht nicht ab und kann unendlich fortgesetzt werden.
Falls (1) gilt und das Wort, auf das keine Regel mehr anwendbar ist, das Axiom ist,
so ist w im Sprachschatz.
Falls (1) gilt und das Wort, auf das keine Regel mehr anwendbar ist, nicht das
Axiom ist, dann können wir daraus nicht schließen, dass s nicht im Sprachschatz
ist. Wir sprechen von einer Sackgasse.
Eine unendliche Ableitung (Fall (2)) sagt auch nichts darüber aus, ob das Wort im
Sprachschatz ist.
Beispiel: Einfache arithmetische Ausdrücke als Chomsky – 2 – Grammatik
T = {a, . . . , z, +, ∗, (, )}, N = {R, E}, Wurzel: E
Regeln:
a→R
..
.
z→R
R∗R→R
E+R→R
R→E
(E) → R
Ableitungen: s. Abb. (1.4).
258
KAPITEL 1. FORMALE SPRACHEN
Neue Grammatik: T wie vorher, N = {F, R, A, E}, Axiom E. Regeln:
a→F
..
.
z→F
F ∗R→R
R+A→A
F →R
R→A
A→E
(E) → F
Diese Grammatik ist eindeutig, aber wieder nicht sackgassenfrei.
In der Menge aller Ableitungen für ein Wort über einer Grammatik können wir
eine sogenannte Linksableitung (analog Rechtsableitung) auszeichnen. Dies ist eine
Ableitung, in der jeweils so weit links wie möglich eine Regel angewandt wird.
Achtung:
(1) In der Regel sind Linksableitungen nicht eindeutig.
(2) In der Menge der strukturell äquivalenten Ableitungen sind Linksableitungen
eindeutig.
Eine Linksableitung führt für ein Wort im Sprachschatz nicht unbedingt zum Axiom.
Akzeptierende Linksableitung: Regeln werden so weit links wie möglich angewendet,
Regeln, die nicht zum Axiom (also in Sackgassen) führen, werden übersprungen.
1.3
Chomsky – 3 – Sprachen, endliche Automaten,
reguläre Ausdrücke
Wir betrachten drei Beschreibungsmittel für formale Sprachen:
• reguläre Ausdrücke
• endliche Automaten
• Chomsky – 3 – Grammatiken
Wir werden zeigen: Alle drei Beschreibungsmittel haben die gleiche Beschreibungsmächtigkeit. Sie beschreiben die gleiche Klasse formaler Sprachen, genannt reguläre Sprachen.
1.3.1
Reguläre Ausdrücke
Gegeben sei eine Zeichenmenge T . Die Syntax und Semantik der regulären Ausdrücke ist wie folgt gegeben:
(1) einfache reguläre Ausdrücke
• x mit x ∈ T ist regulärer Ausdruck mit Sprache {hxi}.
• ε ist regulärer Ausdruck mit Sprache {ε}.
• {} ist regulärer Ausdruck mit Sprache ∅.
(2) zusammengesetzte reguläre Ausdrücke (seien X, Y reguläre Ausdrücke)
1.3. CHOMSKY – 3 – SPRACHEN, ENDLICHE AUTOMATEN, . . .
259
• [X|Y ] ist regulärer Ausdruck mit Sprache LX ∪ LY .
• [XY ] ist regulärer Ausdruck mit Sprache {x ◦ y : x ∈ LX ∧ y ∈ LY }.
• X ∗ ist regulärer Ausdruck mit Sprache {x1 ◦· · ·◦xn : n ∈ N∧x1 , . . . , xn ∈
LX }.
(wobei LX die formale Sprache zum regulären Ausdruck X und LY die formale
Sprache zum regulären Ausdruck Y sei).
X + = XX ∗
Gesetze der regulären Ausdrücke:
• [[X|Y ]|Z] = [X|[Y |Z]]
• [[XY ]Z] = [X[Y Z]]
• [X|Y ] = [Y |X]
• [[X|Y ]Z] = [[XZ]|[Y Z]]
• [X[Y |Z]] = [[XY ]|[XZ]]
• {}∗ = ε
• Xε = X
• X{} = {}
• X ∗ = XX ∗ |ε
• X ∗ = [X|ε]∗
1.3.2
Endliche Automaten
Endlicher Automat A = (S, T, s0 , SZ , δ):
S
endliche Menge von Zuständen
T
endliche Menge von Eingangszeichen
s0 ∈ S
Anfangszustand
SZ ⊆ S Menge der Endzustände
δ : S × (T ∪ {ε}) → 2S (= ℘(S) [Potenzmenge von S])
δ(s, t)
Menge der Nachfolgezustände zu s ∈ S, t ∈ T ∪ {ε}
Deterministischer Automat:
δ(s, ε) ⊆ {s}
|δ(s, t)| ≤ 1 ∀s ∈ S, t ∈ T
Partieller Automat:
∃t ∈ T, s ∈ S : δ(s, t) = ∅
ε – freier Automat:
δ(s, ε) = ∅
∀s ∈ S
260
KAPITEL 1. FORMALE SPRACHEN
a
a
s1
s0
s2
b
Abbildung 1.5: endlicher Automat
Beispiel: A = ({s0 , s1 , s2 } , {a, b} , s0 , {s1 } , δ)
(s. Abb. 1.5)
δ
ε
a
b
s0 ∅ {s1 }
∅
s1 ∅ {s2 }
∅
∅
{s1 }
s2 ∅
a
Notation: Wir schreiben s1 → s2 , falls s2 ∈ δ(s1 , a).
w∗
Jedem Wort w ∈ T ∗ ordnen wir eine Relation → auf Zuständen zu:
∗
ε ∗
ε
→ = →
hai
∗
ε ∗
a
ε ∗
→ =→ ◦ → ◦ →
w1 ◦w2 ∗
→
w ∗
w ∗
=→1 ◦ →2
w1 , w2 ∈ T ∗
w∗
Wir schreiben δ ∗ (s, w) für {s0 : s → s0 }. Damit können wir die Sprache charakterisieren, die durch einen Automaten A beschrieben wird:
n
o
w∗
L(A) = w ∈ T ∗ : ∃z ∈ SZ : s0 → z
1.3.3
Äquivalenz der Darstellungsformen
Zunächst beantworten wir die Frage, ob Automaten mit spontanen Übergängen
beschreibungsmächtiger sind als Automaten ohne spontane Übergänge.
Satz: Zu jedem endlichen Automaten existiert ein ε – freier endlicher Automat,
der die gleiche Sprache akzeptiert.
Beweisidee: Wir konstruieren zu einem gegebenen endlichen Automaten einen ε –
freien Automaten mit gleicher Sprache:
ε ∗
ε ∗
(1) → – Zyklen: Wir fassen Knoten, die durch → – Zyklen verbunden sind, zu
ε ∗
einem Knoten zusammen. Es entsteht ein Automat ohne → – Zyklen (ε –
Schlingen können ohnehin weggelassen werden).
ε ∗
(2) → – Wege (zyklenfrei): Durchschalten von ε – Übergängen (vgl. Abb. 1.6).
Satz: Zu jedem ε – freien nichtdeterministischen Automaten existiert ein ε –
freier deterministischer Automat mit gleichem Sprachschatz.
Beweisidee: Wir konstruieren zu dem Automaten mit Zustandsmenge S einen Automaten mit Zustandsmenge 2S (vgl. Abb. 1.7).
1.3.4
Äquivalenz von regulären Ausdrücken, endlichen Automaten und Chomsky – 3 – Grammatiken
Satz: Zu jedem endlichen Automaten existiert ein regulärer Ausdruck, der die gleiche Sprache beschreibt.
Beweis: (konstruktiv)
1.3. CHOMSKY – 3 – SPRACHEN, ENDLICHE AUTOMATEN, . . .
261
a
a
a
Abbildung 1.6: ε – Übergänge durchschalten
a
s1
s0
a
s2
a
s0
{s 1, s2}
Abbildung 1.7: Übergang von nichtdeterministischen zu deterministischen Automaten
Wir geben zu einem gegebenen endlichen Automaten einen regulären Ausdruck mit
gleichem Sprachschatz an.
Zustandsmenge: S = {s1 , . . . , sn }
O.B.d.A. sei der endliche Automat ε – frei.
Wir konstruieren eine Familie formaler Sprachen L(i, j, k) ⊆ T ∗ , wobei i, j ∈
{1, . . . , n}, k ∈ {0, . . . , n}, und die dazugehörigen regulären Ausdrücke E(i, j, k),
die jeweils die formale Sprache L(i, j, k) beschreiben. Dabei sei
w
L(i, j, k) = w ∈ T ∗ : si → sj
k
w
In → betrachten wir dabei nur Pfade in Übergangsgraphen mit Zwischenzuständen
k
sl mit l ≤ k.
L(i, j, 0) = {hai : sj ∈ δ(si , a)}
a
E(i, j, 0) = a1 | . . . |aq (Menge aller a ∈ T mit si → sj )
L(i, j, k + 1) = L(i, j, k) ∪ {u ◦ v ◦ w : u ∈ L(i, k + 1, k)∧
v ∈ L(k + 1, k + 1, k)∗ ∧ w ∈ L(k + 1, j, k)}
E(i, j, k + 1) = E(i, j, k)|E(i, k + 1, k)E(k + 1, k + 1, k)∗ E(k + 1, j, k)
Die Sprache der endlichen Automaten sind die Wege vom Anfangszustand s1 zu
den Endzuständen SZ = {sZ1 , . . . , sZp }. Dies entspricht den regulären Ausdrücken
E(1, Z1 , n)| . . . |E(1, Zp , n).
Satz: Zu jedem regulären Ausdruck lässt sich ein endlicher Automat angeben,
der die gleiche Sprache beschreibt.
Beweis: Wir geben die Konstruktionsprinzipien an.
262
KAPITEL 1. FORMALE SPRACHEN
x
(a)
(b)
(c)
Abbildung 1.8: Endliche Automaten zu elementaren regulären Ausdrücken
X
Y
Abbildung 1.9:
(1) Für elementare reguläre Ausdrücke: Sei x ∈ T , dann ist x regulärer Ausdruck
mit einer Sprache, die durch den endlichen Automaten aus Abb. (1.8(a)) beschrieben wird. ε ist regulärer Ausdruck mit endlichem Automaten (1.8(b)).
(1.8(c)) ist Automat für die leere Sprache.
(2) Seien X und Y reguläre Ausdrücke, die durch die endlichen Automaten aus
Abb. (1.9) beschrieben werden.
Für [X|Y ] akzeptiert (1.10(a)) die gleiche Sprache. Für XY akzeptiert der
endliche Automat (1.10(b)) die gleiche Sprache. Automat zu X ∗ ist (1.10(c)).
Die Überlegungen zeigen: Zu jedem regulären Ausdruck können wir einen endlichen Automaten angeben, der die gleiche formale Sprache beschreibt — vorausgesetzt, wir finden bei zusammengesetzten regulären Ausdrücken endliche Automaten
für die Unterausdrücke.
Vollständige Induktion über die Anzahl der Symbole in einem regulären Ausdruck
liefert die Behauptung.
Satz: Zu jedem deterministischen ε – freien endlichen Automaten können wir
eine Chomsky – 3 – Grammatik angeben, die die gleiche Sprache beschreibt.
Beweis: Gegeben sei der endliche Automat A = (S, T, s0 , SZ , δ). OBdA gehe keine
Kante nach s0 . Jeder Kante (d.h. jedem Übergang δ(s, a) = s0 ) ordnen wir eine
Regel der Grammatik zu. Wir verwenden die Zustände als Nichtterminalzeichen.
Für Übergänge vom Anfangszustand s0 aus:
δ(s0 , a) = si
verwenden wir die Regel
a → si
Für Übergänge
δ(si , a) = sj
führen wir die Regel
si a → sj
ein.
Zu den Zuständen als Nichtterminalzeichen nehmen wir noch ein Sonderzeichen, die
Wurzel Z (oBdA sei Z ∈
/ S) hinzu. Für jeden Zustand si ∈ SZ nehmen wir die
Regel
si → Z
hinzu. Die entstehende Grammatik akzeptiert die gleiche Sprache.
1.3. CHOMSKY – 3 – SPRACHEN, ENDLICHE AUTOMATEN, . . .
X
Y
263
(a)
X
Y
(b)
X
(c)
Abbildung 1.10: Endliche Automaten zu zusammengesetzten regulären Ausdrücken
264
KAPITEL 1. FORMALE SPRACHEN
Satz: Zu jeder Chomsky – 3 – Grammatik existiert ein endlicher Automat, der
die gleiche Sprache akzeptiert.
Beweis: OBdA sei die Chomsky – 3 – Grammatik linkslinear und alle linkslinearen
Regeln von der Form sa → s0 mit a ∈ T . Wir geben einen endlichen Automaten an:
Zustände
S = N ∪ {s0 }
Eingabezeichen T
Anfangszustand s0
Endzustand
{Z}
Übergangsfunktion
∀s ∈ N, a ∈ T : δ(s, a) = {s0 ∈ S : sa → s0 ist Regel der Grammatik}
∀s ∈ N : δ(s, ε) = {s0 ∈ S : s → s0 ist Regel der Grammatik}
∀a ∈ T : δ(s0 , a) = {s0 ∈ S : a → s0 ist Regel der Grammatik}
Es entsteht ein Automat, der die gleiche Sprache wie die Grammatik beschreibt:
Jede Ableitung in der Grammatik entspricht einer Folge von Zustandsübergängen
im Automaten und umgekehrt.
Fazit
• Die Beschreibungsmittel
– endliche Automaten
– reguläre Ausdrücke
– Chomsky – 3 – Grammatiken
sind gleichmächtig (beschreiben die gleiche Klasse formaler Sprachen).
• Die Übergänge sind konstruktiv: Zu jedem endlichen Automaten (regulären
Ausdruck, Chomsky – 3 – Grammatik) können wir systematisch (durch einen
Algorithmus) einen regulären Ausdruck (Chomksy – 3 – Grammatik, endlichen
Automaten) angeben, der die gleiche formale Sprache beschreibt.
Solche Sprache heißen regulär.
Wir wenden uns nun der Frage zu, wie wir für eine vorgegebene formale Sprache
erkennen können, dass sie regulär ist. Wir beginnen mit einem einfachen Beispiel
für eine nichtreguläre Sprache:
Lab = {an bn : n ∈ N \ {0}}
Diese Sprache können wir ohne Probleme durch eine Chomksy – 2 – Grammatik
beschreiben:
T = {a, b}, N = {Z}, Z Wurzel
ab → Z
aZb → Z
Behauptung: Die Sprache Lab ist nicht regulär.
Beweis: Wir zeigen, dass kein endlicher Automat existiert, der Lab akzeptiert.
(1) Der endliche Automat hat nur endlich viele Zustände (sei h die Anzahl der
Zustände).
1.3. CHOMSKY – 3 – SPRACHEN, ENDLICHE AUTOMATEN, . . .
265
(2) an bn und am bm sind im Sprachschatz, d.h. es existieren Zustände sn , sm und
sz , so dass gilt:
an
∗
bn
∗
s0 → sn ∧ sn → sz
am
∗
bm
∗
s0 → sm ∧ sm → sz
Falls sn = sm gilt, werden auch Wörter an bm bzw. am bn akzeptiert. Da nur
endlich viele Zustände (genaugenommen k) existieren, muss es Zahlen n und
m geben mit n 6= m und sn = sm . Widerspruch.
Problem bei endlichen Automaten: Endliche Automaten können nur eine endliche Information speichern, können nicht unbeschränkt zählen.
Sprachen mit Klammerstrukturen (beliebiges Nesten von Öffnenden und schließenden Klammern) sind nicht regulär.
Satz (Pumping – Lemma für reguläre Sprachen): Zu jeder regulären Sprache L
existiert eine Zahl n ∈ N, so dass für alle Wörter w ∈ L mit |w| ≥ n gilt: w lässt
sich in Wörter x, y, z ∈ T ∗ zerlegen: w = x ◦ y ◦ z mit |y| ≥ 1, |x ◦ y| ≤ n und
x ◦ y i ◦ z ∈ L für alle i ∈ N \ {0}.
Beweis: L ist nach Voraussetzung regulär. Es existiert ein deterministischer endlicher ε – freier Automat, der L akzeptiert. Sei n die Anzahl seiner Zustände und
w∗
w ∈ L. Es gilt s0 → sz . Dabei durchläuft der Automat |w| + 1 Zustände. Gilt
|w| ≥ n, so tritt ein Zustand mindestens zwei Mal auf, d.h.
x∗
y ∗
x∗
yi
z ∗
s0 → sh ∧ sh → sh ∧ sh → sz
Dann gilt auch
∗
z ∗
s0 → sh ∧ sh → sh ∧ sh → sz
Damit lässt sich auch zeigen: Lab ist nicht regulär.
1.3.5
Minimale Automaten
Wir zeigen nun, wie wir für eine reguläre Sprache einen minimalen Automaten (d.h.
einen Automaten mit einer minimalen Anzahl von Zuständen) angeben können.
Sei also L ⊆ T ∗ . Wir definieren eine Äquivalenzrelation ∼L durch (seien v, w ∈ L):
v ∼L w ⇔def ∀u ∈ T ∗ : v ◦ u ∈ L ⇔ w ◦ u ∈ L
Dadurch erhalten wir Äquivalenzklassen
[v]L = {w ∈ T ∗ : w ∼L v}
Satz (Myhill – Nerode): L ist genau dann regulär, wenn die Menge der Äquivalenzklassen endlich ist.
Beweisidee: Jede Äquivalenzklasse definiert einen endlichen Automaten, der die
Sprache akzeptiert. Umgekehrt: Jeder Zustand in einem endlichen Automaten entspricht einer Äquivalenzklasse.
Die Beweisidee liefert bereits einen Hinweis, wie wir einen ε – freien deterministischen endlichen Automaten mit totaler Übergangsfunktion konstruieren können,
der die Sprache L akzeptiert und eine minimale Anzahl von Zuständen hat.
Wir zeigen nun, wie wir aus einem gegebenen endlichen ε – freien deterministischen Automaten einen minimalen Automaten konstruieren, indem wir bestimmte
Zustände zusammenlegen.
• 1. Schritt: Wir entfernen alle Zustände, die vom Anfangszustand aus nicht
erreichbar sind.
266
KAPITEL 1. FORMALE SPRACHEN
• 2. Schritt: Wir konstruieren eine Folge von Prädikaten auf Zuständen: pi :
S 0 × S 0 → B:
w∗
w∗
pi (s1 , s2 ) = ∀w ∈ T ∗ : |w| ≤ i ⇒ [(∃z ∈ SZ : s1 z) ⇔ (∃z ∈ SZ : s2 z)]
pi lässt sich induktiv definieren:
p0 (s1 , s2 ) = (s1 ∈ SZ ⇔ s2 ∈ SZ )
pi+1 (s1 , s2 ) = pi (s1 , s2 ) ∧ ∀x ∈ T : pi (δ(s1 , x), δ(s2 , x))
Da der Automat endlich ist, existiert ein k mit pk = pk+1 . Das Prädikat pk
definiert uns dann, welche Zustände des Automaten äquivalent sind und zu
einem Zustand im minimalen Automaten verschmolzen werden können.
Anwedung von regulären Ausdrücken: z.B. in grep, sed, vi, vim, emacs, lex, flex
1.4
Kontextfreie Sprachen und Kellerautomaten
Chomsky – 3 – Grammatiken genügen im Allgemeinen nicht, um komplexe formale
Sprachen wie Programmiersprachen zu beschreiben (s. z.B. Klammerstrukturen wie
in Lab = {an bn : n ∈ N\{0}}). Es gilt REG ( CF L, d.h. die Klasse der kontextfreien Sprachen ist mächtiger als die der regulären Sprachen. Chomsky – 3 – Grammatiken, reguläre Ausdrücke und endliche Automaten eignen sich nicht, um kontextfreie
Sprachen zu beschreiben. Analog zu regulären Ausdrücken, endlichen Automaten
und Chomsky – 3 – Grammatiken bei regulären Sprachen sind in der Klasse der
kontextfreien Sprachen die folgenden Beschreibungsformen gleichmächtig:
• Chomsky – 2 – Grammatiken
• BNF – Ausdrücke
• Kellerautomaten
Diese Mittel werden bei der Spezifikation und der Syntaxanalyse von Programmiersprachen in Übersetzern und Interpretern eingesetzt.
1.4.1
BNF – Notation
BNF ist eine Erweiterung der regulären Ausdrücke um:
(1) Hilfszeichen (Nichtterminale)
(2) (rekursive) Gleichungen für Hilfszeichen
Sei T eine Menge von Terminalzeichen und N eine Menge von Nichtterminalzeichen.
Eine BNF – Beschreibung einer Sprache L hat die Form
x1 ::= E1
..
.
xn ::= En
mit x1 , . . . , xn ∈ N und E1 , . . . , En reguläre Ausdrücke über T ∪ N .
1.4. KONTEXTFREIE SPRACHEN UND KELLERAUTOMATEN
267
Beispiel: hZi ::= ab|a hZi b beschreibt die kontextfreie Sprache {an bn : n ∈ N \
{0}}.
Für eine BNF – Beschreibung kann für jedes xi (1 ≤ i ≤ n) eine Chomsky –
2 – Grammatik angegeben werden. Die regulären Ausdrücke Ei werden in Chomsky – 3 – Grammatiken (mit neuem Nichtterminalzeichen Zi als Axiom) umgeformt
und für die BNF – Regel xi ::= Ei wird die Regel Zi → xi in die Grammatik
aufgenommen.
Anmerkung: Es gibt zahlreiche Stile und Erweiterungen von BNF. Die Klammern [. . .] dienen oft der Kennzeichnung optionaler Anteile, z.B. x[4] für x|x4. {x}m
n
bezeichnet die n- bis m-malige Wiederholung von x.
1.4.2
Kellerautomaten
Ein Kellerautomat ist ein 7-Tupel KA = (S, T, K, δ, s0 , k0 , SZ ) mit:
• einer endlichen Menge von Zuständen S
• einer endlichen Menge von Eingabezeichen T
• einer endlichen Menge von Kellerzeichen K
• einer endlichen Übergangsrelation δ : S × (T ∪ {ε}) × K → 2S×K
∗
• einem Anfangszustand s0 ∈ S
• einem Kellerstartsymbol k0 ∈ K
• und Endzuständen SZ ⊆ S
Prinzip: Ein Kellerautomat verarbeitet ein Wort w ∈ T ∗ , indem er w von links nach
rechts liest und in jedem Schritt, abhängig vom aktuellen Zustand s, vom Eingangszeichen a und vom obersten Kellerzeichen k entsprechend der Übergangsrelation δ
• in einen neuen Zustand s0 ∈ S übergeht,
• das oberste Zeichen k vom Keller entfernt und
• eine (ggf. leere) Sequenz u ∈ K ∗ auf den Keller legt.
Der Kellerautomat akzeptiert w ∈ T ∗ , wenn die vollständige Verarbeitung von w
von s0 aus zu einem Endzustand s ∈ SZ führt.
Darstellung: als Graphen, analog zu endlichen Automaten: Knoten s ∈ S, Kanten
(a, k, u) ∈ (T ∪ {ε}) × K × K ∗ . Eine Kante (a, k, u) von s nach s0 existiert genau
dann, wenn (s0 , u) ∈ δ(s, a, k) ist.
Beispiel:
Kellerautomat für {(n )n : n ∈ N \ {0}}:
KA = ({s0 , s1 , s2 } , {(, )} , {|, 0} , δ, s0 , 0, {s2 })
δ(s0 , (, 0) = {(s0 , h|0i)}
δ(s0 , (, 1) = {(s0 , h||i)}
δ(s0 , ), 1) = {(s1 , ε)}
δ(s1 , ), 1) = {(s1 , ε)}
δ(s1 , ε, 0) = {(s2 , ε)}
268
KAPITEL 1. FORMALE SPRACHEN
Arbeitsweise formal: Wir betrachten Kellerkonfigurationen (s, w, v) ∈ S × T ∗ ×
K ∗ mit Kontrollzustand s, (Rest-)Eingabewort w und Kellerwort v. Ein Kellerautomat induziert eine Übergangsrelation → auf Konfigurationen vermöge
(s1 , w, hki ◦ v) → (s2 , w, u ◦ v)
falls (s2 , u) ∈ δ(s1 , ε, k)
(s1 , hai ◦ w, hki circv) → (s2 , w, u ◦ v)
falls (s2 , u) ∈ δ(s1 , a, k)
und
Ein Wort w ∈ T ∗ wird vom Kellerautomaten akzeptiert, wenn es ein s ∈ SZ und
ein v ∈ K ∗ gibt, so dass
∗
(s0 , w, hk0 i) → (s, ε, v)
gilt. Die vom Kellerautomaten akzeptierte Sprache bezeichnen wir mit L(KA).
Anmerkung: Akzeptieren durch leeren Keller ist gleichmächtig, d.h. w ∈ T ∗
∗
wird vom Kellerautomaten genau dann akzeptiert, wenn (s0 , w, ε) → (s, ε, ε) mit
s ∈ S gilt.
Durch Kellerautomaten erhalten wir für beliebige kontextfreie Sprachen unmittelbar einen (in der Regel sehr ineffizienten) Erkennungsalgorithmus.
Im Gegensatz zu endlichen Automaten sind deterministische Kellerautomaten
nicht äquivalent zu nichtdeterministischen Kellerautomaten.
1.4.3
Äquivalenz von Kellerautomaten und kontextfreien Sprachen
Satz: Ist L eine kontextfreie Sprache, dann gibt es einen Kellerautomaten K, so
dass L(K) = L ist.
Idee: Gegeben sei eine kontextfreie Grammatik G = (T, N, →, Z). Gesucht ist ein
Kellerautomat K mit L(K) = L(G).
K = (S, T, T ∪ N ∪ {#}, δ, ε, #, {se })
Dabei setzt sich S aus ε, se , sv und der Menge aller (Teil-)Sequenzen der linken
Seiten der Regeln zusammen.
Sei he1 . . . en i, ei ∈ N ∪ T , linke Seite einer Regel he1 . . . en i → A. Wir konstruieren
die Übergangsrelation δ:
(1) Kellern:
(ε, haki) ∈ δ(ε, a, k) a ∈ T
(2) Regelerkennung:
(hei−1 . . . ej i , ε) ∈ δ(hei . . . ej i , ε, ei−1 )
(hei . . . ej+1 i , hki) ∈ δ(hei . . . ej i , ej+1 , k)
(3) Regelanwendung:
(ε, hAki) ∈ δ(he1 . . . en i , ε, k)
(4) Akzeptanz:
(sv , ε) ∈ δ(ε, ε, Z)
(se , ε) ∈ δ(sv , ε, #)
K ist nicht deterministisch und ineffizient!
K kellert die Eingabe, bis er an einer beliebigen Stelle mit dem Aufbau der linken
Seite einer Regel beginnt.
1.4. KONTEXTFREIE SPRACHEN UND KELLERAUTOMATEN
269
Beispiel: Chomsky – 2 – Grammatik G = ({a, b}, {Z}, {aZb → Z, ZZ → Z, ab →
Z})
S = {ε, sv , se , hai , hbi , hZi , haZi , hZbi , hZZi , haZbi , habi}
δ(ε, x, k) = {(hxi , hki), (ε, hxki)}
k 6= # ⇒δ(ε, ε, k) = {(hki , ε)}
δ(hZi , b, k) = {(hZbi , hki), (hZZi , hki)}
δ(ε, ε, Z) = {(sv , ε)}
Beobachtung: K ist nichtdeterministisch und hat Sackgassen (z.B. beim Ableiten
von haabbi), d.h. er ist sehr ineffizient.
Satz: Zu jedem Kellerautomaten K (mit ε ∈
/ L(K)) existiert eine kontextfreie
Grammatik G mit L(K) = L(G).
Beweis: (Literatur)
Vorgehensweisen bei der Reduktion von Wörtern kontextfreier Sprachen durch
Kellerautomaten:
(1) Top-Down: Der Keller beinhaltet zu Beginn das Axiom. Ableitung eines (Teil)Worts v ∈ T ∗ im Keller und Vergleich mit der Resteingabe. Bei Übereinstimmung Kürzung des Eingabeworts und Entfernen von v aus dem Keller.
(2) Bottom-Up: Der Keller ist am Anfang leer. Schrittweises lesen / kellern der
Eingabe, Reduktion von Teilwörtern auf dem Keller. Erfolg, wenn das Axiom
im Keller und die Eingabe leer ist (oben angewendet).
Beide Vorgehensweisen sind i.a. nichtdeterministisch und wegen der Suche im Ableitungsbaum ineffizient.
Verbesserungsansatz: Eliminieren des Nichtdeterminismus durch Beschränkung
der Regelauswahl. Der Erfolg ist dabei abhängig von der kontextfreien Sprache.
1.4.4
Greibach – Normalform
Eine kontextfreie Grammatik G = (T, N, →, Z) ist in Greibach – Normalform, wenn
jede Ersetzungsregel folgende Gestalt hat:
hai ◦ w → x
mit a ∈ T , w ∈ N ∗ und x ∈ N , d.h. Verarbeitung genau eines a ∈ T links bei jeder
Ersetzung.
Satz: Jeder von einer ε – freien, kontextfreien Grammatik erzeugte Sprachschatz
kann auch durch eine kontextfreie Grammatik in Greibach – Normalform erzeugt
werden.
Beweis: (Literatur)
Satz: Zu jeder kontextfreien Grammatik G existiert ein Kellerautomat K mit
L(G) = L(K).
Beweis: G = (T, N, →, Z) sei o.B.d.A. in Greibach – Normalform.
K = ({s0 } , T, N, δ, s0 , Z, {s0 })
∀k ∈ N : δ(s0 , a, k) = {(s0 , w) : hai ◦ w → k}
Es gilt für u ∈ T ∗ und x ∈ N ∗ :
∗
∗
u x ⇔ (s0 , u, x) → (s0 , ε, ε)
Es genügt also eine einelementige Zustandsmenge.
270
KAPITEL 1. FORMALE SPRACHEN
Beispiel:
kontextfreie Grammatik
G = ({a, b} , {Z, U } , {aU → Z, aZU → Z, b → U } , Z)
K = ({s0 } , {a, b} , {Z, U } , δ, s0 , Z, {z0 })
n
o
δ(s0 , a, Z) = (s0 , hU i)[1a] , (s0 , hZU i)[1b]
n
o
δ(s0 , b, U ) = (s0 , ε)[2]
Reduktion von haabbi:
[1a]
(s0 , haabbi , hZi) → (s0 , habbi , hU i) Sackgasse
[1b]
[1a]
[2]
(s0 , haabbi , hZi) → (s0 , habbi , hZU i) → (s0 , hbbi , hZU i) →
[2]
(s0 , hbi , hU i) → (s0 , ε, ε)
Dieser Kellerautomat ist also bereits stark vereinfacht, aber nach wie vor nichtdeterministisch.
Benötigt aus praktischer Sicht: eingeschränkte kontextfreie Sprachen, die beschränkten Nichtdeterminismus verursachen und Sackgassen eliminieren.
1.4.5
LR(k) – Sprachen
(left – rightmost: Eingabe von links nach rechts, Rechtsableitungen, k Zeichen Vorschau)
Idee: Bottom-Up: Eingabewort von links nach rechts lesen und kellern, bis eine
Regel anwendbar ist. Bestimmung der anzuwendenden Regel anhand von höchstens
k Zeichen des Restworts (bzw. rechts der Anwendungsstelle); Rechtskontext.
In einer LR(k) – Sprache kann bei jeder akzeptierenden Linksreduktion durch
Betrachtung der k nächsten Zeichen rechts der Anwendungsstelle die anzuwendende
Regel eindeutig bestimmt werden.
Bei bestimmten Grammatiken kann die Auswahl einer Regel der Form
he1 . . . en i → A
für w = x ◦ he1 . . . en i ◦ y durch Betrachtung eines endlichen Präfixes von y gesteuert
werden.
Kontextbedingung ist die Menge von Wörtern, die die Präfixe von y festlegt,
welche die Regelanwendung zulassen.
Kontextfreie Grammatiken heißen LR – deterministisch, wenn es für alle Regeln
endliche Kontextbedingungen gibt, so dass der von links nach rechts arbeitende
Kellerautomat für alle w ∈ L(G) und alle akzeptierenden Reduktionen in jedem
Schritt genau eine Regel findet, die Reduktion vollzieht und Sackgassen vermeidet.
Satz: Jede LR – deterministische, kontextfreie Grammatik ist eindeutig.
Beweis: Gemäß der Definition von LR – deterministischen, kontextfreien Grammatiken gibt es eine eindeutige Einschränkung des Kellerautomaten, der die Ableitung
erzeugt.
Definition: LR(k): w[i : k] bezeichne das Teilwort hwi . . . wk i von w = hw1 . . . wn i
(i, k, n ∈ N, 1 ≤ i ≤ k ≤ n). Eine azyklische kontextfreie Grammatik (T, N, →, Z)
heißt LR(k) – Grammatik, wenn für jedes Paar von Ableitungen in Linksnormalform
u1 ◦ a1 ◦ x1 u1 ◦ hB1 i ◦ x1 . . . Z
u2 ◦ a2 ◦ x2 u2 ◦ hB2 i ◦ x2 . . . Z
1.4. KONTEXTFREIE SPRACHEN UND KELLERAUTOMATEN
271
mit {a1 → B1 , a2 → B2 } ⊆→ gilt:
u1 ◦ a1 ◦ x1 [1 : k] = u1 ◦ a2 ◦ x2 [1 : k] ⇒ a1 = a2 ∧ B1 = B2 ∧ u1 = u2
d.h. durch x1 [1 : k] ist die anzuwendende Regel eindeutig festgelegt.
Beispiel:
(LR(0))
G = ({a, b}, {Z, X}, {ZX → Z, X → Z, aZb → X, ab → X}, Z)
Ableitung von haabbabi:
haabbabi → haXbabi → haZbabi → hXabi →
hZabi → hZXi → hZi
Kellerautomat: Wort kellern, bis Regel anwendbar; dann Regeln anwenden, solange
möglich.
Keller Restwort
ε
aabbab
abbab
a
aa
bbab
aab
bab
aX
bab
aZ
bab
aZb
ab
ab
X
Z
ab
Za
b
Zab
ε
ZX
ε
Z
ε
Der Keller beinhaltet also immer das Präfix des zu reduzierenden Wortes.
Bei LR(0) ist kein Kontext notwendig, um die richtigen Regeln zu wählen.
Satz: Jede LR(k) – Grammatik ist eindeutig.
Beweis: LR(k) – Grammatiken sind LR – deterministisch.
Beispiel:
(LR(1))
G = ({Z, A, P, E}, {(, ), +, −, ∗, /, a}, →, Z)
Regel
A→Z
E→A
A+E →A
A−E →A
P →E
E∗P →E
E/P → E
(A) → P
a→P
Kontextbedingung
ε
+, −, ), ε
+, −, ), ε
+, −, ), ε
t∈T
t∈T
t∈T
t∈T
t∈T
ha + a ∗ ai . . . hA + E ∗ ai hA + E ∗ P i . . . Z
LR(k) – Grammatiken erlauben die Reduktion von Wörtern durch Kellerautomaten mit akzeptablem Aufwand. Insbesondere LR(1) – Grammatiken werden in der
Praxis eingesetzt.
Anmerkung: Es gibt keinen Algorithmus, der für eine beliebige Grammatik G
entscheidet, ob sie LR(k) ist.
272
1.4.6
KAPITEL 1. FORMALE SPRACHEN
LL(k) – Grammatiken
(left leftmost)
Gegensatz zu LR(k): Top-Down – Produktion der Ableitungen, startend vom
Axiom Z, dadurch Linksableitung (bzw. Rechtsreduktion) statt Linksreduktion.
Idee: (Kellerautomat)
(1) Produktion von v ∈ T ∗ ausgehend von Nichtterminalen auf dem Keller und
abhängig vom Rechtskontext.
(2) Vergleich des erzeugten v mit Präfix des (Rest-)Eingabeworts.
(3) Bei Übereinstimmung: Entfernen von v aus dem Keller und dem Eingabewort.
Definition: LL(k): Eine azyklische, kontextfreie Grammatik G = (T, N, →, Z)
heißt LL(k) – Grammatik (mit k ∈ N), wenn für alle Paare von Ableitungen in
Rechtsnormalform (a1 , a2 , v, u1 , u2 ∈ (T ∪ N )∗ , w ∈ T ∗ , B ∈ N )
∗
∗
∗
∗
∗
∗
v ◦ u1 v ◦ a1 ◦ w v ◦ hBi ◦ w Z
v ◦ u2 v ◦ a2 ◦ w v ◦ hBi ◦ w Z
mit {a1 → B, a2 → B} ⊆→ gilt:
u1 [1 : k] = u2 [1 : k] ⇒ a1 = a2
d.h. die Zerteilung ist mit u1 [1 : k] eindeutig festgelegt.
Beispiel: LL(1) – Grammatik G = ({z}, {a, +, −}, {a → z, −z → z, +zz → z}, z)
Reduktion von h+-a+aai durch Kellerautomaten:
LL – Technik (top-down) LR – Technik (bottom-up)
Keller
Resteingabe
Keller
Resteingabe
Z
+-a+aa
ε
+-a+aa
ZZ+
+-a+aa
+
-a+aa
ZZ
-a+aa
+a+aa
ZZ-a+aa
+-a
+aa
ZZ
a+aa
+-Z
+aa
..
..
..
..
.
.
.
.
ε
ε
Z
ε
Jede LL(k) – Grammatik ist auch eine LR(k) – Grammatik, d.h. u.a. jede
LL(k) – Grammatik ist eindeutig.
1.4.7
Rekursiver Abstieg
Der rekursive Abstieg ist ein klassisches Verfahren der Programmierung eines TopDown – Kellerautomaten zur Zerteilung von Wörtern einer kontextfreien Sprache,
orientiert an BNF:
• Für jedes Nichtterminal xi ∈ N der linken Seiten der Regeln xi ::= E1 | . . . |En
wird eine Prozedur implementiert, die (rekursiv) eine vollständige Falluntersuchung der rechten Seite der zu xi gehörenden Regel durchführt.
In der Regel erfolgt ebenfalls Steuerung über das Präfix des Restworts.
1.4. KONTEXTFREIE SPRACHEN UND KELLERAUTOMATEN
273
N0
Ni
Nj
u
v
w
x
y
Abbildung 1.11: Zerlegung von z
1.4.8
Das Pumping – Lemma für kontextfreie Sprachen
Eine kontextfreie Grammatik G = (T, N, →, Z) ist in CNF, falls alle Regeln folgende
Gestalt haben:
a → A oder BC → A
für a ∈ T und A, B, C ∈ N .
Satz: Zu jeder kontextfreien Grammatik G mit ε ∈
/ L(G) gibt es eine äquivalente
kontextfreie Grammatik in CNF. (ohne Beweis)
Satz (Pumping – Lemma für kontextfreie Sprachen): Zu jeder kontextfreien Sprache L existiert ein n ∈ N, so dass sich alle z ∈ L mit |z| ≥ n in u, v, w, x, y ∈ T ∗
zerlegen lassen:
z =u◦v◦w◦x◦y
mit
(1) |v ◦ x| ≥ 1
(2) |v ◦ w ◦ x| ≤ n
(3) ∀i ≥ 0 : u ◦ v i ◦ w ◦ xi ◦ y ∈ L
Beweis: Sei G = (T, N, →, N0 ) eine CNF – Grammatik mit L(G) = L. Weiter
seien k = |N |, n = 2k und z ∈ L mit |z| ≥ n.
Der Ableitungsbaum T für z ist ein Binärbaum der Höhe h + 1 mit |z| ≥ n Blättern:
• Knoten mit genau einem Sohn (a → A) oder genau zwei Söhnen (BC → A)
• h ≥ ld |z| ≥ ld n = k
Im Pfad p0 , . . . , ph in T von der Wurzel p0 bis zum Blatt ph ist die Markierung
N0 , . . . , Nh der Knoten p0 , . . . , ph eine Folge von h + 1 ≥ k + 1 Nichtterminalen. Es
gibt also i, j ∈ {0, . . . , h}, so dass Ni = Nj =: N , i < j und Ni+1 , . . . , Nh paarweise
verschieden sind (vgl. Abb. 1.11).
(1) Da G in CNF ist, ist v 6= ε oder x 6= ε, d.h. |v ◦ x| ≥ 1.
∗
(2) Der Teilbaum T 0 ab Knoten pi ist Ableitungsbaum für Ni v ◦ w ◦ x. Da
Ni+1 , . . . , Nh paarweise verschieden sind, folgt: T 0 hat die Höhe h − i ≤ k, d.h.
|v ◦ w ◦ x| ≤ 2h−i ≤ 2k = n
(3) Die Ableitungen für u ◦ v i ◦ w ◦ xi ◦ y ergeben sich aus den Kombinationen der
Möglichkeiten:
∗
N0 u ◦ N ◦ y,
∗
N0 w,
∗
N v◦N ◦x
274
1.5
KAPITEL 1. FORMALE SPRACHEN
Kontextsensitive Grammatiken
Kontextsensitive Grammatiken sind definitionsgemäß wortlängenmonoton, d.h. für
∗
jeden Schritt x y gilt |x| ≥ |y|. Kontextsensitive Grammatiken sind sehr allgemein; jede wortlängenmonotone Grammatik ist strukturäquivalent zu einer kontextsensitiven Grammatik.
Es ist möglich, für eine kontextsensitive Sprache L einen (ineffizienten) Algorithmus anzugeben, der für Wörter w ∈ T ∗ entscheidet, ob w ∈ L ist.
Begründung: Bei einer wortlängenmonotonen Grammatik ist für jedes w ∈ T ∗ die
Menge der durch Reduktionen von w entstehenden Wörter endlich; mögliche unendliche Reduktionen können aufgrund der Wortlängenmonotonheit erkannt werden.
Für eine Chomsky – 0 – Grammatik G0 existiert im Allgemeinen kein solcher
Algorithmus. Für G0 existiert lediglich ein Algorithmus, der für jedes w ∈ L(G0 ) mit
Resultat true terminiert. Für w ∈
/ L(G0 ) kann die Terminierung nicht garantiert
werden.
Kapitel 2
Berechenbarkeit
Es gibt mathematisch exakt beschreibbare Probleme, für die es keine Lösungsalgorithmen gibt.
Typisches Problem:
Aufgabe: Berechnung einer gegebenen Funktion f
Lösung: Algorithmus, der f berechnet.
Definition: Berechenbarkeit
Eine Funktion f heißt berechenbar, wenn es einen Algorithmus gibt, der für
jedes Argument x (Eingabe) den Wert f (x) (Ausgabe) berechnet.
Nicht berechenbare Funktionen können nicht durch Programme in einer Rechenanlage beschrieben werden.
Wir betrachten o.B.d.A. n-stellige Funktionen f : Nn → N, wobei lediglich
Repräsentationen von N zur Verfügung stehen (siehe Teil I). Wir verwenden die
Zeichenmenge T und die injektive, umkehrbare Abbildung
rep : N → T ∗
Demnach existiert auch
abs : {t ∈ T ∗ : ∃n ∈ N : rep(n) = t} → N
so dass abs ◦ rep = id = rep ◦ abs gilt.
Statt abstrakter Funktionen f : Nn → N betrachten wir ein konkretes f : (T ∗ )n →
T ∗ mit
f (x1 , . . . , xn ) = abs f (rep(x1 ), . . . , rep(xn ))
Konkrete Algorithmen arbeiten auf Repräsentationen (von N). Für einen realistischen Berechenbarkeitsbegriff müssen diese Repräsentationen handhabbar sein. Wir
setzen deshalb u.a. voraus, dass T endlich ist.
2.1
Hypothetische Maschinen
Wir suchen eine Möglichkeit zur Präzisierung des Begriffs Algorithmus: hypothetische Maschinen. Dies sind mathematische Nachbildungen des Zustandsraums
und der Übergangsfunktion realer Maschinen mit dem Ziel der Präzisierung und
der Vereinfachung.
Endliche Automaten und Kellerautomaten genügen nicht; u.a. können Kellerautomaten für kontextfreie Sprachen, aber nicht für kontextsensitive Sprachen L
berechnen, ob w ∈ L ist.
Wir betrachten allgemeine Modelle als Kellerautomaten mit unbeschränkten
Mengen von Zuständen.
Mögliche Kritik: unrealistisch weil technisch nicht realisierbar etc.
275
276
KAPITEL 2. BERECHENBARKEIT
2.1.1
Turing – Maschinen
(nach A. M. Turing, 1936) Eine Turing – Maschine besteht aus:
• einem unendlichen Band von Zellen zur Speicherung von Zeichen,
• einem Schreib- / Lesekopf und
• einer Steuereinheit
Ein endlicher Abschnitt des Bandes trägt relevante Information, alle anderen Zellen
enthalten das Symbol # für die leere Information (o.B.d.A. sei # ∈
/ T ).
Prinzip: Die Turing – Maschine liest in jedem Schritt ein Zeichen t ∈ T ∪ {#}
unter dem Kopf. Abhängig vom Zustand wird das Zeichen überschrieben, ein neuer
Zustand eingenommen und der Kopf um höchstens eine Position nach links oder
rechts bewegt.
Eine Turing – Maschine T M = (T, S, δ, s0 ) umfasst:
• eine endliche Menge T von Zeichen, mit denen das Band beschrieben wird
(# ∈
/ T)
• eine endliche Menge S von Zuständen
• eine endliche Übergangsrelation
δ : S × (T ∪ {#}) → 2S×(T ∪{#})×{,,↓}
und bewirken dabei eine Verschiebung des Kopfes um eine Zelle nach
links bzw. rechts, ↓ das Beibehalten der Position.
Gilt für eine Turing – Maschine ∀t ∈ T ∪ {#}, s ∈ S : |δ(s, t)| ≤ 1, so heißt sie
deterministisch.
Beispiel: Prüfung, ob die Anzahl der L in einem Wort hw1 . . . wn i mit n ≥ 1 und
wi ∈ {0, L} gerade ist.
Zu Beginn sei . . . #w1 . . . wn # . . . auf dem Band und der Kopf auf wn positioniert.
Die Turing – Maschine hält mit Bandinhalt . . . #L# . . . an, falls die Anzahl der L
gerade war und mit . . . #0# . . . sonst.
T M = ({0, L}, {s0 , s1 , s2 , s3 }, δ, s0 )
Übergangsfunktion:
δ
0
L
#
s0 (s0 , 0, ) (s0 , L, ) (s1 , #, )
s1 (s1 , #, ) (s2 , #, )
(s3 , L, ↓)
s2 (s2 , #, ) (s1 , #, )
(s3 , 0, ↓)
s3
∅
∅
∅
Das Verhalten einer Turing – Maschine kann auch graphisch durch einen Automaten
dargestellt werden — s. Abb. (2.1). Die Markierung 0 # für einen Zustandsübergang von s nach s0 bedeutet, dass dieser Übergang im Zustand s ausgeführt werden
kann, falls 0 unter dem Lesekopf steht; dann wird # geschrieben und der Lesekopf
bewegt sich nach links.
Eine Konfiguration der Turing – Maschine beschreibt den Berechnungszustand.
Wir verwenden ein 4 – Tupel
(s, l, a, r) ∈ S × (T ∪ {#})∗ × (T ∪ {#}) × (T ∪ {#})∗
Da das Band nach links und nach rechts unendlich fortgesetzt ist, sind folgende
Konfigurationen äquivalent:
• (s, l, a, r)
2.1. HYPOTHETISCHE MASCHINEN
277
# # <<
s0
0 0 >>
L L >>
0 # <<
s1
#Lv
L # <<
L # <<
s3
s2
#0v
0 # <<
Abbildung 2.1: Graphische Darstellung einer Turing – Maschine
• (s, h#i ◦ l, a, r)
• (s, l, a, r ◦ h#i)
Die Berechnung einer Turing – Maschine (ausgehend von einer Konfiguration, d.h.
einem Anfangszustand, einer (endlichen) Anfangsbandbelegung und Stellung des
Lesekopfs) besteht aus einer endlichen oder unendlichen Folge von Konfigurationen
K0 → K1 → K2 → · · · → . . .
Wir definieren eine Relation auf Konfigurationen
(s, l, a, r) → (s0 , l0 , a0 , r0 )
wie folgt: Es gilt genau eine der folgenden Aussagen:
(1) z =↓ ∧l = l0 ∧ r = r0 ∧ a0 = x (Kopf bleibt stehen)
(2) z = ∧l = l0 ◦ ha0 i ∧ r0 = hxi ◦ r (Kopf nach links)
(3) z = ∧l0 = l ◦ hxi ∧ r = ha0 i ◦ r0 (Kopf nach rechts)
wobei (s0 , x, z) ∈ δ(s, a).
Eine Konfiguration k heißt terminal, wenn keine Nachfolgekonfiguration existiert.
Die Turing – Maschine bleibt stehen, die Berechnung endet.
Eine Berechnung heißt vollständig, wenn sie unendlich ist oder endlich ist und
die letzte Konfiguration terminal ist.
Bemerkung: Jede Turing – Maschine lässt sich in einer der gebräuchlichen Programmiersprachen (C, Java, . . . ) simulieren.
Wir stützen nun auf Turing – Maschinen den Begriff der Berechenbarkeit ab
(genauer: Turing – Berechenbarkeit).
Eine partielle Funktion f : T ∗ → T ∗ heißt Turing – berechenbar, wenn es eine
deterministische Turing – Maschine gibt, so dass für jedes Wort t ∈ T ∗ folgende
Aussagen gelten:
(1) f (t) hat einen Bildpunkt (f (t) ist definiert; genauer: der Wert von f für das
Argument t ist definiert) und es existiert eine vollständige Berechnung
(s0 , t, #, ε) → · · · → (se , r, #, ε)
wobei f (t) = r ist.
(2) f ist für das Argument t nicht definiert und es existiert eine unendliche Berechnung
(s0 , t, #, ε) → · · · → . . .
278
KAPITEL 2. BERECHENBARKEIT
Dieser Berechenbarkeitsbegriff lässt sich auf partielle Abbildungen
f : Nn → N
übertragen. Dazu müssen wir nun eine Zahldarstellung festlegen. Wir stellen natürliche Zahlen durch Strichzahlen dar und verwenden t als Trennzeichen. f heißt berechenbar, wenn eine Funktion
g : {|, t}∗ → {|, t}∗
existiert, die berechenbar ist und ∀x1 , . . . , xn ∈ N gilt:
f (hti ◦ |x1 ◦ hti ◦ · · · ◦ hti ◦ |xn ◦ hti) = hti ◦ |y ◦ hti
genau dann, wenn
f (x1 , . . . , xn ) = y
ist.
Beispiel:
einige Turing – berechenbare Funktionen
(1) Konstante:
c : Nn → N, c(x1 , . . . , xn ) = k für gegebenes k ∈ N
(2) Nachfolgefunktion:
succ : N → N mit succ(n) = n + 1
(3) Projektionen:
πin : Nn → N mit πin (x1 , . . . , xn ) = xi
(4) Vorgänger:
(
x−1 x>0
pre : N → N total: pre(x) =
0
x=0
(
x−1 x>0
pred : N → N partiell: pred(x) =
undef x = 0
Komplexere Funktionen können wir durch Komposition (Funktionskomposition) gewinnen. Wir definieren eine verallgemeinerte Komposition für partielle Funktionen:
Gegeben seien
g : Nn → N
hi : N m → N
1≤i≤n
Wir definieren
f : Nm → N
durch die Gleichung
f (x1 , . . . , xm ) = g (h1 (x1 , . . . , xm ) , . . . , hn (x1 , . . . , xm ))
falls h1 , . . . , hn für x1 , . . . , xm und f für h1 (x1 , . . . , xm ), . . . , hn (x1 , . . . , xm ) definiert
ist; sonst ist f für x1 , . . . , xm undefiniert.
Satz: f ist Turing – berechenbar, wenn g, h1 , . . . , hn Turing – berechenbar sind.
2.1. HYPOTHETISCHE MASCHINEN
279
Notation: Wir schreiben dann für f auch
g ◦ [h1 , . . . , hn ]
Es gilt: Addition, Multiplikation, Division (d.h. die übliche Arithmetik) ist Turing –
berechenbar.
Frage: Gibt es Funktionen, die nicht Turing – berechenbar sind?
Ja — siehe später.
Bemerkungen:
(1) Ob wir Turing – Maschinen mit mehreren Bändern, nur einseitig unendliche
Turing – Maschinen verwenden, ändert nichts am Begriff der Turing – Berechenbarkeit.
(2) Andere Berechenbarkeitsbegriffe haben sich als äquivalent erwiesen. Dazu folgen zwei Beispiele.
2.1.2
Registermaschinen
Eine Registermaschine ist (wie eine Turing – Maschine) eine hypothetische Maschine (d.h. mathematisch definiert), die unseren gebräuchlichen Rechnern entspricht.
Eine Registermaschine mit n Registern (n – Registermaschine) besitzt n Register
(Speicherplätze für natürliche Zahlen) und ein Programm (ein n – Registermaschinen – Programm). Diese Programme haben eine extrem einfache Syntax:
(1) ε ist ein n – Registermaschinen – Programm (leeres Programm).
(2) succi mit 1 ≤ i ≤ n ist ein n – Registermaschinen – Programm.
(3) predi mit 1 ≤ i ≤ n ist ein n – Registermaschinen – Programm.
(4) Sind M1 und M2 n – Registermaschinen – Programme, so ist M1 ; M2 ein n –
Registermaschinen – Programm.
(5) Ist M ein n – Registermaschinen – Programm, so ist whilei (M ) mit 1 ≤ i ≤ n
ein n – Registermaschinen – Programm.
Semantik von Registermaschinen durch Zustandsübergangsbeschreibung
Konfigurationen einer n – Registermaschine:
(s, p) ∈ Nn × n-PROG
Dabei bezeichnet n-PROG die Menge aller Programme für n – Registermaschinen.
Zustands- bzw. Konfigurationsübergangsfunktion (für n – Registermaschinen mit
i ∈ N, 1 ≤ i ≤ n):
(s, succi ) → ((s1 , . . . , si−1 , si + 1, si+1 , . . . , sn ), ε)
(s, predi ) → ((s1 , . . . , si−1 , k, si+1 , . . . , sn ), ε)
mit
(
si − 1 falls si > 0
k=
0
sonst
(s, (p1 ; p2 )) → (s0 , (p01 ; p2 )) falls (s, p1 ) → (s0 , p01 )
(s, (ε, p2 )) → (s, p2 )
(
(s, (p; whilei (p))) falls si > 0
(s, whilei (p)) →
(s, ε)
sonst
280
KAPITEL 2. BERECHENBARKEIT
Bemerkung: Die Konfigurationen der Form (s, ε) sind terminal, d.h. es existiert
keine Nachfolgekonfiguration (das Programm terminiert).
Wie gehabt definieren wir Berechnungen (endliche / unendliche) als Folgen von
Konfigurationen in der → – Relation.
Beispiel:
Registermaschinen – Programme
(1) whilei (predi )
entspricht
while si > 0 do si := si - 1 od
(si wird auf 0 gesetzt).
si := k entspricht
whilei (predi ); succi ; . . . ; succi
(mit k Aufrufen von succi ).
(2) Folgende
Funktionen können in Registermaschinen programmiert werden:
• übliche Arithmetik
• übliche boolesche Algebra
Eine Funktion
f : Nn → Nn
heißt Registermaschinen – berechenbar, wenn es ein n – Registermaschinenprogramm p gibt, so dass gilt: falls
f (s1 , . . . , sn ) = (s01 , . . . , s0n )
dann gilt
∗
(s, p) → (s0 , ε)
(mit s = (s1 , . . . , sn ) und s0 = (s01 , . . . , s0n )). Auch
g : Nk → N
mit 1 ≤ k ≤ n und (für ein i ∈ N, 1 ≤ i ≤ n)
g(x1 , . . . , xk ) = πin (f (x1 , . . . , xk , xk+1 , . . . , xn ))
für gegebene xk+1 , . . . , xn heißt Registermaschinen – berechenbar.
2.2
Rekursive Funktionen
Die beiden bisher betrachteten Berechnungsmodelle (Turing – Maschinen, Registermaschinen) sind hypothetische Maschinen. Jetzt betrachten wir ein stärker durch
Beschreibung charakterisiertes Berechnungsmodell: rekursiv definierte Funktionen.
Auch dafür können wir Algorithmen zur Auswertung angeben (vgl. Termersetzung).
2.2.1
Primitiv rekursive Funktionen
Wir betrachten n – stellige (totale oder partielle) Funktionen f : Nn → N. Wir
nennen folgende Funktionen Grundfunktionen:
succ : N → N
zero
(0)
mit succ(n) = n + 1
:→ N
(1)
zero : N → N
πin : Nn → N
i-te Projektion, 1 ≤ i ≤ n
2.2. REKURSIVE FUNKTIONEN
281
Aus einer gegebenen Menge von Funktionen gewinnen wir weitere durch Komposition
g ◦ [k1 , . . . , kn ]
oder durch Anwendung des Schemas der primitiven Rekursion:
Gegeben seien Funktionen
g : Nk → N
h : Nk+2 → N
Wir definieren
f : Nk+1 → N
wie folgt: ∀x1 , . . . , xk , n ∈ N:
f (x1 , . . . , xk , 0) = g(x1 , . . . , xk )
f (x1 , . . . , xk , n + 1) = h(x1 , . . . , xk , n, f (x1 , . . . , xk , n))
Beispiel:
add(x, 0) = x
add(x, n + 1) = add(x, n) + 1 = h(x, n, add(x, n)) = succ(π33 (x, n, add(x, n)))
Dies entspricht einer induktiven Definition von f .
Fakt: Sind g, h totale Funktionen, so ist durch das Schema f eindeutig bestimmt
und total.
Beweis: Induktion
Bemerkung: Sind g, h partielle Funktionen, so ist f auch eindeutig festgelegt,
aber unter Umständen partiell.
Die Menge der primitiv rekursiven Funktionen (P R) ist induktiv wie folgt definiert:
(1) Die Grundfunktionen sind in P R.
(2) Funktionen, die durch Komposition von Funktionen aus P R gewonnen werden
können, sind in P R.
(3) Funktionen, die durch das Schema der primitiven Rekursion über Funktionen
g, h ∈ P R gewonnen werden können, sind in P R.
Wir können — wie für die Komposition — auch für das Schema der primitiven
Rekursion eine kompakte Notation einführen:
pr : (Nk → N) × (Nk+2 → N) → (Nk+1 → N)
mit
f = pr(g, h)
Es gilt:
(1) Alle arithmetischen Funktionen (totale Division, totale Subtraktion) sind in
P R.
(2) Alle Funktionen in P R sind total.
(3) Alle Funktionen in P R sind Turing – berechenbar.
(4) Alle Funktionen in P R sind Registermaschinen – berechenbar.
282
KAPITEL 2. BERECHENBARKEIT
(5) Da alle Funktionen in P R total sind, existieren trivialerweise Funktionen, die
Turing – berechenbar bzw. Registermaschinen – berechenbar, aber nicht in
P R sind.
Wir zeigen nun, dass es eine totale Funktion gibt, die offensichtlich Turing – und
Registermaschinen – berechenbar ist, aber nicht in P R:
ack : N2 → N


n=0
m + 1
ack(n, m) = ack(n − 1, 1)
n > 0, m = 0


ack(n − 1, ack(n, m − 1)) sonst
Feststellungen:
(1) Durch diese Gleichung ist ack eindeutig bestimmt und total.
(2) Wir definieren eine Schar von Funktionen Bn : N → N durch Bn (m) :=
ack(n, m). Wir erhalten:
B0 (m) = m + 1
B1 (m) = m + 2
B2 (m) = 2m + 3
B3 (m) = 2m+2 − 3
..
.
Alle Funktionen Bn sind primitiv rekursiv. Bn+1 kann durch das Schema der
primitiven Rekursion aus Bn definiert werden.
Satz: ack ∈
/ PR
Beweis (Skizze): Durch Induktion über die Anzahl der Anwendungen des Schemas
der primitiven Rekursion können wir zeigen: Zu jeder Funktion g ∈ P R, g : Nn → N
existiert eine Konstante c ∈ N, so dass gilt
!
n
X
g(x1 , . . . , xn ) < ack c,
xi
i=1
für alle x1 , . . . , xn ∈ N.
Anmerkung: Angenommen, ack ∈ P R. Dann wäre h : N → N mit h(n) =
ack(n, n) auch in P R. Es würde eine Zahl c ∈ N geben mit
∀n ∈ N : ack(n, n) = h(n) < ack(c, n)
Mit n = c erhalten wir den Widerspruch
ack(c, c) = h(c) < ack(c, c)
In anderen Worten: ack wächst schneller als alle Funktionen in P R.
Fazit: Es existieren totale Funktionen, die Turing – berechenbar, Registermaschinen – berechenbar, aber nicht in P R sind.
ack ist offensichtlich durch Rekursion definierbar, also ist der Begriff der primitiven
Rekursion zu eng.
2.2.2
µ – rekursive Funktionen
Jetzt betrachten wir auch partielle Funktionen, verwenden alle Funktionen aus P R
(und alle Konstruktionsprinzipien) und zusätzlich eine weitere Konstruktion, die
der µ – Rekursion.
2.2. REKURSIVE FUNKTIONEN
Beispiel:
283
rekursive Definition
ulam : N → N
(
1
n≤1
ulam(n) =
ulam(g(n)) n > 1
(
falls n gerade
b n2 c
g(n) =
3n + 1 sonst
Feststellung: g ist primitiv rekursiv.
Frage: Terminiert ulam für alle Argumente n ∈ N?
Antwort: unbekannt, aber Vermutung: Antwort ist ja.
Falls ulam immer terminiert, gilt ulam(n) = 1; ulam ist dann primitiv rekursiv.
Das Schema der µ – Rekursion:
Gegeben sei eine partielle Funktion
f : Nk+1 → N
Wir definieren
µ(f ) : Nk → N
durch
µ(f )(x1 , . . . , xk ) = min {y ∈ N : f (x1 , . . . , xk , y) = 0}
falls
∃y ∈ N : f (x1 , . . . , xk , y) = 0∧∀z ∈ N : 0 ≤ z ≤ y ⇒ f ist definiert für (x1 , . . . , xk , z)
Gilt diese Bedingung nicht, so definieren wir µ(f ) für (x1 , . . . , xk ) als nicht definiert.
Verfahren zur Berechnung von y = µ(f )(x1 , . . . , xk ):
(1) Berechne y0 = f (x1 , . . . , xk , 0).
(a) y0 = 0 ⇒ y = 0
(b) y0 > 0: gehe zu (2)
(c) Berechnung terminiert nicht: µ(f ) ist nicht definiert für (x1 , . . . , xk ).
(2) analog für f (x1 , . . . , xk , 1) ...
Achtung: Auch wenn f total ist, kann µ(f ) partiell sein.
Die Menge der µ – rekursiven Funktionen M R definieren wir induktiv wie folgt:
1. Alle primitiv rekursiven Funktionen sind in M R.
2. Funktionen, die durch Komposition oder das Schema der primitiven Rekursion
aus Funktionen in M R gebildet werden können, sind in M R.
3. Falls f ∈ M R, dann ist µ(f ) ∈ M R.
Beispiel:
µ – rekursive Definition der partiellen Subtraktion:
(
.
a−b a≥b
a−b=
undef sonst
Wir definieren
.
a − b = µ(h0 )(a, b)
284
KAPITEL 2. BERECHENBARKEIT
mit geeignetem h0 : N3 → N. Wir wählen
h0 (a, b, y) = sub(b + y, a) + sub(a, b + y)
wobei
(
a−b a≥b
sub(a, b) =
0
sonst
sei.
Fälle
a≥b
sub(b + y, a)
=0
a≥b
sub(a, b + y)
=0
>0
b>a
2.2.3
+
Fälle
y =a−b≥0
y <a−b
y+b<a
>0
Allgemeine Bemerkungen zur Rekursion
In der Informatik existieren viele Spielarten von Rekursion zur Definition von Funktionen, Prozeduren, Methoden, Datentypen, Mengen, formalen Sprachen usw.
In der rekursiven Definition einer Funktion verwenden wir Gleichungen der Form
f (x) = E, wobei f in E auftritt, oder f (D) = E mit eingeschränktem Ausdruck D.
Beispiel
(strukturelle Rekursion, induktive Definition)
ack(0, 0) = 1
ack(0, m + 1) = ack(0, m) + 1
ack(n + 1, 0) = ack(n, 1)
ack(n + 1, m + 1) = ack(n, ack(n + 1, m))
2.3
Äquivalenz der Berechenbarkeitsbegriffe
Wir haben vier Begriffe der Berechenbarkeit eingeführt:
• Turing – Maschinen
• Registermaschinen
• primitive Rekursion
• µ – Rekursion
Primitive Rekursion ist schwächer als Turing – Maschinen, Registermaschinen, µ –
Rekursion. Wir zeigen nun die Äquivalenz von Turing – Maschinen, Registermaschinen und µ – Rekursion.
2.3.1
Äquivalenz von µ – Berechenbarkeit und Turing – Berechenbarkeit
Der Logiker Kurt Gödel hat eine Idee zur Darstellung von Zeichenfolgen durch
Zahlen entwickelt → Gödelisierung (1933).
Eine Gödelisierung ist eine Abbildung
f : A∗ → N
wobei A eine endliche Menge von Zeichen sei, mit folgenden Eigenschaften:
2.3. ÄQUIVALENZ DER BERECHENBARKEITSBEGRIFFE
285
i. f ist injektiv.
ii. f ist berechenbar (Turing – berechenbar).
iii. Es ist berechenbar, ob eine Zahl n ∈ N im Bildbereich von f liegt (d.h.
∃w ∈ A∗ : f (w) = n).
iv. Es existiert ein Algorithmus, der zu jeder Zahl im Bildbereich das zugehörige
Wort berechnet (f ist algorithmisch umkehrbar).
∼
Satz: T M = M R
Beweis:
1. f ∈ M R ⇒ f ∈ T M
Beweisidee: Für jede Grundfunktion in M R (bzw. P R) können wir eine Turing – Maschine angeben. Das Schema der Komposition, der primitiven Rekursion und der µ – Rekursion können wir durch Turing – Maschinen nachbauen.
2. f ∈ T M ⇒ f ∈ M R
Konfigurationen von Turing – Maschinen entsprechen Wörtern aus A∗ (mit
geeignetem A). Wir verwenden eine Gödelisierung
rep : A∗ → N
(einfach zu konstruieren). Es zeigt sich, dass die Funktion
g:N→N
mit
k0 → k1 ⇔ g(rep(k0 )) = rep(k1 )
primitiv rekursiv ist (d.h. die Nachfolgefunktion auf Konfigurationen entspricht einer primitiv rekursiven Funktion auf der Darstellung der Konfigurationen durch natürliche Zahlen).
Ferner definieren wir eine primitiv rekursive Funktion
h : N2 → N
durch
(
0
h(n, m) =
1
falls g m (n) eine terminalen Konfiguration entspricht
sonst
Die Funktion
it : N2 → N
sei spezifiziert durch
it(n, 0) = n
it(n, m + 1) = it(g(n), m)
it beschreibt die Ausführung von m Schritten der Turing – Maschine. Die
Funktion
tm : N → N
sei definiert durch
tm(n) = it(n, µ(h)(n))
wobei µ(h)(n) die Anzahl der Schritte der Turing – Maschine bis zur Terminierung für die Eingangskonfiguration, dargestellt durch n, angibt, falls die
Turing – Maschine terminiert.
286
2.3.2
KAPITEL 2. BERECHENBARKEIT
Äquivalenz von Registermaschinen- und Turing – Berechenbarkeit
Jede Registermaschine lässt sich in eine Turing – Maschine übersetzen. Dazu brauchen wir eine Darstellung der Konfigurationen der Registermaschine durch eine
Turing – Maschine:
1. Die Registerinhalte werden auf dem Band der Turing – Maschine dargestellt
(z.B. als Strichzahlen).
2. Aus Registermaschinen – Programmen konstruieren wir den endlichen Automaten, der die Turing – Maschine steuert.
Zu einer gegebenen Turing – Maschine kann eine Registermaschine konstruiert werden, die die Turing – Maschine simuliert.
Idee: Konfigurationen der Turing – Maschine werden gödelisiert und in den Registern dargestellt. Die Fahrbewegungen und Zustandsübergänge der Turing – Maschine werden durch Programmschritte der Registermaschine simuliert.
2.3.3
Churchs These
(Alazo Church, 1936):
Jede intuitiv berechenbare Funktion ist µ – rekursiv.
2.4
Entscheidbarkeit
Wir haben eine Reihe von Berechenbarkeitsbegriffen kennengelernt, die jeweils auf
das gleiche Konzept von berechenbarer Funktion führen (Churchsche These).
Wir wenden uns nun der Frage zu, welche Funktionen berechenbar / nicht berechenbar und welche Prädikate entscheidbar (d.h. durch Algorithmen eindeutig mit
ja oder nein beantwortbar) sind.
2.4.1
Nicht berechenbare Funktionen
Jeder Formalismus zur Berechenbarkeit (Turing – Maschinen, Registermaschinen,
µ – Rekursion) definiert Programme / Algorithmen durch Wörter über einem Zeichensatz, d.h. in jedem Fall existiert eine formale Sprache P RG ⊆ A∗ mit geeignetem Zeichensatz A, so dass gilt: Jeder Algorithmus wird dargestellt durch ein Wort
p ∈ P RG.
Für jede berechenbare Funktion f existiert ein p ∈ P RG, das f berechnet. Die
Menge der Funktionen
Nn → N
ist überabzählbar (Ergebnis der Mengenlehre). N ist abzählbar. A∗ ist abzählbar,
falls A endlich ist. P RG ist ebenfalls abzählbar. Dies ergibt sofort: Es gibt viel
mehr Funktionen Nn → N als Programme in P RG, d.h. fast jede Funktion ist nicht
berechenbar.
Wir können Programme durch Zahlen codieren, d.h. es gibt eine Gödelisierung
code : P RG → N
Satz: Es existiert eine totale Funktion g : N → N, die nicht berechenbar ist.
Beweis: Wir spezifizieren g durch (∀n ∈ N):
(
fp (n) + 1 falls ∃p ∈ P RG : code(p) = n und fp definiert für Argument n
g(n) =
0
sonst
2.4. ENTSCHEIDBARKEIT
287
Dabei sei für p ∈ P RG die Funktion fp : N → N die durch p berechnete partielle
Funktion.
Achtung: Die Spezifikation von g ist konsistent und eindeutig, da code eine injektive
Funktion ist.
Annahme: g ist berechenbar. Dann existiert ein Programm q ∈ P RG mit fq = g.
Mit code(q) = m gilt
g(m) = fq (m) + 1 = g(m) + 1
Widerspruch.
Bemerkung: g ist zunächst nur von theoretischem Interesse.
2.4.2
(Nicht) entscheidbare Prädikate
Eine Ja – Nein – Frage entspricht logisch einem Prädikat. Ein Prädikat
p:M →B
kann auch als Funktion
p0 : M → {0, 1} ⊂ N
aufgefasst werden. Damit können wir das Konzept der Berechenbarkeit auf Prädikate übertragen. Ein berechenbares totales Prädikat heißt auch entscheidbar. Ein
Prädikat p : M → B heißt:
entscheidbar: wenn es einen Algorithmus gibt, der für jedes Argument m ∈ M
terminiert und korrekt 1 für p(m) = true und 0 für p(m) = false ausgibt.
positiv semientscheidbar: wenn es einen Algorithmus gibt, der für jedes Argument
m ∈ M terminiert und korrekt 1 ausgibt, falls p(m) = true gilt und falls p(m)
= false gilt entweder 0 ausgibt oder nicht terminiert.
negativ semientscheidbar: falls ¬p positiv semientscheidbar ist.
Beispiele
1. entscheidbare Funktionen
• Gleichheit zweier Zahlen in Binärdarstellung
• Primzahleigenschaft
• Zugehörigkeit eines Wortes zum Sprachschatz einer Chomsky – 3 – Grammatik
• Existenz der Nullstellen eines Polynoms über den ganzen Zahlen
2. positiv semientscheidbare (aber nicht allgemein entscheidbare) Funktionen
• Terminierung einer Turing – Maschine für eine bestimmte Eingabe (Halteproblem)
• Zugehörigkeit eines Wortes zum Sprachschatz einer Chomsky – 0 – Sprache
3. negativ semientscheidbar
• Gleichheit zweier durch primitive Rekursion gegebener Funktionen
4. unentscheidbare Prädikate (weder positiv noch negativ semientscheidbar)
• Terminierung einer durch µ – Rekursion beschriebenen Funktion für alle
Eingaben
288
KAPITEL 2. BERECHENBARKEIT
Satz: Das Terminierungsproblem für µ – rekursive Funktionen ist nicht entscheidbar.
Beweis: Sei µ−P RG die Menge der Wörter, die µ – rekursive Programme darstellen.
Widerspruchsannahme: Das Terminierungsproblem ist entscheidbar. Dann existiert
ein µ – rekursives Programm p ∈ µ − P RG, das eine Funktion fp berechnet, so dass
gilt
(
0
falls ∃q ∈ µ − P RG : code(q) = x ∧ fq (x) für x nicht definiert
fp (x) =
undef sonst
Wir erhalten für m = code(p):
(
0
fp (m) =
undef
falls fp (code(p)) = undef
sonst
Widerspruch.
2.4.3
Rekursion und rekursiv aufzählbare Mengen
Wir betrachten jetzt eine Obermenge U sowie Teilmengen M ⊆ U . Ein Prädikat
auf U
p:U →B
(charakteristisches Prädikat zu M ⊆ U )
ist dann gegeben durch
(
true
p(x) =
false
falls x ∈ M
falls x ∈
/M
Eine Menge heißt rekursiv, wenn das charakteristische Prädikat entscheidbar ist.
Satz: Jede Chomksy – 1 – Sprache ist rekursiv.
Eine totale Abbildung
e:N→U
heißt Aufzählung der Menge M , falls
M = {e(n) : n ∈ N} =
[
{e(n)}
n∈N
M heißt rekursiv aufzählbar, falls e berechenbar ist. Jede rekursiv aufzählbare
Menge hat ein passendes charakteristisches Prädikat.
Bemerkung: In einer Aufzählung einer Menge M
e(0), e(1), e(2), . . .
können Elemente beliebig oft vorkommen, aber jedes Element muss mindestens
einmal vorkommen.
Beispiele:
1. Die Menge der primitiv rekursiven Funktionen ist rekursiv aufzählbar, aber
nicht rekursiv.
2. Die Menge der Argumente, für die eine µ – rekursive Funktion (Turing –
Maschine, Registermaschine) terminiert, ist rekursiv aufzählbar, aber nicht
rekursiv.
2.4. ENTSCHEIDBARKEIT
289
Satz: Jede Chomsky – Sprache ist rekursiv aufzählbar.
Beweis: Die Grammatik der Sprache definiert einen Baum von Ableitungen, aus
dem wir ein Aufzählungsverfahren gewinnen.
Satz: Die Menge der Argumente einer berechenbaren Funktion f , für die f nicht
definiert ist (der Algorithmus nicht terminiert) ist i.a. nicht rekursiv aufzählbar.
Beweis: Folgt direkt aus dem Halteproblem.
Satz: Eine Menge S ⊆ T ∗ ist genau dann rekursiv, wenn S und T ∗ \ S rekursiv
aufzählbar sind.
Beweis: Wir zählen S und T ∗ \ S parallel auf. Irgendwann tritt jedes Element in
einer der Aufzählungen auf. Dann liegt fest, ob es in S oder T ∗ \ S liegt. Dieses
Verfahren terminiert für jedes Element aus T ∗ .
290
KAPITEL 2. BERECHENBARKEIT
Kapitel 3
Komplexitätstheorie
Jeder Algorithmus verbraucht bei der Ausführung gewisse Betriebsmittel, erfordert
einen Berechnungsaufwand.
Fragen:
i. Bei einem gegebenen Algorithmusbegriff (Turing – Maschine, Registermaschine, µ – Rekursion) können wir nun für ein gegebenes Problem (z.B. Multiplikation) fragen, welcher Algorithmus den geringsten Berechnungsaufwand
hat.
ii. Wie hängt die Klassifizierung des Berechnungsaufwands von der Wahl des
Algorithmenbegriffs ab?
Wir werden die Aufwandsschätzung für Algorithmen und für Probleme an Turing –
Maschinen orientieren.
3.1
Komplexitätsmaße
Wir wählen zwei einfache Maßzahlen für den Berechnungsaufwand eines Algorithmus:
• Anzahl der Schritte in der Berechnung (→ Zeitkomplexität)
• Anzahl der benötigten (Hilfs –)Speicherzellen (→ Bandkomplexität)
3.1.1
Zeitkomplexität
Wir betrachten k – Band – Turing – Maschinen. Für jedes Band existiert ein Lese- /
Schreibkopf. Für einen gegebenen Eingabewert (etwa auf Band 1) führt die Turing –
Maschine eine endliche oder unendliche Anzahl von Schritten aus. Terminiert die
Turing – Maschine für ein Wort w, so bezeichne b(w) die Anzahl der Schritte. Gilt
für eine Funktion T : N → N für alle Worter w ∈ Z ∗ der Länge n = |w| stets b(w) ≤
T (n), so heißt die Turing – Maschine (der Algorithmus) T (n) – zeitbeschränkt.
Achtung: Diese Definition schließt nichtdeterministische Turing – Maschinen mit
ein. Dann müssen alle Berechnungen für w durch T (n) beschränkt sein.
Mehrband – Turing – Maschinen vermeiden den Aufwand von Kopfbewegungen,
um zwischen Argumenten hin- und herzufahren und liefern realistischere Abschätzungen.
Die Definition von Zeitbeschränktheit lässt sich von Algorithmen auf Probleme
übertragen: Ein Problem1 heißt T (n) – zeitbeschränkt, wenn es einen Algorithmus
gibt, der das Problem berechnet und T (n) – zeitbeschränkt ist.
1 Ein
Problem entspricht immer der Aufgabe, eine Funktion / Prozedur zu berechnen
291
292
KAPITEL 3. KOMPLEXITÄTSTHEORIE
Beispiel:
Das Problem, zu entscheiden, ob ein Wort in der Sprache
L = {ak cbk : k ∈ N}
ist, ist (n + 1) zeitbeschränkt. Dafür ist nur zu zeigen, dass ein Algorithmus (eine
Turing – Maschine) existiert, der in n + 1 Schritten für ein Wort w der Länge n
entscheidet, ob w ∈ L ist.
Wir sagen, die Turing – Maschine ist linear beschränkt.
Addition: Seien die Zahlen a, b ∈ N dargestellt durch Binärzahlen; eine Turing –
Maschine für die Addition benötigt
log2 (b + a) + 2 + 2 log2 (b) + 1 + 1
Schritte. Sprechweise: Das Problem ist log – zeitbeschränkt.
Anmerkung: Wir betrachten nur Probleme mit T (n) ≥ n+1, d.h. wir betrachten
nur Problemstellungen, wo die gesamte Eingabe für den Algorithmus relevant ist.
3.1.2
Bandkomplexität
Bandkomplexität bewertet den Speicheraufwand eines Algorithmus. Wir messen die
Bandkomplexität wieder durch Turing – Maschinen über die Anzahl der im Laufe
einer Berechnung beschriebenen Speicherzellen. Wir betrachten Turing – Maschinen
mit folgender Charakteristik:
• ein Eingabeband, das nicht beschrieben wird und eine Endmarkierung enthält
• k einseitig unendliche Bänder
Für ein Eingabewort w ∈ Z ∗ verwendet die Turing – Maschine bi (w) ∈ N Speicherzellen auf ihrem i – ten Band (1 ≤ i ≤ k).
Gilt für eine Abbildung
S:N→N
für alle Wörter w mit n = |w|, alle i ,1 ≤ i ≤ k
bi (w) ≤ S(n)
so heißt die Turing – Maschine S(n) – bandbeschränkt. Wir sagen: Die Turing –
Maschine hat Bandkomplexität S(n).
Beispiel: Sprache L = {an cbn : n ∈ N}
Wenn wir die Zahlen in Binärschreibweise notieren, dann benötigen wir 1 + log2 (n)
Speicherzellen, um die an Zeichen zu zählen. Wir erhalten einen logarithmisch bandbeschränkten Algorithmus.
Definition: Ein Problem heißt S(n) – bandbeschränkt, wenn es einen Algorithmus (eine Turing – Maschine) gibt, der das Problem löst und S(n) – bandbeschränkt
ist.
Wenn wir von Bandkomplexität S(n) sprechen, meinen wir stets max{1, S(n)}.
Wir sind bei Abschätzungen von Band- / Zeitkomplexität in der Regel nicht an
exakten Zahlen interessiert, sondern an Größenordnungen. Dazu verwenden wir Abbildungen
f, g : N → N
um das assymptotische Verhalten abzuschätzen. Wir sagen: Die Funktion g wächst
mit der Ordnung Θ(f (n)), wenn gilt:
∃c, d ∈ N \ {0}, n0 ∈ N : ∀n ∈ N : n ≥ n0 ⇒ f (n) ≤ dg(n) ≤ cf (n)
3.1. KOMPLEXITÄTSMASSE
Beispiel: Zeit zur Lösung eines Problems der Größenordnung
führung des Lösungsalgorithmus O(n) Mikrosekunden benötigt.
n
log2 n
n
n log2 n
n2
10
0, 000003 sec 0, 00001 sec 0, 00003 sec 0, 0001 sec
102 0, 000007 sec 0, 0001 sec
0, 0007 sec
0, 01 sec
103 0, 000010 sec 0, 001 sec
0, 01 sec
1 sec
104 0, 000013 sec 0, 01 sec
0, 13 sec
1, 7 min
105 0, 000017 sec 0, 1 sec
1, 7 sec
2, 8 Std
Diese Tabelle zeigt den Unterschied zwischen
293
n, wenn die Aus2n
0, 001 sec
1016 Jahre
astronomisch
astronomisch
astronomisch
• prinzipieller (theoretischer) Berechenbarkeit und
• praktischer Berechenbarkeit.
Auch die Bandkomplexität lässt sich auf nichtdeterministische Turing – Maschinen anwenden. Eine nichtdeterministische Turing – Maschine hat Bandkomplexität
S(n), falls für jedes Wort w ∈ Z ∗ mit |w| = n alle Berechnungen der Turing –
Maschine höchstens S(n) Speicherzellen pro Band verwenden.
3.1.3
Zeit- und Bandkomplexitätsklassen
Eine Problemstellung (Aufgabe: berechne Funktionswert / Prädikat) heißt deterministisch T (n) – zeitbeschränkt (bzw. deterministsich T (n) – bandbeschränkt), wenn
es eine deterministische Turing – Maschine gibt, die das Problem löst und T (n) –
zeitbeschränkt (bzw. T (n) – bandbeschränkt) ist.
Analog kann nichtdeterministische T (n) – Zeit- / Bandbeschränktheit definiert werden.
Abkürzungen:
• DTIME (T (n)): Klasse der Probleme, die deterministisch T (n) – zeitbeschränkt
sind
• NTIME (T (n)): Klasse der Probleme, die nichtdeterministisch T (n) – zeitbeschränkt sind
• DSPACE (T (n)): Klasse der Probleme, die deterministisch T (n) – bandbeschränkt sind
• NSPACE (T (n)): Klasse der Probleme, die nichtdeterministisch T (n) – bandbeschränkt sind
Wichtig: Hier brauchen uns nur die Größenordnungen zu interessieren (assymptotisches Verhalten), wie folgende Sätze zeigen:
Satz (Bandkompression): Ist ein Problem S(n) – bandbeschränkt, so ist für jede
Konstante c ∈ R mit c > 0 das Problem auch cS(n) – bandbeschränkt.
Beweis: Konstruiere eine Turing – Maschine, die r Zeichen in ein Zeichen zusammenfasst.
Korollar: Für alle c ∈ R mit c > 0 gilt
NSPACE (S(n)) = NSPACE (cS(n))
DSPACE (S(n)) = DSPACE (cS(n))
Satz (Reduktion der Bänderzahl): Sei M eine Turing – Maschine, S(n) – bandbeschränkt mit k Bändern. Wir können eine Turing – Maschine konstruieren, die
die k Bänder von M auf einem Band simuliert.
Satz (linearer Speed – Up): Wird ein Problem von einer T (n) – zeitbeschränkten
k – Band – Turing – Maschine gelöst, so wird es für jede reelle Zahl c ∈ R, c > 0
294
KAPITEL 3. KOMPLEXITÄTSTHEORIE
auch durch eine cT (n) – zeitbeschränkte k – Band – Turing – Maschine gelöst, falls
k > 1 und limn→∞ T (n)
n = ∞.
Korollar: Unter diesen Voraussetzungen gilt
DTIME (T (n)) = DTIME (cT (n))
NTIME (T (n)) = NTIME (cT (n))
Satz (nach Savitch): Für jedes Akzeptanzproblem A (Erkennung, ob ein Wort
in einer Sprache liegt), gilt:
Ist A ∈ NSPACE (S(n)), dann gilt auch A ∈ DSPACE S 2 (n) , falls gilt (die Funktion S ist vollbandkonstruierbar): Es gibt eine Turing – Maschine, so dass gilt: Für
alle n ∈ N existiert ein Eingabewort w mit |w| = n, so dass die Turing – Maschine
tatsächlich S(n) Speicherzellen benötigt.
3.1.4
Polynomiale und nichtdeterministisch polynomiale Zeitkomplexität
Ein Problem heißt (bezüglich Zeitkomplexität) polynomial deterministisch lösbar, wenn es in DTIME nk liegt mit k ∈ N. Analog definieren wir exponentielle
Zeitkomplexität: DTIME (2n ).
Besonders interessiert
[
DTIME (2n ) \
DTIME nk
k∈N
Dies sind die Probleme, die für größere n theoretisch, aber nicht praktisch berechenbar sind. Wir diskutieren nun zwei Klassen:
[
P :=
DTIME nk
k∈N
N P :=
[
NTIME nk
k∈N
Es gilt N P ⊆ DTIME (2n ).
Frage: Gilt P = N P?
Klarer ist diese Frage für die Band- / Speicherkomplexität beantwortet:
[
PSPACE =
DSPACE ni
k≥1
N P SPACE =
[
NSPACE ni
k≥1
Nach dem Satz von Savitch:
NSPACE ni ⊆ DSPACE n2i
Dies ergibt sofort
PSPACE = N P SPACE
Es gilt
NSPACE (log n) ⊆ P ⊆ N P ⊆ PSPACE
Dies ergibt eine Klassifizierung von Problemen in solche, die praktisch lösbar sind
und solche, die aufgrund des benötigten Aufwands (exponentielle Zunahme des Aufwandes abhängig von der Größe der Eingabe) praktisch nicht mehr lösbar sind.
3.1. KOMPLEXITÄTSMASSE
3.1.5
295
Nichtdeterminismus in Algorithmen — Backtracking
Bisher hatten wir Nichtdeterminismus im Wesentlichen bei Turing – Maschinen
studiert. Nun behandeln wir Nichtdeterminismus auf der Ebene von Programmiersprachen.
Nichtdeterminismus liegt in einem System oder Algorithmus vor, wenn bei bestimmten Schritten Wahlmöglichkeiten gegeben sind.
Achtung: Wir betrachten hier keine Annahmen über Wahrscheinlichkeiten für
die Wahl der Schritte.
Für funktionale Sprachen können wir Nichtdeterminismus durch folgende Konstruktion einführen: Wir betrachten für Ausdrücke E1 und E2 den nichtdeterministischen Ausdruck
E1 E2
Dieser Ausdruck ergibt bei Auswertung den (einen) Wert von E1 oder den (einen)
Wert von E2.
Bemerkungen:
• Diese Idee lässt sich auch auf Anweisungen übertragen.
• Auch die Terminierung kann von der nichtdeterministischen Auswahl abhängen.
Wir betrachten im Folgenden nichtdeterministische Algorithmen, bei denen die Terminierung gesichert ist.
Beispiel:
1
2
4
6
8
10
12
Nichtdeterministische Erzeugung von Permutationen
fct f = (nat n) seq nat:
if n = 0 then
<>
else
einf (f(n-1), n)
fi
fct einf = (seq nat s, nat n) seq nat:
if s = <> then
<n>
else
conc (<n>, s) [] conc (<first(s)>, einf(rest(s), n))
fi
Behauptung: f (n) erzeugt nichtdeterministisch eine Permutation von {1, . . . , n},
wobei jede Permutation als Ergebnis auftreten kann.
Beweis: Induktion über n ∈ N.
1. Fall: n = 0: f (0) liefert <>.
2. Fall: Annahme: f (n − 1) liefert eine beliebige Permutation über {1, . . . , n − 1}. n
wird in f (n−1) an einer beliebigen Position eingefügt. Dies liefert eine Permutation
von {1, . . . , n}; jede Permutation kann so erzeugt werden.
Bisher hatten wir nur Beispiele betrachtet, wo jeder nichtdeterministische Berechnungspfad zu einem Ergebnis führt, das unseren Vorstellungen genügt. Oft studieren wir nichtdeterministische Berechnungen (z.B. Turing – Maschinen), wo gewisse Pfade nicht zu einem brauchbaren Ergebnis führen. Dazu definieren wir einen
neuen Ausdruck failure, der darstellt, dass dieses Ergebnis nicht brauchbar ist.
296
KAPITEL 3. KOMPLEXITÄTSTHEORIE
Beispiel:
1
2
4
6
8
fct sqrt = (nat n, nat y) nat:
if y^2 > n then
failure
elif y^2 <= n <= (y+1)^2 then
y
else
sqrt (n, 2y+1) [] sqrt (n, 2y)
fi
sqrt(n, 1) für n > 0 liefert die natürlichzahlige Wurzel, wenn wir vereinbaren, dass
nichtdeterministische Berechnungen, die mit failure enden, nicht akzeptiert werden.
Eine nichtdeterministische Berechnung darf nur mit failure enden, wenn alle
Zweige auf failure führen.
Beispiel: Erfüllbarkeit von aussagenlogischen Formeln.
Wir betrachten eine logische Formel (boolescher Ausdruck), der genau x1 , . . . , xn als
freie Identifikatoren der Sorte bool enthält. Der Ausdruck heißt erfüllbar, wenn es
Werte b1 , . . . , bn ∈ B gibt, so dass für x1 = b1 , . . . , xn = bn der Ausdruck den Wert
true liefert. Damit ist die Sequenz hb1 . . . bn i der Nachweis für die Erfüllbarkeit.
Wir stellen den Ausdruck als Funktion dar:
1
fct ausdruck = (seq bool b) bool
Ein nichtdeterministischer Algorithmus für die Erfüllbarkeit:
2
4
6
8
10
fct erfuellbar = (seq bool s) bool:
if |s| = n then
if ausdruck(s) then
true
else
failure
fi
else
erfuellbar (conc(s, <L>)) [] erfuellbar (conc(s, <0>))
fi
erfuellbar(<>) liefert lauter failure – Resultate und damit insgesamt das Resultat failure, falls der gegebene Ausdruck nicht erfüllbar ist; sonst aber true.
Für Programmiersprachen, die Konzepte wie nichtdeterministische Auswahl und
failure anbieten, müssen spezielle Auswertungstechniken (Auswertung durch Suche) eingesetzt werden.
Stichworte:
• Logikprogrammierung
• Constraint – Programmierung
Dazu existieren eigene Theorien: (failure E) = E.
Die Idee der nichtdeterministischen Auswertung lässt sich weiter verallgemeinern: Wir verwenden einen Auswahloperator
some m x : q(x)
wobei q ein Prädikat sei:
fct q = (m x) bool: ...
(3.1)
3.2. N P – VOLLSTÄNDIGKEIT
297
(3.1) liefert einen beliebigen Wert x, für den q(x) gilt. Falls für alle m x : ¬q(x)
gilt, liefert (3.1) den Wert ⊥.
Dieser Operator ist nützlich, um Algorithmen zu skizzieren.
Beispiel: Hamiltonkreis
Gegeben sei die Sorte {0, . . . , n − 1} node der Knoten im Graph. Kanten werden
durch die Funktion
fct g = (node, node) bool
dargestellt (vgl. boolesche Matrix). Es ist g (i, k) = true genau dann, wenn eine
Kante vom Knoten i zum Knoten k existiert. Ein Hamiltonkreis für einen Graph
durch eine Teilmenge S von Knoten ist ein geschlossener Weg, in dem alle Knoten
in S genau ein Mal auftreten.
Rechenvorschrift für Hamiltonkreis:
1
2
4
6
8
10
12
14
fct hk = (set node s, seq node q) seq node:
if s = emptyset then
if g (last(q), first(q)) then
q
else
failure
fi
else
node x = some node z: z in s;
if g (last(q), x) then
hk (s\{x}, conc (q, <x>))
else
failure
fi
fi
Se x ∈ s ein beliebiger Knoten aus der nichtleeren Menge s. hk (s\{x}, <x>) liefert
einen Hamiltonkreis, falls ein solcher existiert.
3.2
N P – Vollständigkeit
Probleme in N P \ P sind nach heutigem Wissensstand nur exponentiell zu lösen.
Es existieren Probleme Q in N P mit folgender Eigenschaft: Jedes andere Problem
in N P kann in polynomialer Zeit auf Q zurückgeführt werden. Dann gilt:
Q ∈ P ⇒ P = NP
Solche Probleme Q heißen N P – vollständig (N P – hart).
3.2.1
Das Erfüllungsproblem
Ein boolescher Ausdruck ist gegeben durch eine Menge von Identifikatoren x1 , . . . , xn
der Sorte bool und die Verknüpfungen ¬, ∧, ∨, . . . . Natürlich können wir eine Darstellung von booleschen Ausdrücken auf Turing – Maschinen finden. Identifikatoren
lassen sich durch Zahlen repräsentieren.
Satz: Das Erfüllbarkeitsproblem LSAT für boolesche Ausdrücke ist N P – vollständig.
Beweis:
1. LSAT ∈ N P: Wir können eine Turing – Maschine konstruieren, die nacheinander alle Werte für die Identifikatoren nichtdeterministisch festlegt und dann
überprüft, ob damit der Ausdruck den Wert true hat.
298
KAPITEL 3. KOMPLEXITÄTSTHEORIE
2. Für alle R ∈ N P existiert eine Turing – Maschine, die R in polynomialer
Zeit in das Problem LSAT überführt. Wir beschränken uns auf Probleme der
formalen Sprachen, genauer auf das Wortproblem.
Ausgangspunkt: Gegeben ist eine nichtdeterministische Turing – Maschine,
die für ein gegebenes Wort in polynomialer Zeit entscheidet, ob das Wort in
der Sprache ist. Beweisidee: Wir konstruieren eine Turing – Maschine U , die
aus der gegebenen Turing – Maschine eine neue konstruiert, die dem Erfüllbarkeitsproblem entspricht, so dass der zugrundeliegende Ausdruck genau dann
erfüllbar ist, wenn das Wort akzeptiert wird. Dazu konstruieren wir einen booleschen Ausdruck, der die Konfigurationen der Ausgangs – Turing – Maschine
durch Wahrheitswerte charakterisiert sowie die Frage, ob es sich bei der Folge
von Konfigurationen um eine Berechnung handelt, beantwortet.
3.2.2
N P – vollständige Probleme
Mittlerweile ist für eine reiche Zahl von Problemen nachgewiesen, dass sie N P –
vollständig sind. Gelingt es für eines dieser Probleme, einen deterministischen, polynomial beschränkten Algorithmus anzugeben, so ist P = N P bewiesen.
Prominente Beispiele
i. Clique
Gegeben:
• ungerichteter Graph:
– V Menge der Knoten
– R ⊆ V × V Menge der Kanten
• k∈N
Problem: Existiert eine Teilmenge C ⊆ V mit |C| = k und C × C ⊆ R?
Dies entspricht der Frage, ob der Graph einen Teilgraph mit k Knoten enthält,
in dem alle Knoten paarweise durch Kanten verbunden sind.
ii. Ganzzahlige Programmierung / Optimierung
Gegeben:
• Matrix A ∈ Zn×n
• Vektor v ∈ Zn
Gesucht ist ein Vektor c ∈ {0, 1}n , so dass gilt: Ac ≥ v (elementweise).
iii. Überdeckende Knotenmengen
Gegeben:
• ungerichteter Graph:
– Knotenmenge V
– Kantenmenge R ⊆ V × V mit R = RT
• k∈N
Gesucht ist eine Knotenmenge U ⊆ V mit |U | = k und U ist Überdeckung,
d.h.
∀v, w ∈ V : (v, w) ∈ R ⇒ ¬(v ∈ U ⇔ w ∈ U )
Anschaulich: Gibt es eine Färbung der Knoten (U ), so dass jede Kante einen
gefärbten und einen ungefärbten Knoten verbindet?
3.3. EFFIZIENTE ALGORITHMEN . . .
299
iv. Gerichteter Hamiltonkreis
(siehe oben)
v. Problem des Handlungsreisenden
Gegeben:
• Menge von Knoten V = {1, . . . , n}
• dist : V × V → N
• k∈N
Es gelte die Dreiecksungleichung:
∀i, j, l ∈ V : dist(i, l) + dist(l, j) ≥ dist(i, j)
Gesucht: Permutation x1 , . . . , xn der Knoten mit
dist(xn , x1 ) +
n−1
X
dist(xi , xi+1 ) ≤ k
i=1
Folgende verwandte Problemstellungen sind oft von annähernd gleicher Komplexität:
• Stelle fest, ob für ein Problem Q eine Lösung existiert.
• Berechne eine Lösung zu Q.
• Berechne
3.3
optimale Lösung zu Q.
Effiziente Algorithmen für N P – vollständige
Probleme
Auch wenn ein Problem nach heutigem Kenntnisstand nur mit exponentiellem Aufwand zu behandeln ist, ist für praktische Aufgaben die Reduzierung des Aufwands
(und sei es nur um Faktoren) von großer Bedeutung.
Im Folgenden studieren wir eine Reihe von Techniken zur Reduzierung des Aufwands. Für N P – vollständige Probleme müssen wir mit exponentiellem Aufwand
rechnen. Dabei sind Reduktionen im Aufwand die einzige Möglichkeit, gewisse Probleme noch halbwegs vernünftig algorithmisch behandeln zu können. Dies führt auf
die Frage, wie ein vorgegebener Algorithmus effizienter gemacht werden kann.
Techniken zur Aufwandsreduzierung beim Rechnen mit großen Aufrufbäumen:
1. Branch and Bound (geschicktes Backtracking): Der Lösungsraum
(als Baum angeordnet) wird geschickt organisiert:
• Einfach auszuwertende Zweige werden zuerst durchlaufen.
• Frühzeitig (ohne sie ganz zu durchlaufen) Zweige (Teilbäume) als uninteressant erkennen und nicht vollständig durchlaufen (pruning).
2. Approximative Verfahren: Oft sind optimale Lösungen für ein Problem
gesucht (Rundreise von Handlungsreisenden mit minimaler Länge, Scheduling
mit optimaler Verteilung etc.). In solchen Fällen sind oft Näherungslösungen
auch akzeptabel. Statt des absoluten Optimums wird eine Näherung berechnet, die hinreichend gut ist.
300
KAPITEL 3. KOMPLEXITÄTSTHEORIE
3. Dynamisches Programmieren: Treten in Aufrufbäumen Aufrufe mit gleichen Parametern öfter auf, so können wir durch Tabellieren der Ergebnisse das
mehrfache (aufwändige) Aufrufen vermeiden. Im Extremfall rechnen wir statt
Top-Down (es wird eine Kaskade von rekursiven Aufrufen erzeugt) BottomUp: Wir erzeugen eine Tabelle mit Ergebnissen, in die wir die einfachen Fälle
zuerst eintragen und dann schrittweise die Tabelle vervollständigen (Nachteile:
hoher Speicheraufwand, komplexe Datenstrukturen).
4. Probabilistische Algorithmen: Diese Algorithmen verwenden Zufallszahlen (Zufallsgeneratoren). Varianten:
• Algorithmen, die mit einer bestimmten Wahrscheinlichkeit ein korrektes
oder optimales Ergebnis liefern.
• Algorithmen, bei denen die Terminierung innerhalb einer Zeitschranke
mit einer bestimmten Wahrscheinlichkeit gesichert ist.
5. Einschränkung der Problemklasse: Häufig können eigentlich N P – vollständige Problemstellungen durch Einschränkungen auf Teilprobleme zurückgeführt werden, die polynomial lösbar sind.
6. Heuristische Methoden: Dies sind Methoden, die ad-hoc eingesetzt werden, ohne genaue Angabe, warum oder wie gut sie funktionieren.
Für eine angemessene Behandlung N P – vollständiger Probelme ist besonderes
Geschick des Programmierers erforderlich.
3.3.1
Geschicktes Durchlaufen von Baumstrukturen
Wir betrachten eine spezielle Problemstellung: Wir minimieren eine Funktion
f :M →O
wobei O eine linear geordnete Menge sei, über M . Dabei stellen wir uns vor, dass
wir die Menge als Baum anordnen können:
1
2
fct baum = (m x) set m:
if vollstaendig(x) then
{x}
4
else
union (i, baum(g(i, x)), 1, k(x))
6
fi
S
wobei union (i, f, min, max) = min≤i≤max f sei. baum(x) erzeugt eine Menge,
die baumartig angeordnet ist.
8
10
12
14
16
18
fct suchemin = (set m s) m:
if |s| = 1 then
some m x: x in s
else
m x = some m z: z in s
m y = suchemin (s\{x})
if f(x) <= f(y) then
x
else
y
fi
fi
3.3. EFFIZIENTE ALGORITHMEN . . .
301
Aufruf: suchemin(baum(x))
Dieses Verfahren ist sehr schematisch und in der Regel ineffizient. Durch geschicktes Verbinden von Suchen und erzeugen können wir den Aufwand reduzieren.
Beispiel: Erfüllbarkeitsproblem
Gegeben sei a : seq bool → bool
bool, definiert auf Sequenzen der Länge n. Wir nehmen
an, dass wir eine Approximationsfunktion fail(a, s) besitzen, die für alle booleschen
Sequenzen s mit |s| ≤ n folgende Eigenschaft hat:
fail(a, s) ∧ |s ◦ z| = n ⇒ a(s ◦ z) = f alse
Folgender Algorithmus nutzt dies aus:
1
2
4
6
8
10
12
14
16
fct suche = (seq bool x) seq bool:
if length(x) = n then
x
else
seq bool s = conc (x, <L>)
if fail (a, s) then
suche (conc (x, <0>))
else
seq bool y = suche (s)
if a(y) then
y
else
suche (conc (x, <0>))
fi
fi
fi
Dieser Algorithmus setzt voraus, dass das gesuchte Element im Baum ist. Sonst
kann über das Prädikat fail die Suche im letzten else – Zweig noch verkürzt
werden.
3.3.2
Alpha / Beta – Suche
Wir betrachten 2 – Personen – Spiele. Zwei Spieler α und β machen abwechselnd
Züge, bis eine Endstellung erreicht ist. Wir nehmen an, dass das Spiel endlich ist,
d.h. dass keine unendlichen Zugfolgen existieren. In jeder Endstellung ist ein Gewinn / Verlust für jeden Spieler festgelegt. Wir sind an einem optimalen Spiel
interessiert.
Wir bilden diese Situation in Informatikstrukturen wie folgt ab:
1. Sorten:
• player: Sorte der Spieler (= {α, β})
• position: Sorte der Stellungen
2. Spielzüge:
• fct Z = (player, position) set position
Z (s, p) beschreibt die Menge der zulässigen Züge für Spieler s in Stellung p.
3. Endstellungen:
302
KAPITEL 3. KOMPLEXITÄTSTHEORIE
• fct t = (player, position) bool
t (s, p) legt fest, ob für Spieler s die Position p eine Endstellung ist.
4. Gewinn:
• fct g = (player, position) bool
g (s, p) legt fest, ob die Stellung p als Endstellung einen Gewinn für
Spieler s darstellt.
5. Hilfsfunktion:
• fct gegner = (player) player
Es gilt gegner(α) = β und gegner(β) = α.
Wir bezeichnen eine Stellung p als sicher für Spieler s, falls wir bei optimalem Spiel
immer gewinnen. Rechenvorschrift:
1
2
4
6
fct sicher = (player s, position p) bool:
if t (s, p) then
g (s, p)
else
exists q in Z (s, p): not sicher (gegner (s), q)
fi
Es gilt (falls ¬t (gegner (s) , q)):
¬ sicher (gegner (s) , q) =
¬∃qi ∈ Z (gegner (s) , q) : ¬ sicher (gegner (gegner (s)) , q 0 ) =
∀q 0 ∈ Z (gegner (s) , q) : sicher (s, q 0 )
0
Nun betrachten wir ein leicht verändertes Problem, indem wir annehmen, dass in
jeder Endstellung p für einen Spieler s durch
fct g = (player, position) int
festgelegt wird, wie viel der Spieler gewinnt (bzw. verliert).
1
2
4
6
fct opt = (player s, position p) int:
if t (s, p) then
g (s, p)
else
max {-opt (gegner (s), q): q in Z (s, p)}
fi
Jetzt gilt für ¬t (gegner (s) , q):
− opt (gegner (s) , q) =
− max {− opt (gegner (gegner (s)) , q 0 ) : q 0 ∈ Z (gegner (s) , q)} =
min {− opt (s, q 0 ) : q 0 ∈ Z (gegner (s) , q)}
Wir betrachten nun Optimierungsmöglichkeiten. Wir unterstellen, dass wir eine
Abschätzung für unseren Gewinn
opt(s, p) ≤ maxopt(s, p)
besitzen (Abschätzung / obere Schranke für den Gewinn).
Idee: Wir suchen nun ein Spielergebnis relativ zu vorgegebenen Schranken. Dazu
definieren wir eine Sorte eint mit Werten aus Z ∪ {−∞, +∞}.
Festlegung:
max ∅ = −∞
min ∅ = +∞
3.3. EFFIZIENTE ALGORITHMEN . . .
303
Sei M eine Menge von Stellungen. Sei a = max{opt(s, p) : p ∈ M }. Wir definieren
nun eine neue Funktion
fct setopt = (player s, set position p, eint min, max) eint
Dabei gelte (es sei min ≤ max)


max falls max ≤ a
setopt(s, M, min, max) = a
falls min ≤ a ≤ max


min falls a ≤ min
Wir geben einen Algorithmus für setopt an, der effizienter ist als opt.
Wichtig:
opt(s, p) = setopt(s, {p}, −∞, +∞)
1
2
4
6
8
10
12
14
16
18
20
22
fct setopt = (player s, set position m, eint min, max) eint:
if m = emptyset then
min
else
position p = some position q: q in m
if maxopt (s, p) <= min then
setopt (s, m\{p}, min, max)
else
eint a = if t (s, p) then
g (s, p)
else
-setopt (gegner(s), Z(s, p), -max, -min)
fi
if a >= max then
max
elif a > min then
setopt (s, m\{p}, a, max)
else
setopt (s, m\{p}, min, max)
fi
fi
fi
3.3.3
Dynamisches Programmieren
Idee: Optimierung von Algorithmen mit Rekursions – Aufrufbäumen, in denen viele
gleiche Aufrufe auftreten: Jeder Aufruf wird nur ein Mal ausgewertet, tabelliert und
dann wird jeweils der ermittelte Wert verwendet.
Nachteile:
• hoher Speicherplatzbedarf
• Algorithmen werden kompliziert
Beispiel:
1
2
4
6
Handlungsreisenden – Problem
fct perm = (seq node s, set node m) set seq node:
if m = emptyset then
{s}
else
node x = some node z: z in m;
union (i, perm (insert (s, x, i), m\{x}), 0, length (s))
fi
304
KAPITEL 3. KOMPLEXITÄTSTHEORIE
mintour (m, i)
mintour (m\{i}, k1)
...
mintour (m\{i}, kn)
...
...
Abbildung 3.1: Rekursionsstruktur von mintour
insert (s, x, i) fügt den Knoten x in die Sequenz s nach Position i ein.
perm (ε, M) erzeugt die Menge aller Permutationen der Menge M.
Es sei eine Abstandsfunktion gegeben:
1
2
fct d = (node i, j) nat:
"Abstand zwischen i und j"
Aufgabe des Handlungsreisenden: Minimiere
sd(s) + d(first(s), last(s))
über s ∈ perm(ε, v) für die Knotenmenge v 6= ∅.
1
2
fct mintour = (set node m, node i: i in m) nat:
min {sd (conc (<x0>, s)): s in perm (<>, m) and last (s) = i}
mintour (m, i) berechnet von einem gegebenen Anfangsknoten x0 aus alle Wege
von x0 nach i über alle Knoten in m, wobei i ∈ m sei.
Problem des Handlungsreisenden für Knotenmenge m und Ausgangspunkt x0 ∈
/
m:
min {mintour(m, x) + d(x, x0 )}
x∈m
(Länge des kürzesten Wegs von x0 nach x über alle Knoten in m und Rückkehr zu
x0 ).
Rechenvorschrift für mintour:
1
2
4
6
fct mintour = (set node m, node i: i in m) nat:
if |m| = 1 then
d (x0, i)
else
min {mintour (m\{i}, k) + d (k, i): k in m\{i}}
fi
Dieser Algorithmus hat die Rekursionsstruktur (sei m = {k1 , . . . , kn }) aus Abbildung (3.1).
Q
Eine Analyse zeigt, dass in jeder Schicht t des Aufrufbaums nur 1≤i≤t n − i + 1
verschiedene Aufrufe auftreten. Dies liefert insgesamt n2 2n verschiedene Aufrufe,
d.h. einen Berechnungsaufwand von O(n2 2n ) gegenüber O(n!) beim ursprünglichen
Algorithmus.
3.3.4
Greedy – Algorithmen
Greedy – Algorithmen arbeiten nach dem Prinzip der lokalen Optimierung.
Idee: Wähle den nächsten Schritt (lokal) optimal.
Dies führt nicht immer zu einem globalen Optimum. Für gewisse Problemklassen
führt lokale Optimierung jedoch zu einer globalen Optimierung.
3.3. EFFIZIENTE ALGORITHMEN . . .
305
Beispiel: Dijkstras Algorithmus der kürzesten Wege
Gegeben sind eine Knotenmenge
V = {1, . . . , n}
und eine Abstandsfunktion
d : V × V → N ∪ {∞}
Wir suchen die Länge des kürzesten Wegs zwischen zwei Punkten.
Wir betten das Problem ein und betrachten folgende Aufgabe:
Gegeben:
• Knoten x, y
• Knotenmenge m ⊆ V
Gesucht: kürzester Weg von x nach y
x → x1 → x2 → x3 → · · · → xn → y
wobei x1 , . . . , xn ∈ m seien.
1
2
4
6
fct dijkstra = (set node m, node x, y) enat:
if m = emptyset then
d (x, y)
else
node c = some node z: z in m and p (z)
min {dijkstra (m\{c}, x, y), d (x, c) + dijkstra (m\{c}, c, y)}
fi
Dabei sei p (z) = ∀ node b : b ∈ m ⇒ d(x, z) ≤ d(x, b).
Eine effiziente Implementierung dieses Algorithmus arbeitet mit Techniken der
dynamischen Programmierung auf einer Matrix, die jeweils die Wege zwischen Knoten speichert, soweit nur Knoten in einer bestimmten vorgegebenen Menge als innere
Knoten verwendet werden.
306
KAPITEL 3. KOMPLEXITÄTSTHEORIE
Kapitel 4
Effiziente Algorithmen und
Datenstrukturen
(s. gleichnamiges Buch von Niklas Wirth)
Für gegebene Aufgabenstellungen ist es wichtig, geschickt Datenstrukturen zu wählen
und Algorithmen, die darauf effizient arbeiten. Wichtig bei der Wahl der Datenstrukturen ist dabei die Frage, welche Zugriffsoperationen möglichst effizient ausgeführt werden sollen.
4.1
Diskussion ausgewählter Algorithmen
Sortieren ist eine der wichtigsten Aufgaben in der betriebswirtschaftlichen Informatik.
4.1.1
Komplexität von Sortierverfahren
Gängige Sortierverfahren:
Insertsort
Selectsort
Mergesort
Quicksort
Heapsort
Bubblesort
Sortieren
Sortieren
Sortieren
Sortieren
Sortieren
Sortieren
durch
durch
durch
durch
durch
durch
Einsortieren
Auswählen
Mischen
Zerteilen
Auswahlbäume
Vertauschen
Aufwand
n2
n2
n log n
n log n
n log n
n2
Beispiel: Bubblesort
Gegeben sei ein Feld var [1:n] array int a. Folgendes Programm sortiert die
Elemente im Feld a aufsteigend.
1
2
4
6
8
var nat i := 1;
while i < n do
if a[i] > a[i+1] then
a[i], a[i+1] := a[i+1], a[i];
if i > 1 then
i := i - 1
else
i := i + 1
fi
307
308 KAPITEL 4. EFFIZIENTE ALGORITHMEN UND DATENSTRUKTUREN
10
12
else
i := i + 1
fi
od
Wir stellen uns nun die Frage, mit welchem Aufwand wir eine Sequenz sortieren
können. Bubblesort hat im Allgemeinen einen Aufwand von n2 , ist also von O(n2 ).
Wichtige Unterschiede in der Aufwandsschätzung:
• Aufwand im ungünstigsten Fall (worst case)
• durchschnittlicher Aufwand (bei gegebener Wahrscheinlichkeitsverteilung auf
den Eingaben)
Weiterer Aspekt: Speicheraufwand
Aufwand von Quicksort:
• worst case: n2
• Durchschnitt: n log n
Welches Verfahren gewählt wird, hängt stark von Rahmenbedingungen ab:
• Größe der Sequenz: Sind die Sequenzen sehr klein, so empfehlen sich einfache,
speziell darauf zugeschnittene Verfahren.
• Kenntnisse über die Anordnung der Elemente (Wahrscheinlichkeitsverteilung
der Eingabe): Sind die Elemente in der zu sortierenden Sequenz nicht zufällig
angeordnet, sondern beispielsweise schon weitgehend vorsortiert, so arbeiten
bestimmte Verfahren gut, andere extrem schlecht.
• Nebenbedingungen technischer Art: Wie sind die Sequenzen gespeichert? Wird
im Hauptspeicher (internes Sortieren) oder im Hintergrundspeicher (externes
Sortieren) sortiert?
4.1.2
Wege in Graphen
Viele praktische Fragestellungen führen auf Probleme, die sich mit Wegen in Graphen beschreiben lassen. Wir behandeln stellvertretend einen Algorithmus zum Finden von Wegen in einem gerichteten Graphen: Warshalls Algorithmus.
Gegeben:
• Knotenmenge V = {1, . . . , n}
• Kanten: c : V × V → B mit c(i, j) ∼ es gibt eine Kante von i nach j.
Wir rechnen die transitive Hülle aus:
1
2
fct th = (nat i, j) bool:
wth (i, j, n)
Idee: wth (i, j, h) bestimmt die Antwort auf die Frage
von i nach j über innere Knoten ≤ k?
4
6
8
existiert ein Weg in c
fct wth = (nat i, j, k) bool:
if k = 0 then
c (i, j)
else
wth (i, j, k-1) or (wth (i, k, k-1) and wth (k, j, k-1))
fi
4.2. BÄUME
309
Diese Idee können wir in einen Algorithmus umwandeln, der auf einem zweidimensionalen Feld a (Adjazenzmatrix) arbeitet:
{∀i, j : 1 ≤ i, j ≤ n ⇒ a[i, j] = c(i, j)}
1
2
4
6
for k := 1 to n do
for i := 1 to n do
for j := 1 to n do
a[i,j] := a[i,j] or (a[i,k] and a[k,j])
od
od
od
{∀i, j : 1 ≤ i, j ≤ n : a[i, j] = wth(i, j, n)}
Komplexität: n3
4.2
Bäume
Baumstrukturen sind ein Kernthema der Informatik.
4.2.1
Geordnete, orientierte, sortierte Bäume
Es existieren zwei Blickweisen auf Bäume:
1. Bäume als Datenstrukturen mit rekursivem Aufbau und Zugriffsfunktionen
2. Bäume als Graphen
Zu Bäumen als Graphen: Ein ungerichteter Graph G heißt nichtorientierter
Baum, falls G
• zyklenfrei und
• zusammenhängend ist.
Ein gerichteter Graph heißt orientierter Baum, falls
• eine Wurzel k existiert (ein Knoten k) und
• von k aus alle anderen Knoten auf genau einem Weg erreichbar sind.
Ein geordneter Baum ist ein orientierter Baum, bei dem für jeden Knoten eine
Reihenfolge für seine Teilbäume gegeben ist.
sort ordtree = ordtree (m root, seq ordtree subtrees)
Der Verzweigungsgrad ist die (maximale) Anzahl der Teilbäume pro Knoten.
In einem vollständigen Baum haben alle Wege (zu Blättern von der Wurzel) die
selbe Länge.
4.2.2
Darstellung von Bäumen durch Felder
Üblicherweise werden Bäume durch verkettete Zeigerstrukturen in Rechnern dargestellt. Wir können vollständige Bäume auch statisch in Feldern ablegen.
a
Darstellung im Feld: [a, b, d, e, c, f, g]
b c
d ef g
310 KAPITEL 4. EFFIZIENTE ALGORITHMEN UND DATENSTRUKTUREN
m1 m2 . . .
. . .
. . .
mk
. . .
. . .
Abbildung 4.1: B – Baum
4.2.3
AVL – Bäume
Beim Arbeiten mit Bäumen sind wir am kurzen (effizienten) Zugriff auf die Elemente
interessiert. Dies ist nur gewährleistet, wenn die Bäume nicht entarten, d.h. wenn
die Höhe des Baumes logarithmisch mit der Anzahl der Knoten im Baum wächst.
Dies könnte auf die Forderung hinauslaufen, dass alle Wege von der Wurzel zu
den Blättern sich höchstens um 1 in der Länge unterscheiden. Allerdings wird es
sehr aufwändig, beim Einfügen oder Löschen von Knoten die Ausgewogenheit zu
erhalten.
Ausweg: Statt völliger Ausgewogenheit fordern wir, dass sich die Höhe des linken
und rechten Teilbaumes um maximal 1 unterscheiden.
AVL – Baum: Sortierter Binärbaum, bei dem sich in allen Knoten die Höhen
der Teilbäume nur um maximal 1 unterscheiden. Sortiert bedeutet dabei: Für alle
Teilbäume gilt: Die Einträge im linken Teilbaum sind kleiner oder gleich der Wurzel
und die Wurzel ist kleiner oder gleich allen Einträgen im rechten Teilbaum.
Vorteil: Gesteuerte Suche ist möglich.
Wichtig: Bei AVL – Bäumen können Knoten mit geringem Aufwand eingefügt oder
gelöscht werden.
4.2.4
B – Bäume
B – Bäume sind die Grundlage für die Speicherung von Informationen in Datenbanken. Sie wurden 1970 von R. Bayer vorgeschlagen.
Sei n ∈ N eine gegebene Zahl. Ein B – Baum über einer linear geordneten Menge
M hat die Gestalt aus Abb. 4.1.
Ein B – Baum ist leer oder enthält zwischen n und 2n Teilbäume: n − 1 ≤ k ≤
2n − 1. Ein B – Baum ist sortiert.
Vorteile:
1. Es kann gezielt gesucht werden.
2. Die Höhe (Anzahl der Suchschritte) ist extrem klein. (Beim Speichern der B –
Bäume auf Hintergrundmedien sind nur wenige Seitenzugriffe erforderlich.)
3. Einfügen und Löschen von Elementen ist extrem billig.
Bemerkung: Die Wurzel des B – Baums kann weniger als n Einträge enthalten.
In B – Bäumen haben alle Wege von der Wurzel zu Blättern die gleiche Länge.
4.3
Effiziente Speicherung großer Datenmengen
Die Datenstruktur der (endlichen) Mengen wird in den meisten Programmiersprachen nicht direkt unterstützt. Im Folgenden betrachten wir Datenstrukturen für die
effiziente Behandlung großer Mengen.
4.3. EFFIZIENTE SPEICHERUNG GROSSER DATENMENGEN
4.3.1
311
Rechenstruktur der Mengen (mit Zugriff über Schlüssel)
Wir nehmen an, dass wir Datensätze der Sorte data speichern wollen. Für jeden
Datensatz setzen wir einen Schlüssel voraus:
fct key = (data) key
Wir wollen eine endliche Menge von Daten speichern und über Schlüssel darauf
zugreifen.
Die Sorte store bezeichne die Menge der endlichen Mengen von Daten. Zunächst
interessiert uns die Struktur der Elemente der Sorte store nicht, sondern die Operationen, die wir zur Verfügung haben:
1
2
4
fct
fct
fct
fct
emptystore = store
get = (store, key) data
insert = (store, data) store
delete = (store, key) store
Logische Festlegung der Wirkungsweise dieser Funktionen (Schnittstellenverhalten):
k = key(d) ⇒ get (insert (s, d) , k) = d
k 6= key(d) ⇒ get (insert (s, d) , k) = get(s, k)
delete (emptystore, k) = emptystore
k = key(d) ⇒ delete (insert (s, d) , k) = delete(s, k)
k 6= key(d) ⇒ delete (insert (s, d) , k) = insert (delete(s, k), d)
Zusätzliche Operationen:
6
fct isempty = (store) bool
fct isentry = (store, key) bool
isentry(emptystore, k) = false
isentry(insert(s, d), k) = (k = key(d) ∨ isentry(s, k))
4.3.2
Mengendarstellung durch AVL – Bäume
Wir stellen die Elemente der Sorte store durch AVL – Bäume dar:
sort store = tree data
Damit sind bestimmte Funktionen wie root, left, right auf store vorgegeben. Die
weiteren (eigentlichen) Funktionen auf store programmieren wir nun, abgestützt
auf diese Grundfunktionen.
1
2
4
6
8
10
12
14
fct get = (store s, key k) data:
if key (root (s)) = k then
root (s)
elif k < key (root (s)) then
get (left (s), k)
else
get (right (s), k)
fi
fct insert = (store s, data d) store:
if s = emptytree then
cons (emptytree, d, emptytree)
elif key (d) = key (root (s)) then
cons (left (s), d, right (s))
elif key (d) < key (root (s)) then
312 KAPITEL 4. EFFIZIENTE ALGORITHMEN UND DATENSTRUKTUREN
16
18
cons (insert (left (s), d), root (s), right (s))
else
cons (left (s), root (s), insert (right (s), d))
fi
Achtung: insert liefert einen sortierten, aber nicht notwendigerweise balancierten
Baum — selbst, wenn s balanciert ist.
1
2
4
6
8
10
12
14
16
18
20
22
24
26
28
30
32
34
36
38
40
42
44
46
fct balinsert = (store a, data d) store:
if a = emptytree then
cons (emptytree, d, emptytree)
elif key (d) = key (root (a)) then
cons (left (a), d, right (a))
elif key (d) < key (root (a)) then
store b = balinsert (left (a), d);
if hi (b) > 1 + hi (right (a)) then
leftbalance (b, root (a), right (a))
else
cons (b, root (a), right (a))
fi
else
store b = balinsert (right (a), d);
if hi (b) > 1 + hi (left (a)) then
rightbalance (left (a), root (a), b)
else
cons (left (a), root (a), b)
fi
fi
fct leftbalance = (store b, data aw, store ar) store:
if hi (left (b)) >= hi (right (b)) then
cons (left (b), root (b), cons (right (b, aw, ar)))
else
cons (cons (left (b), root (b), left (right (b))),
root (right (b)), cons (right (right (b)), aw, ar))
fi
fct rightbalance = (store al, data aw, store b) store:
if hi (right (b)) >= hi (left (b)) then
cons (cons (al, aw, left (b)), root (b), right (b))
else
cons (cons (al, aw, left (left (b))), root (left (b)),
cons (right (left (b)), root (b), right (b)))
fi
fct baldelete = (store a, key k) store:
if a = emptytree then
emptytree
elif key (root (a)) = k then
delete_root (a)
elif k < key (root (a)) then
store b = baldelete (left (a), k);
if 1 + hi (b) < hi (right (a)) then
rightbalance (b, root (a), right (a))
else
4.3. EFFIZIENTE SPEICHERUNG GROSSER DATENMENGEN
48
50
52
54
56
313
cons (b, root (a), right (a))
fi
else
store b = baldelete (right (a), k);
if 1 + hi (b) < hi (l (a)) then
leftbalance (left (a), root (a), b)
else
cons (left (a), root (a), b)
fi
fi
58
60
62
64
66
68
70
72
74
76
78
fct delete_root = (store a) store:
if left (a) = emptytree then
right (a)
elif right (a) = emptytree then
left (a)
else
data e = greatest (left (a));
store b = baldelete (left (a), key (e))
if 1 + hi (b) < hi (right (a)) then
rightbalance (b, e, right (a))
else
cons (b, e, right (a))
fi
fi
fct greatest = (store a) data:
if right (a) = emptytree then
root (a)
else
greatest (right (a))
fi
Das Beispiel der AVL – Bäume ist typisch für Datenstrukturen, die einer größeren
Grundsorte entstammen (im Beispiel Binärbäume), aber noch zusätzliche Eigenschaften (im Beispiel Balanciertheit und Sortiertheit) aufweisen. Diese Eigenschaften heißen auch Datenstrukturinvarianten.
Prinzip einer Datenstrukturinvarianten: Alle zur Verfügung gestellten Operationen haben die Eigenschaft, dass — falls die Invariante für die Eingangsparameter
gilt — diese auch für das Ergebnis gilt.
Diese Version von AVL – Bäumen hat linearen Aufwand (O(n), wobei n die
Anzahl der Elemente im Baum ist).
Idee: Wir speichern im Baum zusätzlich zur jeweiligen Wurzel die Höhe.
Ergebnis: O(log(n)) – Algorithmen für das Einfügen und Löschen.
Sortendeklaration:
sort btree = cons (btree left, data root, btree right, nat hi)
Weitere Optimierung: Statt der Höhe speichern wir eine Zahl aus {−1, 0, 1} für
linker Teilbaum höher / ausgewogen / rechter Teilbaum höher.
Beim Einfügen gilt: Wenn ein Mal balanciert wird, hat der entstehende Baum
die gleiche Höhe wie der ursprüngliche Baum vor dem Einfügen. Dies zeigt, dass
höchstens ein Balancierschritt pro Einfügung erforderlich ist.
Dieses Argument gilt nicht für das Löschen. Hier kann im ungünstigsten Fall in
jedem rekursiven Aufruf ein Balancierschritt auftreten.
314 KAPITEL 4. EFFIZIENTE ALGORITHMEN UND DATENSTRUKTUREN
4.3.3
Streuspeicherverfahren
Streuspeicherverfahren (hashing) erlauben die Speicherung von Daten unter ihren Schlüsseln in einem linearen Feld der Länge z (wobei z deutlich kleiner ist als
die Anzahl der verschiedenen Schlüssel). Wir benötigen eine Streufunktion
h : key → [0, z − 1]
Die benötigten Operationen emptystore, get, insert, delete werden nun für ein
Feld realisiert:
sort store = [0:z-1] array data
Dabei nehmen wir an, dass data ein Element empty umfasst, das als Platzhalter
dient für Feldelemente, die nicht belegt sind.
emptystore entspricht dem Feld, in dem alle Einträge den Wert empty haben.
Um ein neues Element d in die Streuspeichertabelle (hashtable) einzutragen,
berechnen wir zum Schlüssel key(d) den Index h (key (d)). Falls das entsprechende Feldelement leer ist, erfolgt der Eintrag; falls nicht, sprechen wir von einer
Kollision und nutzen ein Verfahren zur Kollisionsauflösung.
Aufgaben bei Streuspeichertabellen:
• Größe des Feldes festlegen
• Wahl der Streufunktion
• Bestimmung eines Verfahrens zur Kollisionsauflösung
Erfahrungswerte:
• Tabellengröße so wählen, dass die Tabelle höchstens zu 90% gefüllt ist.
• Streufunktion so wählen, dass sie gut streut (starke Abhängigkeit von Daten
und Häufigkeit von Daten).
Einfache Wahl (seien die Schlüssel Zahlen): h(i) = i mod z. Dabei sollte z
als Primzahl gewählt werden.
• Folgende Verfahren zur Kollisionsauflösung:
– offene Adressierung: Bei Kollisionen wird das kollidierende Element ebenfalls im Feld gespeichert.
– geschlossene Adressierung: Die Elemente werden in einem gesonderten
Bereich gespeichert.
Das Suchen eines freien Platzes bei offener Adressierung nennen wir Sondieren.
• Lineares Sondieren besteht darin, dass wir in gleich langen Schritten nach
einem freien Platz suchen:
h(k), h(k) + j, h(k) + 2j, . . .
• quadratisches Sondieren:
h(k), h(k) + 1, h(k) + 4, h(k) + 9, . . .
(natürlich jeweils mod z).
Erfahrungswert: Bei 90% Füllung sind im Mittel 2, 56 Sondierungsschritte erforderlich.