4. Prozedurales Programmieren
4. Prozedurales Programmieren
4. Prozedurales Programmieren
Sie wollen auch ein ePaper? Erhöhen Sie die Reichweite Ihrer Titel.
YUMPU macht aus Druck-PDFs automatisch weboptimierte ePaper, die Google liebt.
<strong>4.</strong> <strong>Prozedurales</strong><br />
<strong>Programmieren</strong><br />
• Der Begriff des Algorithmus‘<br />
• Grundkonzepte prozeduraler Programmierung<br />
• Formulierung von Algorithmen und Daten-<br />
strukturen mit prozeduralen Sprachen<br />
• Verifikation prozeduraler Programme<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
1
<strong>4.</strong>1 Der Begriff des Algorithmus<br />
Zentrale Begriffe der algorithmischen Vorgehens:<br />
• Algorithmus<br />
• Variablen zur Speicherung von Werten<br />
• Ausführungszustand =<br />
Speicherzustand + Steuerungszustand<br />
• Zustandsveränderung<br />
• Aktion<br />
• Ablauf<br />
• Determinismus, Determiniertheit<br />
Die prozedurale Modellierung und Programmierung<br />
baut auf den klassischen Algorithmusbegriff auf.<br />
Eine Berechnung wird als zustandsverändernder<br />
Ablauf betrachtet.<br />
Damit orientiert sie sich am Berechnungskonzept von<br />
Rechnern, der auch auf die Beschreibung von<br />
Abläufen basiert, in denen in jedem Schritt der<br />
Ausführungszustand verändert wird.<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
2
Begriffsklärung: (Algorithmus)<br />
Ein Algorithmus ist ein Verfahren zur schrittweisen<br />
Ausführung von (Berechnungs-) Abläufen, das<br />
sich präzise und endlich beschreiben lässt, so dass:<br />
- die Beschreibung auf wohlverstandenen, ausführbaren<br />
(„effektiven“) Einzelschritten basiert;<br />
- in jedem Schritt eine oder mehrere Aktionen ggf.<br />
parallel ausgeführt werden;<br />
- jede Aktion von einem Zustand in einen<br />
Nachfolgezustand führt.<br />
Man sagt, die Ausführung eines Algorithmus<br />
terminiert, wenn sie nach endlich vielen Schritten<br />
beendet ist; andernfalls spricht man von einer nichtterminierenden<br />
Ausführung.<br />
Bemerkung:<br />
Es gibt viele Begriffsklärungen für „Algorithmus“, die<br />
sich aber in den wesentlichen Aspekten gleichen.<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
3
Beispiel: (Algorithmus, der erste)<br />
Verdoppeln nach Adam Riese (1574):<br />
Dupliren:<br />
Lehret wie du ein zahl zweyfaltigen solt.<br />
Thu ihm also<br />
Schreib die zahl vor dich /<br />
mach ein Linien darunder /<br />
heb an zu forderst /<br />
Duplir die erste Figur.<br />
Kompt ein zahl die du mit einer Figur schreiben magst /<br />
so setz die unden.<br />
Wo mit zweyen /<br />
schreib die erste/ Die ander behalt im sinn.<br />
Darnach duplir die ander /<br />
und gib darzu/<br />
das du behalten hast /<br />
und schreib abermals die erste Figur /<br />
wo zwo vorhanden /<br />
und duplir fort biß zur letzsten /<br />
die schreibe gantz auß /<br />
als folgende Exempel außweisen:<br />
...<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
4
Um die Zustände zwischen den Schritten<br />
präziser fassen zu können, führt man Variablen<br />
ein, die Werte speichern können:<br />
Variablen stellen wir graphisch durch Rechtecke dar:<br />
v: true<br />
v1: 7<br />
v enthält/speichert den Wert true<br />
v1 enthält/speichert den Wert 7<br />
Begriffsklärung: (Speichervariable)<br />
Eine Speichervariable (oder einfach nur Variable)<br />
ist ein Speicher/Behälter für Werte. Charakteristische<br />
Operationen auf einer Variablen v:<br />
- Zuweisen eines Werts w an v;<br />
- Lesen des Wertes, den v enthält/speichert/hat.<br />
Der Zustand einer Variablen v ist undefiniert, wenn<br />
ihr noch kein Wert zugewiesen wurde; andernfalls ist<br />
der Zustand von v durch den gespeicherten Wert<br />
charakterisiert (vgl. Folie 179).<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
5
Beispiel: (Algorithmus, der zweite)<br />
Berechnung des größten gemeinsamen Teilers:<br />
Seien m, n, v Variablen für int-Werte;<br />
lese die Werte w1 und w2 ein, für die der ggT berechnet<br />
werden soll, und weise w1 an m und w2 an n zu;<br />
solange der Wert von m größer als 0 ist, tue Folgendes<br />
und prüfe danach wieder die Bedingung:<br />
berechne n mod m und weise das Ergebnis an v zu;<br />
weise den Wert von m an n zu;<br />
weise den Wert von v an m zu;<br />
gebe den Wert aus, den n enthält.<br />
Aufgabe:<br />
Führen Sie den Algorithmus für mehrere Eingaben aus.<br />
m: n: v:<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
6
Formulierung des obigen Algorithmus durch ein<br />
Flussdiagramm:<br />
GGT-Beginn<br />
leseInt � m<br />
leseInt � n<br />
m>0<br />
true<br />
n mod m � v<br />
m � n<br />
v � m<br />
false<br />
schreibeInt � n<br />
GGT-Ende<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
7
Formulierung des obigen Algorithmus durch ein<br />
Java-Programm:<br />
public class GGT {<br />
// Berechnet ggT für 2 gelesene Werte<br />
}<br />
public static void main( String[] args ){<br />
int m;<br />
int n;<br />
int v;<br />
}<br />
IO.println("Argument 1:");<br />
m = IO.readInt();<br />
IO.println("Argument 2:");<br />
n = IO.readInt();<br />
IO.println("ggT:");<br />
while( m>0 ) {<br />
v = n % m;<br />
n = m;<br />
m = v;<br />
}<br />
IO.println( n );<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
8
Formulierung des obigen Algorithmus durch ein<br />
C++ Programm (ohne zusätzliche Ausgaben):<br />
#include <br />
// Berechnet ggT für 2 gelesene Werte<br />
void main(){<br />
int m;<br />
int n;<br />
int v;<br />
}<br />
cin >> m;<br />
cin >> n;<br />
while( m>0 ) {<br />
v = n % m;<br />
n = m;<br />
m = v;<br />
}<br />
cout
Begriffsklärung: (Zustände)<br />
Jeder Schritt bei der Ausführung eines Algorithmus<br />
führt von einem Ausführungszustand zum<br />
Nachfolgezustand. Ein Ausführungszustand ist<br />
gekennzeichnet durch<br />
- den Speicherzustand (im Wesentlichen der<br />
Zustand der Variablen);<br />
- den Steuerungszustand (vereinfacht gesagt, die<br />
Stelle im Programm, an der die Ausführung<br />
angekommen ist).<br />
Ein Ausführungsschritt führt zu einer Zustandsveränderung,<br />
also einer Veränderung von Speicherund/oder<br />
Steuerungszustand.<br />
Begriffsklärung: (Aktion)<br />
In einem Ausführungsschritt wird üblicherweise<br />
eine Aktion ausgeführt. Aktionen sind:<br />
- Zuweisungen an Variablen<br />
- Kommunikation mit der Umgebung (Ein- und Ausgabe)<br />
Die Aktion bestimmt nachfolgende Steuerungszustände<br />
bzw. die Terminierung des Algorithmus.<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
10
Begriffsklärung: (Ablauf)<br />
Der Ablauf eines Algorithmus zu gegebenen<br />
Eingaben wird charakterisiert durch<br />
- die Sequenz der Ausführungszustände,<br />
- die Sequenz der ausgeführten Aktionen.<br />
Begriffsklärung: (Effizienz)<br />
Ein Algorithmus A heißt effizienter als ein Algorithmus<br />
B, wenn der „Aufwand“ zur Ausführung von A geringer<br />
ist als der „Aufwand“ zur Ausführung von B und zwar<br />
für die zulässigen Eingabedaten.<br />
Oft wird nur erwartet, dass der Aufwand für alle bis<br />
auf endlich viele Eingaben geringer ist oder nur im<br />
Mittel über die zulässigen Eingaben.<br />
Mit den gemachten Präzisierungen lässt sich der<br />
Aufwand einer Ausführung quantifizieren:<br />
- Zeitkomplexität: Wie viele Schritte braucht der<br />
Algorithmus in Abhängigkeit von den Eingabewerten?<br />
- Raumkomplexität: Wie viel Speicherplatz braucht der<br />
Algorithmus in Abhängigkeit von den Eingabewerten?<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
11
Begriffsklärung: (deterministisch)<br />
Ein Algorithmus heißt deterministisch, wenn<br />
für alle Eingabedaten der Ablauf des Algorithmus<br />
eindeutig bestimmt ist.<br />
Andernfalls heißt er nicht-deterministisch.<br />
Beispiel: (nicht-deterministischer Algorithmus)<br />
Aufgabe:<br />
Erkenne, ob eine Eingabezeichenreihe über Kleinbuchstaben<br />
eines der Worte “otto“, “toto“, “total“ enthält.<br />
Nicht-deterministischer Algorithmus:<br />
• Seien<br />
- z die Eingabe und<br />
- pflän(z) = { 0, ... , länge(z)-4 }<br />
• Solange pflän ≠ ∅ tue Folgendes:<br />
- wähle ein x aus pflän aus;<br />
- pflän = pflän \ { x } ;<br />
- w = „z ohne die ersten x Buchstaben“ ;<br />
- prüfe, ob w mit “otto“, “toto“, “total“ beginnt ;<br />
- wenn ja, terminiert der Algorithmus mit „ja“;<br />
andernfalls setze die Schleife fort.<br />
• Terminiere mit „nein“.<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
12
Begriffsklärung: (determiniert)<br />
Ein Algorithmus heißt determiniert, wenn er<br />
bei gleichen zulässigen Eingabewerten stets das<br />
gleiche Ergebnis liefert.<br />
Andernfalls heißt er nicht-determiniert.<br />
Beispiele: (Determiniertheit)<br />
1. Jeder Algorithmus, der eine Funktion berechnet,<br />
ist determiniert.<br />
2. Der Algorithmus von der letzten Folie ist<br />
determiniert.<br />
<strong>4.</strong>2 Grundkonzepte<br />
prozeduraler Programmierung<br />
Algorithmen lassen sich mit unterschiedlichen<br />
Sprachmitteln beschreiben:<br />
- umgangssprachlich<br />
- mit graphischen Notationen<br />
- mit mathematischer Sprache<br />
- mit programmiersprachlichen Mitteln.<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
13
Begriffsklärung: (prozedurales Paradigma)<br />
Gemäß des prozeduralen Paradigmas (vgl. F2.39) wird<br />
- der Zustand eines Systems mit Variablen beschrieben<br />
- werden die möglichen Systemabläufe algorithmisch<br />
formuliert und<br />
- bilden Prozeduren das zentrale Strukturierungs-<br />
und Abstraktionsmittel.<br />
Beispiel: (Prozedurale Systemsicht)<br />
Ein Rechensystem (z.B. PC) kann man prozedural<br />
wie folgt beschreiben:<br />
- Jede Datei ist eine Variable für Listen von Bytes.<br />
- Nach dem Starten des Rechners wird ein nichtterminierender<br />
Algorithmus ausgeführt:<br />
}<br />
while( true ) {<br />
warte auf Eingabe eines Programmnamens;<br />
starte Programm mit eingegebenem Namen<br />
als parallelen Algorithmus;<br />
- Jedes Programm entspricht dabei einer Prozedur.<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
14
<strong>4.</strong>2.1 Sprachliche Basis:<br />
Teilsprache von Java<br />
Der Abschnitt <strong>4.</strong>2 stellt diejenigen Grundkonzepte<br />
der prozeduralen Programmierung vor, die als<br />
Voraussetzung für die objektorientierte<br />
Programmierung benötigt werden:<br />
• Deklaration und Verwendung von Variablen<br />
• Anweisungen<br />
• Deklaration und Verwendung von Prozeduren<br />
Zur praktischen Programmierung verwenden wir<br />
eine Teilsprache von Java.<br />
Vorteile:<br />
- Der Übergang zur objektorientierten Programmierung<br />
wird einfacher.<br />
- Der Zusammenhang zwischen prozeduraler und<br />
objektorientierter Programmierung wird klarer.<br />
Nachteile:<br />
- Da klassenlose, rein prozedurale Programmierung<br />
von Java nicht unterstützt wird, entsteht ein<br />
notationeller Mehraufwand.<br />
- Bestimmte Sprachelemente bleiben zunächst<br />
unerklärt.<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
15
Vorgehen:<br />
- Basisdatentypen<br />
- Ausdrücke<br />
- Variablendeklarationen<br />
- Programmstruktur und vereinfachte Notation<br />
Basisdatenstrukturen in Java:<br />
Java bietet insbesondere Basisdatenstrukturen<br />
mit den Typen<br />
boolean<br />
char<br />
int, long, short,<br />
float, double<br />
sowie entsprechende Funktionen und Konstanten.<br />
Die nichtstrikten Operationen sind:<br />
_ ? _ : _ statt if _ then _ else _ in ML<br />
_ && _ statt _ andalso _ in ML<br />
_ || _ statt _ orelse _ in ML<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
16
Ausdrücke in Java:<br />
Mittels der Konstanten und Funktionen der<br />
elementaren Datenstrukturen lassen sich analog<br />
zu ML Ausdrücke bilden.<br />
(Darüber hinaus kann man deklarierte Prozeduren<br />
in Ausdrücken aufrufen. Dadurch können bei der<br />
Auswertung von Ausdrücken Seiteneffekte<br />
entstehen.)<br />
Beispiel: (Ausdrücke in Java)<br />
5 + 89<br />
45 / 6<br />
47474747L * a für geeignetes a<br />
3.6 * (-4 + 23)<br />
d == 78 + e für geeignete d, e<br />
7 >= 45 == true<br />
abs(a) + abs(d) für geeignete a, d, abs<br />
sein | !sein für geeignete Variable sein<br />
true || tuEs() für geeignete Prozedur tuEs<br />
a > b ? a : b // liefert max(a,b)<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
17
Variablendeklarationen in Java:<br />
Eine Variablendeklaration hat die Form:<br />
;<br />
und definiert eine neue Variable vom angegebenen<br />
Typ mit dem angegebenem Bezeichner. Beispiele:<br />
int a;<br />
boolean b;<br />
float meineGleitkommavariable;<br />
char cvar;<br />
Eine Variablendeklarationsliste entsteht durch<br />
Aneinanderreihen von einzelnen Deklarationen.<br />
Programmstruktur und -syntax:<br />
Ein prozedurales Programm besteht aus:<br />
- einer Liste von Typdeklaration<br />
- einer Liste von globalen Variablendeklarationen<br />
- einer Liste von Prozedurdeklarationen<br />
- einer Hauptprozedur<br />
In Java lassen sich prozedurale Programme mit<br />
folgendem Programmrahmen formulieren:<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
18
.java<br />
public class {<br />
}<br />
static <br />
...<br />
static <br />
static <br />
...<br />
static <br />
static <br />
...<br />
static <br />
public static void main(String[] args)<br />
<br />
wobei k ≥ 0, m ≥ 0, n ≥ 0.<br />
Bemerkung:<br />
• Die Reihenfolge der Deklarationen kann vertauscht<br />
werden.<br />
• Die hier nicht erklärten Sprachkonstrukte werden<br />
in Kapitel 5 behandelt.<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
19
Konventionen:<br />
• Wir gehen davon aus, dass die Datei InputOutput.java<br />
im aktuellen Dateiverzeichnis liegt. Sie stellt bereit:<br />
public class IO {<br />
}<br />
public static int readInt(){...}<br />
public static String readString(){...}<br />
public static char readChar(){...}<br />
public static void print(Object o){...}<br />
public static void println(Object o){...}<br />
Mit den Prozeduren print und println können<br />
insbesondere Werte und Objekte der Typen<br />
int, char und String ausgegeben werden.<br />
• Im Folgenden werden wir die ersten beiden und<br />
die letzte Zeile des Programmschemas der letzten<br />
Folie sowie die Schlüsselwörter static und<br />
public auf Folien der Übersichtlichkeit halber<br />
weglassen.<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
20
Beispiel: (Notation prozeduraler Programme)<br />
Foo.java<br />
public class Foo {<br />
}<br />
static int a;<br />
static boolean bvar;<br />
static int abs( int n ) {<br />
if( n>=0 ){<br />
return n;<br />
} else {<br />
return -n;<br />
}<br />
}<br />
static void setA() {<br />
a = abs(a);<br />
}<br />
public static void main( String[] args ){<br />
}<br />
IO.println("Eingabe von a:");<br />
a = IO.readInt();<br />
setA();<br />
IO.println( a );<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
21
Abkürzend schreiben wir hier:<br />
int a;<br />
boolean bvar;<br />
int abs( int n ) {<br />
if( n>=0 ){<br />
return n;<br />
} else {<br />
return -n;<br />
}<br />
}<br />
void setA() {<br />
a = abs(a);<br />
}<br />
void main( String[] args ){<br />
}<br />
println("Eingabe von a:");<br />
a = readInt();<br />
setA();<br />
println( a );<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
22
Überblick übers weitere Vorgehen:<br />
• Anweisungen<br />
• Prozeduren<br />
• Variablen in Programm und Speicher<br />
• Felder<br />
• Benutzerdefinierte Typen<br />
• Sichtbarkeit von Bindungen<br />
• Iteration und Rekursion<br />
• Weitere prozedurale Sprachkonzepte<br />
Bemerkung:<br />
• Die Konzepte sind rekursiv von einander abhängig:<br />
- Anweisungen benutzen Variablen und Ausdrücke<br />
- Prozeduren basieren auf Anweisungen<br />
- Verwendung von Variablen kann nur im Kontext<br />
von Anweisungen und Prozeduren erklärt werden.<br />
- Ausdrücke erlauben die Verwendung von<br />
Prozeduren.<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
23
• In der programmiersprachlichen Realisierung<br />
der Konzepte verwischen ihre Grenzen; z.B.:<br />
- Parameter lassen sich wie Variable verwenden.<br />
- Einfache Anweisungen lassen sich in Ausdrücken<br />
verwenden.<br />
<strong>4.</strong>2.2 Anweisungen<br />
Begriffsklärung: (Anweisung)<br />
Anweisungen sind programmiersprachliche Beschreibungsmittel:<br />
- Einfache Anweisungen beschreiben Aktionen.<br />
- Zusammengesetzte Anweisungen beschreiben,<br />
wie mehrere Aktionen auszuführen sind, also<br />
Teile von Algorithmen.<br />
Beispiel: (Anweisungen)<br />
1. Zuweisung eines Werts an eine Variable:<br />
= ;<br />
2. Schleifenanweisung:<br />
while( ) {<br />
}<br />
<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
24
Bemerkung:<br />
Unterschied: Anweisung / Ausdruck<br />
- Die Auswertung von Ausdrücken liefert ein<br />
Ergebnis.<br />
- Die Ausführung von Anweisungen verändert<br />
den Zustand. Im Allg. liefert sie kein Ergebnis.<br />
Wir betrachten die folgenden Anweisungen und ihre<br />
Realisierung in Java:<br />
• Einfache Anweisungen:<br />
- Zuweisung<br />
- Prozeduraufruf<br />
• Anweisungsblöcke<br />
• Schleifenanweisungen:<br />
- while-, do-, for-Anweisung<br />
• Verzweigungsanweisungen:<br />
- bedingte Anweisung<br />
- Fallunterscheidung<br />
- Auswahlanweisung<br />
• Sprunganweisungen:<br />
- Abbruchanweisung<br />
- Rückgabeanweisung<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
25
Einfache Anweisungen<br />
Zuweisung: (engl. assignment)<br />
Syntax in Java:<br />
= ;<br />
Semantik:<br />
Werte den Ausdruck aus und weise das Ergebnis<br />
der Variablen zu. (In Java ist eine Zuweisung<br />
syntaktisch ein Ausdruck, liefert also einen Wert und<br />
zwar das Ergebnis der Auswertung von der rechten<br />
Seite der Zuweisung.)<br />
Beispiele:<br />
a = 27 % 23;<br />
b = true;<br />
meineGleitkommavariable = 3.14;<br />
Sprechweise:<br />
„Variable b ergibt sich zu true“ oder<br />
„Variable b wird true zugewiesen“ .<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
26
Bemerkung:<br />
In einer Zuweisung (und anderen Sprachkonstrukten)<br />
kann eine Variable v mit zwei Bedeutungen<br />
vorkommen:<br />
1. Das Vorkommen links vom Zuweisungszeichen<br />
meint die Variable. Man spricht vom L-Wert<br />
(engl. l-value) des Ausdrucks v .<br />
2. Das Vorkommen rechts vom Zuweisungszeichen<br />
meint den in v gespeicherten Wert. Man spricht<br />
vom R-Wert (engl. r-value) des Ausdrucks v .<br />
Die Unterscheidung wird wichtiger, wenn auch links<br />
des Zuweisungszeichens komplexere Ausdrücke<br />
stehen.<br />
Beispiel: (L-Wert, R-Wert)<br />
a = 7 + a ;<br />
L-Wert R-Wert<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
27
Prozeduraufruf: (engl. procedure call)<br />
Syntax:<br />
( ) ;<br />
� ε | <br />
� <br />
Semantik:<br />
| , <br />
Werte die Ausdrücke der aktuellen Parameter aus.<br />
Übergebe die Ergebnisse an die formalen Parameter.<br />
Führe die Anweisungen der Prozedur aus.<br />
Liefere den Rückgabewert, wenn vorhanden.<br />
Beispiele:<br />
1. Prozeduraufruf mit Ergebnis:<br />
a = ggt(a,abs(45-87)); // Zuweisung<br />
2. Prozeduraufruf mit Seiteneffekt:<br />
print( ggt(24,248)); // Anweisung<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
28
Anweisungsblöcke<br />
Ein Anweisungsblock (engl. block statement) ist eine<br />
Liste bestehend aus Deklarationen und Anweisungen.<br />
Syntax in Java:<br />
{ }<br />
� ε<br />
Semantik:<br />
| <br />
| <br />
| <br />
| <br />
Stelle den Speicherplatz für die Variablen bereit und<br />
führe die Anweisungen der Reihe nach aus.<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
29
Beispiel:<br />
{ int i; i = 7; int a; a = 27 % i; }<br />
Üblicherweise schreibt man Deklarationen und<br />
Anweisungen untereinander, so dass ein Textblock<br />
entsteht.<br />
{<br />
}<br />
int i;<br />
int x;<br />
i = readInt();<br />
x = abs(i);<br />
Schleifenanweisungen<br />
Schleifenanweisungen (engl. loop statements)<br />
steuern die iterative, d.h. wiederholte Ausführung<br />
von Anweisungen/Anweisungsblöcken.<br />
while-Anweisung:<br />
Syntax in Java:<br />
while( ) <br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
30
Semantik:<br />
Werte den Ausdruck aus, die sogenannte Schleifenbedingung.<br />
Ist die Bedingung erfüllt, führe die<br />
Anweisung aus, den sogenannten Schleifenrumpf,<br />
und wiederhole den Vorgang. Andernfalls beende<br />
die Ausführung der Schleifenanweisung.<br />
Beispiel:<br />
Aus der Prozedur ggt:<br />
while( m>0 )<br />
{<br />
v = n % m;<br />
n = m;<br />
m = v;<br />
}<br />
Bemerkung:<br />
Der Schleifenrumpf ist in den meisten Fällen ein<br />
Anweisungsblock. Syntaktisch korrekt ist aber<br />
auch z.B.:<br />
while( true ) print(i);<br />
while( m>0 ) {<br />
v = n % m;<br />
n = m;<br />
m = v;<br />
}<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
31
do-Anweisung:<br />
Syntax in Java:<br />
do while( ) ;<br />
Semantik:<br />
Führe die Anweisung aus. Werte danach den booleschen<br />
Ausdruck aus. Ist die Bedingung erfüllt ist,<br />
wiederhole den Vorgang. Andernfalls beende die<br />
Ausführung der Schleifenanweisung.<br />
Bemerkung:<br />
Die do-Anweisung ist immer dann sinnvoll, wenn man<br />
einen Vorgang mindestens einmal ausführen muss.<br />
In solchen Situationen spart man sich gegenüber der<br />
Verwendung der while-Schleife die anfänglich unnötige<br />
Auswertung der Bedingung.<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
32
Beispiel:<br />
Wir betrachten die Ausgabe der Elemente einer nichtleeren<br />
Liste. Wir gehen davon aus, dass es einen<br />
Typ<br />
IntList mit Prozeduren head, tail und isempty gibt:<br />
IntList grades;<br />
int g;<br />
... // Zuweisung an die Variable noten<br />
// Ausgabe der nicht-leeren Notenliste<br />
do {<br />
g = head(grades);<br />
println(g);<br />
grades = tail(grades);<br />
} while( !isempty(grades) );<br />
for-Anweisung (Zählanweisung):<br />
Die for-Anweisung dient vorrangig zur Bearbeitung<br />
von Vektoren und Matrizen, deren einzelne<br />
Komponenten über Indizes angesprochen werden.<br />
Wir betrachten zunächst ein Beispiel:<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
33
Beispiel: (Zählanweisung)<br />
// Skalarprodukt<br />
void main( String[] args ){<br />
int i;<br />
}<br />
// zwei Vektoren mit jeweils 3 Elementen<br />
int v1[] = new int[3];<br />
int v2[] = new int[3];<br />
int skalprod;<br />
v1[0] = 1;<br />
v1[1] = 3;<br />
v1[2] = 8;<br />
v2[0] = 2;<br />
v2[1] = 3;<br />
v2[2] = 4;<br />
skalprod = 0;<br />
for( i = 0; i
Syntax in Java:<br />
for( ;<br />
;<br />
) <br />
� = <br />
Semantik:<br />
Ausdrucksanweisung:<br />
1. Produktion: wie bei Zuweisung.<br />
| ++<br />
| --<br />
2./3. Produktion: Lese den Wert der Variablen.<br />
Erhöhe/erniedrige den Wert der Variablen um 1.<br />
Liefere den gelesenen Wert zurück.<br />
for-Schleife:<br />
Ausdrucksanweisung1<br />
boolescher<br />
Ausdruck<br />
true<br />
Anweisung<br />
Ausdrucksanweisung2<br />
false<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
35
Verzweigungsanweisungen<br />
Verzweigungsanweisungen (engl. branch statements)<br />
stellen Bedingungen an die Ausführung einer<br />
Anweisung oder wählen einen von mehreren<br />
Zweigen zur Ausführung aus.<br />
Bedingte Anweisung:<br />
Syntax in Java:<br />
if( ) <br />
Semantik:<br />
Werte den Ausdruck aus. Ist das Ergebnis true,<br />
führe die Anweisung aus. Andernfalls ist die<br />
Ausführung sofort beendet.<br />
Beispiele:<br />
if( i != 0 ) { n = k/i; }<br />
if( gute_Idee_vorhanden<br />
{<br />
}<br />
&& genug_Zeit_zur_Vorbereitung )<br />
halte_anregende_Weihnachtsvorlesung();<br />
mache_schneller_damit_frueher_fertig();<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
36
Fallunterscheidung:<br />
Syntax in Java:<br />
if( ) <br />
Semantik:<br />
else <br />
Werte den Ausdruck aus. Ist das Ergebnis true,<br />
führe Anweisung1, andernfalls Anweisung2 aus.<br />
Beispiel:<br />
if( i!=0 ) {<br />
n = k/i;<br />
n++;<br />
} else {<br />
}<br />
fehlerbehandlung(“Division durch Null“);<br />
Auswahlanweisung:<br />
Auswahlanweisungen erlauben es, in Abhängigkeit<br />
von dem Wert eines Ausdrucks direkt in einen von<br />
endlich vielen Fällen zu verzweigen. In Java<br />
gibt es dafür die switch-Anweisung. Wir verzichten<br />
hier auf eine genaue Beschreibung von Syntax und<br />
Semantik und betrachten ein Beispiel:<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
37
Beispiel: (Auswahlanweisung)<br />
// AuswahlanweisungsTest<br />
void main( String[] args ){<br />
}<br />
char c;<br />
boolean machWeiter = true;<br />
do {<br />
print("Ein Zeichen aus {a,b,c,e}:");<br />
c = readChar();<br />
switch( c ) {<br />
case 'a': proza(); break;<br />
case 'b': prozb(); break;<br />
case 'c': prozc(); break;<br />
case 'e':<br />
machWeiter = false;<br />
break;<br />
default:<br />
}<br />
print("Falsches Eingabezeichen");<br />
} while( machWeiter );<br />
wobei proza, prozb, prozc beliebige Prozeduren<br />
sind, zum Beispiel:<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
38
void proza() {<br />
}<br />
/* hier könnte 'was Interessantes stehen */<br />
println("Ich bin Prozedur A. Alles gut?");<br />
println("Probleme mit dem Weina'stress?\n");<br />
void prozb() {<br />
}<br />
/* hier natürlich auch */<br />
println("Wer A sagt, muss auch B sagen.");<br />
println("Haben Sie schon A gesagt.\n");<br />
void prozc() {<br />
}<br />
println("C wie no comment. Cool,oder?\n");<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
39
Sprunganweisungen<br />
Sprunganweisungen (engl. jump statements) legen<br />
eine Fortsetzungsstelle der Ausführung fest, die<br />
möglicherweise weit von der aktuellen Anweisung<br />
entfernt liegt. Wir betrachten hier nur Sprünge, die<br />
der Programmstruktur folgen.<br />
Abbruchanweisung:<br />
Syntax in Java:<br />
break;<br />
Semantik:<br />
Die Ausführung wird am Ende der umfassenden<br />
zusammengesetzen Anweisung fortgesetzt.<br />
Beispiele:<br />
1. In Auswahlanweisung: siehe oben.<br />
2. In Schleifenanweisungen:<br />
while( true ) {<br />
...<br />
if( ) break;<br />
...<br />
}<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
40
Rückgabeanweisung:<br />
Syntax in Java:<br />
return ;<br />
return ;<br />
Semantik:<br />
1. Produktion:<br />
Nur in Prozeduren ohne Rückgabewert erlaubt.<br />
Beende die Ausführung der Prozedur.<br />
Setze die Ausführung an der Aufrufstelle fort.<br />
2. Produktion:<br />
Nur in Prozeduren mit Rückgabewert erlaubt.<br />
Werte den Ausdruck aus.<br />
Beende die Ausführung der Prozedur.<br />
Liefere den Wert des Ausdrucks als Ergebnis<br />
und setze die Ausführung an der Aufrufstelle fort.<br />
Beispiel:<br />
int abs( int n ) {<br />
if( n>=0 ) {<br />
return n;<br />
} else {<br />
return -n;<br />
}<br />
}<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
41
<strong>4.</strong>2.3 Prozeduren<br />
Prozeduren erlauben es, von konkreten Anweisungen<br />
bzw. Anweisungssequenzen zu abstrahieren.<br />
D.h. Prozeduren legen die Parameter einer zusammengesetzten<br />
Anweisung fest und geben ihr einen Namen.<br />
Dies ermöglicht:<br />
- Wiederverwendung,<br />
- Schnittstellenbildung und Information Hiding.<br />
Darüber hinaus ermöglichen Prozeduren die rekursive<br />
Ausführung von Anweisungen.<br />
Begriffsklärung: (Prozedur)<br />
Eine Prozedur ist eine Abstraktion einer Anweisung.<br />
Sie gibt der Anweisung einen Namen und legt fest,<br />
was die Parameter der Anweisung sind. Prozeduren<br />
können Ergebnisse liefern.<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
42
Bemerkung:<br />
Prozeduren abstrahieren Anweisungen genauso, wie<br />
Funktionen Ausdrücke abstrahieren (vgl. Folie 108).<br />
Beispiel: (Prozedur als Abstraktion)<br />
Aufgabe:<br />
Berechne den Absolutbetrag einer ganzen Zahl<br />
gespeichert in Variable i und schreibe ihn in die<br />
Variable x.<br />
Algorithmus:<br />
Wenn i größer oder gleich 0, liefere i als Ergebnis;<br />
andernfalls –i. Weise das Ergebnis an x zu.<br />
void main(...){<br />
int i;<br />
int x;<br />
i = readInt();<br />
int result;<br />
if( i>=0 ) {<br />
result = i;<br />
} else {<br />
result = -i;<br />
}<br />
x = result;<br />
}<br />
Abstraktion bzgl. i,<br />
result wird zum<br />
Ergebnis.<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
43
Deklaration von Prozeduren/Methoden:<br />
Syntax in Java:<br />
( )<br />
<br />
Semantik:<br />
int abs( int n ) {<br />
if( n>=0 ) {<br />
return n;<br />
} else {<br />
return -n;<br />
}<br />
}<br />
void main(...){<br />
int i;<br />
int x;<br />
i = readInt();<br />
}<br />
x = abs(i);<br />
Die Semantik ist durch den Aufrufmechanismus<br />
und den Anweisungsblock, den sogenannten<br />
Prozedurrumpf bestimmt (Genaueres siehe unten).<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
44
Beispiele:<br />
1. Iterativer Algorithmus zur Berechnung vom ggT:<br />
int ggT ( int m, int n ) {<br />
int v;<br />
}<br />
while( m>0 ) {<br />
v = n % m;<br />
n = m;<br />
m = v;<br />
}<br />
return n;<br />
2. Rekursiver Algorithmus zur Berechnung vom ggT:<br />
int ggT ( int m, int n ) {<br />
}<br />
if( m == 0 ) {<br />
return n;<br />
} else {<br />
}<br />
Bemerkung:<br />
return ggT(n%m,m);<br />
Ein- und Ausgabeoperationen sind typischerweise<br />
durch Prozeduren realisiert.<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
45
Definition: (Funktionsprozedur)<br />
Prozeduren, die Ergebnisse liefern, heißen<br />
Funktionsprozeduren.<br />
Begriffsklärung: (Effekte)<br />
Die Ausführung von Prozeduren verändert den<br />
Speicherzustand und bewirkt Ein- und Ausgaben.<br />
Zusammenfassend sprechen wir von den Effekten<br />
der Ausführung einer Prozedur, aufgeteilt in:<br />
• Haupteffekte: Abliefern von Ergebnissen,<br />
Verändern von Variablenparametern;<br />
• Seiteneffekte: Ein- und Ausgabe, Verändern<br />
globaler Variablen, Erzeugen und Löschen<br />
dynamischer Variablen (z.B. Instanzvariablen).<br />
Bemerkung:<br />
• Variablenparameter und dynamische Variablen<br />
betrachten wir erst später.<br />
• Eine Funktionsprozedur kann benutzt werden, um<br />
eine Funktion zu implementieren (Beispiel ggT).<br />
Sie kann aber auch hauptsächlich zur Realisierung<br />
von Seiteneffekten benutzt werden.<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
46
Beispiel: (Prozedur mit Seiteneffekt)<br />
Prozedur, die Zahlen von der Eingabe liest und<br />
addiert, bis eine Null eingegeben wird. Die Summe<br />
der eingegebenen Zahlen wird ausgedruckt, die<br />
Anzahl der Eingaben zurückgeliefert:<br />
// Summiere<br />
int summiere() {<br />
// Arbeitet nur bei korrekter Eingabe<br />
// der Zahlen durch Benutzer<br />
int anz = 0;<br />
int summe = 0;<br />
int eingabe;<br />
}<br />
do {<br />
print("Summand (0 beendet):");<br />
eingabe = readInt();<br />
summe += eingabe;<br />
anz++;<br />
} while( eingabe != 0 );<br />
println( "Summe: " + summe );<br />
return anz-1;<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
47
Anwendung der Prozedur summiere:<br />
void main( String[] args ) {<br />
boolean b = true;<br />
}<br />
while( b ) {<br />
int anzahl;<br />
anzahl = summiere();<br />
println("Es wurden "+ anzahl<br />
+ " Zahlen summiert");<br />
}<br />
println("Beenden mit j/n:");<br />
char c;<br />
c = readChar();<br />
if( c == 'j' ) {<br />
b = false;<br />
}<br />
Bemerkung:<br />
• Die Hauptprozedur main wird vom Betriebssystem<br />
aufgerufen. Das Starten eines prozeduralen<br />
Programms entspricht dem Aufruf der Hauptprozedur.<br />
• Argumente vom Programmaufruf werden main als<br />
String-Feld mitgegeben.<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
48
Definition: (rekursive Prozedurdekl.)<br />
Eine Prozedurdeklaration P heißt direkt rekursiv,<br />
wenn der Prozedurrumpf einen Aufruf von P enthält.<br />
Eine Menge von Prozedurdeklarationen heißen<br />
verschränkt rekursiv oder indirekt rekursiv<br />
(engl. mutually recursive), wenn die Deklarationen<br />
gegenseitig voneinander abhängen.<br />
Eine Prozedurdeklaration heißt rekursiv, wenn<br />
sie direkt rekursiv ist oder Element einer Menge<br />
verschränkt rekursiver Prozeduren ist.<br />
Bemerkung:<br />
Wir identifizieren Prozeduren mit ihren Prozedurdeklarationen.<br />
D.h. zwei Prozedurdeklarationen<br />
deklarieren immer zwei unterschiedliche Prozeduren.<br />
( Im Unterschied dazu konnten zwei Funktionsdeklarationen<br />
die gleiche Funktion deklarieren.)<br />
Beispiel:<br />
int p1 ( int n ) { return 1; }<br />
int p2 ( int n ) { return 1; }<br />
sind zwei unterschiedliche Prozeduren.<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
49
Beispiel: (rekursive Prozeduren)<br />
// Aufrufbaum<br />
final int TAB = 4;<br />
int indent = 0 ; // Einruecktiefe<br />
void drucke_eingerueckt( String s ) {<br />
int i;<br />
for( i = 0; i
void p( int i ) {<br />
drucke_eingerueckt("Betrete p");<br />
indent += TAB;<br />
if( i > 2 ) {<br />
p( 2 );<br />
q( 2 );<br />
r( 2 );<br />
}<br />
indent -= TAB;<br />
drucke_eingerueckt("Verlasse p");<br />
}<br />
void q( int iq ) {<br />
drucke_eingerueckt("Betrete q");<br />
indent += TAB;<br />
p(iq);<br />
indent -= TAB;<br />
drucke_eingerueckt("Verlasse q");<br />
}<br />
- r und s sind nicht rekursiv.<br />
- q ist verschränkt rekursiv.<br />
- p ist direkt und verschränkt rekursiv.<br />
(Beachte: q wurde vor seiner Deklaration<br />
angewendet.)<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
51
Mit der Hauptprozedur:<br />
public static void main( String[] args ) {<br />
drucke_eingerueckt("Betrete main");<br />
indent += TAB;<br />
p(3);<br />
r(1);<br />
indent -= TAB;<br />
drucke_eingerueckt("Verlasse main");<br />
} }<br />
ergibt sich folgende Ausgabe:<br />
Betrete main<br />
Betrete p<br />
Betrete p<br />
Verlasse p<br />
Betrete q<br />
Betrete p<br />
Verlasse p<br />
Verlasse q<br />
Betrete r<br />
Verlasse r<br />
Verlasse p<br />
Betrete r<br />
Betrete s<br />
Verlasse s<br />
Verlasse r<br />
Verlasse main<br />
Ablauf<br />
Jeder Balken entspricht einer Prozedurinkarnation.<br />
Es gibt mehrere Inkarnationen der gleichen Prozedur.<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
52
Bemerkung:<br />
Eine wichtige Struktur des Ablaufs prozeduraler<br />
Programme ist der Aufrufbaum. Am Beispiel:<br />
p.2<br />
p.1<br />
p.3<br />
main.1<br />
r.2<br />
q.1 r.1 s.1<br />
Die Lebensdauern von Inkarnationen der gleichen<br />
Prozedur können sich überlappen (z.B.: p.1 und p.3).<br />
Begriffsklärungen: (zur Prozedurausführung)<br />
Beim Aufruf einer Prozedur p wird eine neue<br />
Inkarnation von p erzeugt. Dabei wird:<br />
- Speicherplatz für die lokalen Variablen angelegt;<br />
- die Anweisung gemerkt, an der nach Ausführung<br />
der Prozedurinkarnation fortzusetzen ist.<br />
Die Lebensdauer einer Prozedurinkarnation beginnt<br />
mit deren Erzeugung und endet mit der Ausführung<br />
des Rumpfes zu dieser Inkarnation. Die Lebensdauer<br />
erstreckt sich über einen Teil des Programmablaufs.<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
53
Der Prozeduraufrufbaum beschreibt die Prozeduraufrufstruktur<br />
in einem Programm- oder Prozedurablauf.<br />
Seine Baumknoten sind mit Prozedurinkarnationen<br />
markiert, so dass jede Inkarnation PI genau die<br />
Inkarnationen als Kinder hat, die von PI aus erzeugt<br />
wurden und zwar in der Reihenfolge des Ablaufs.<br />
Bemerkung:<br />
Die Lebensdauern von Inkarnationen der gleichen<br />
Prozedur können sich überlappen. Deshalb muss<br />
es ggf. mehrere Kopien/Instanzen der gleichen<br />
lokalen Variablen geben.<br />
Beispiel:<br />
// Lebensdauer<br />
void p( int i ){ //b(p)<br />
int a;<br />
a = i; //1(p)<br />
if( a == 2 ) { //2(p)<br />
p( a-1 ); //3(p)<br />
}<br />
println(i); //4(p)<br />
} //e(p)<br />
int c;<br />
void main(...){ //b(m)<br />
p(2); //1(m)<br />
} //e(m)<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
54
<strong>4.</strong>2.4 Variablen in Programm und Speicher<br />
Jede Variablendeklaration definiert eine<br />
Programmvariable. Wir unterscheiden:<br />
- globale Variablen<br />
- (prozedur-)lokale Variablen<br />
- Klassenvariablen, Attribute, usw.<br />
Jeder Programmvariablen entsprechen zur<br />
Ausführungszeit/Laufzeit des Programms eine oder<br />
mehrere Speichervariablen:<br />
- Jeder globalen Variablen ist genau eine Speichervariable<br />
zugeordnet.<br />
- Ist v eine lokale Variable zur Prozedur p, dann gibt<br />
es zu jeder Inkarnation von p eine Speichervariable<br />
für v.<br />
- (andere Variablentypen behandeln wir später).<br />
Begriffsklärung: (statisch/dynamisch)<br />
Statisch bezeichnet man in der Programmiertechnik<br />
alle Aspekte, die sich auf das Programm beziehen und<br />
die man aus ihm ersehen kann, ohne es auszuführen.<br />
Statische Aspekte sind unabhängig von Eingaben.<br />
Dynamisch bezeichnet man alle Aspekte, die sich auf<br />
die Ausführungszeit beziehen.<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
55
Beispiele: (statisch/dynamisch)<br />
Statisch:<br />
- Programmvariablen<br />
- Prozedurdeklarationen<br />
- Anweisung<br />
- Aspekte der Übersetzung<br />
Dynamisch:<br />
- Speichervariablen<br />
- Prozedurinkarnationen<br />
- Ablauf, Ausführung<br />
- Aufrufbäume<br />
- Lebensdauer (von Prozedurinkarnationen, usw.)<br />
Definition: (Lebensdauer von Variablen)<br />
Die Lebensdauer einer Speichervariable ist der<br />
Teil des Ablaufs, in dem sie für die Ausführung<br />
bereit steht.<br />
Die Lebensdauer globaler Variablen erstreckt sich<br />
über den gesamten Ablauf des Programms.<br />
Variablen zu lokalen Deklarationen leben solange<br />
wie die zugehörige Prozedurinkarnation bzw. über<br />
die Dauer der Ausführung ihres Blocks.<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
56
Beispiel: (Lebensdauer von Variablen)<br />
Wir betrachten den Ablauf des obigen Programms<br />
mit den Markierungen und die Variablenlebensdauer:<br />
b(m.1)<br />
1(m.1)<br />
b(p.1)<br />
1(p.1)<br />
2(p.1)<br />
3(p.1)<br />
b(p.2)<br />
1(p.2)<br />
2(p.2)<br />
4(p.2)<br />
e(p.2)<br />
4(p.1)<br />
e(p.1)<br />
e(m.1)<br />
Bemerkung:<br />
c a.1 a.2<br />
Es gibt auch Variablen, deren Lebensdauer direkt<br />
über Anweisungen gesteuert werden kann.<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
57
Beispiel: (Lebensdauer von Variablen, die 2.)<br />
Wir betrachten den Ablauf des folgenden<br />
C-Programmfragments:<br />
int k = 0;<br />
while( k
<strong>4.</strong>2.5 Felder<br />
Ein Feld (engl. Array) ist ein n-Tupel von Variablen<br />
des gleichen Typs T (vgl. Folie 159).<br />
Die genaue Syntax und Semantik von Feldern ist<br />
von Sprache zu Sprache verschieden.<br />
In Java sind Felder Objekte. Objekte veranschaulichen<br />
wir durch einen rechteckigen Kasten:<br />
- die Titelzeile enthält den Typ;<br />
- der Hauptteil enthält die Variablen/Komponenten<br />
mit ihren Namen bzw. Indizes.<br />
Variablen für Felder speichern Referenzen/Zeiger<br />
auf das Feldobjekt.<br />
Beispiel: (Veranschaulichung eines Felds)<br />
int[] a = new int[3] ;<br />
int[] b = a<br />
a:<br />
b:<br />
int[]<br />
length: 3<br />
0 : -7<br />
1 : 46<br />
2 : 0<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
59
Feldtypen, Operationen auf Feldern:<br />
Mit T[] wird in Java der Typ von Feldern mit<br />
Komponenten vom Typ T bezeichnet.<br />
Deklaration einer Variablen<br />
T[] a;<br />
stellt nur den Speicherplatz für die Variable bereit;<br />
sie erzeugt kein Feld!<br />
Ein neues Feld mit n Komponenten vom Typ T<br />
wird von dem Ausdruck new T[n] erzeugt.<br />
Der Ausdruck liefert eine Referenz auf des Feld<br />
als Ergebnis. Initialisierung:<br />
- Die Variable length ist mit n initialisiert.<br />
- Alle anderen Komponenten sind mit dem<br />
Initialwert des Typs T initialisiert. (Für alle<br />
Zahltypen ist der Initialwert 0, für boolean ist<br />
er false.)<br />
Die Lebensdauer eines Felds<br />
- beginnt mit der Erzeugung und<br />
- endet mit der Terminierung des Programms.<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
60
Beispiel: (Lebensdauer von Feldern)<br />
int[] erzeugeFeld( int n ) {<br />
int size = n < 0 ? 0 : n;<br />
return new int[size];<br />
}<br />
void eineProz() {<br />
int[] a = erzeugeFeld(78);<br />
...<br />
}<br />
Sei im Folgenden exp ein Ausdruck von einem ganzzahligen<br />
Typ (byte, short, int, long), der sich zu k auswertet.<br />
Ausdrücke zum Lesen und Schreiben eines Felds a:<br />
- a.length liefert die Anzahl der Feldkomponenten<br />
- a[exp] erzeugt eine IndexOutOfBounds-Ausnahme<br />
falls k < 0 oder a.length ≤ k; andernfalls:<br />
R-Wert: der in der k-ten Feldkomponente<br />
gespeicherte Wert;<br />
L-Wert: die k-te Feldkomponente, d.h. z.B. weist<br />
a[exp] = 7 ;<br />
der k-ten Feldkompente den Wert 7 zu.<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
61
Beispiel: (<strong>Programmieren</strong> mit Feldern)<br />
Das folgende Programm liest zwei Vektoren ein und<br />
gibt die Summe aus:<br />
void main(String[] args) {<br />
}<br />
int i;<br />
int[] a = new int[3]; // 3-elementiger Vektor<br />
int[] b = new int[3];<br />
for( i=0; i
Beispiele: (2-dimensionale Felder &<br />
Initialisierung von Feldern)<br />
String[] argf;<br />
float [] vector_3d = new float[3];<br />
int [][] matrix_3x3 = new int[3][3];<br />
Folgendes Beispiel zeigt weitere Erzeugungs- und<br />
Initialisierungsmöglichkeiten bei Feldern:<br />
char[] vor = new char[3];<br />
vor[0] = 'v';<br />
vor[1] = 'o';<br />
vor[2] = 'r';<br />
char[] Fliegen = {'F','l','i','e','g','e','n'};<br />
char[][] satz = { Fliegen,<br />
{'f','l','i','e','g','e','n'},<br />
vor,<br />
Fliegen };<br />
int i,j;<br />
String s = "";<br />
for( i = 0; i < satz.length; i++ ) {<br />
for( j = 0; j < satz[i].length; j++ ) {<br />
s = s + satz[i][j];<br />
}<br />
if( i+1 < satz.length ) s = s + " ";<br />
}<br />
println( s + ".");<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
63
Nach der Zuweisung an satz entsteht folgendes<br />
Geflecht:<br />
satz:<br />
Fliegen:<br />
vor:<br />
char[]<br />
length: 3<br />
0 : ‘v‘<br />
1 : ‘o‘<br />
2 : ‘r‘<br />
char[][]<br />
length: 4<br />
0 :<br />
1 :<br />
2 :<br />
3 :<br />
char[]<br />
length: 7<br />
0 : ‘F‘<br />
1 : ‘l‘<br />
2 : ‘i‘<br />
3 : ‘e‘<br />
5 : ‘e‘<br />
6 : ‘n‘<br />
char[]<br />
length: 7<br />
0 : ‘f‘<br />
1 : ‘l‘<br />
2 : ‘i‘<br />
3 : ‘e‘<br />
4 : ‘g‘ 4 : ‘g‘<br />
5 : ‘e‘<br />
6 : ‘n‘<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
64
<strong>4.</strong>2.6 Benutzerdefinierte Typen<br />
Die meisten prozeduralen Sprachen unterstützen<br />
benutzerdefinierte Typen, insbesondere<br />
Verbundtypen.<br />
Begriffsklärung: (Verbunde)<br />
Ein (Variablen-)verbund (engl. Record) ist ein<br />
n-Tupel von Variablen nicht notwendig des gleichen<br />
Typs. Die einzelnen Variablen nennt man die<br />
Komponenten des Verbunds. Die Komponenten<br />
werden über deklarierte Namen angesprochen.<br />
Die genaue Syntax und Semantik von Verbunden ist<br />
von Sprache zu Sprache verschieden.<br />
In Java lassen sich Verbunde nur als vereinfachte<br />
Objekte deklarieren und erzeugen. Als Ergebnis<br />
der Erzeugung erhält man dabei eine Referenz auf<br />
den Verbund. Die Referenz kann man in Variablen<br />
vom Verbundtyp speichern. Verbunde mit Referenzen<br />
nennen wir referenzierte Verbunde.<br />
Da wir im Folgenden nur referenzierte Verbunde<br />
betrachten, sprechen wir einfach von Verbunden.<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
65
Beispiel: (Veranschaulichung eines<br />
referenzierten Verbunds)<br />
p:<br />
Sprachmittel für (referenzierte) Verbunde:<br />
- Deklaration eines Verbundtyps<br />
- Deklaration von Variablen für (Referenzen auf)<br />
Verbunde<br />
- Erzeugung von Verbunden<br />
Paar<br />
q: ersteKomp: 3<br />
zweiteKomp : -7<br />
- Lesen der Verbundkomponenten<br />
- Zuweisen an Verbundkomponenten<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
66
Syntax anhand von Beispielen:<br />
Deklaration eines<br />
Verbundtyps Paar:<br />
Deklaration der Variablen<br />
p, q für Referenzen auf<br />
Verbunde vom Typ Paar:<br />
Erzeugung eines Verbunds<br />
vom Typ Paar und Zuweisung<br />
der Referenz an p:<br />
Weitergabe einer Referenz,<br />
hier von p an q:<br />
Lesen einer Komponente:<br />
class Paar {<br />
int ersteKomp;<br />
int zweiteKomp;<br />
}<br />
Paar p;<br />
Paar q;<br />
p = new Paar();<br />
q = p;<br />
int a;<br />
a = p.ersteKomp;<br />
Schreiben einer Komponente: int a;<br />
p.ersteKomp = a+1;<br />
Der Initialwert für Verbundtypen<br />
ist null:<br />
class C {<br />
Paar pk;<br />
}<br />
new C().pk == null<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
67
Verbunde zur Realisierung von Tupeln:<br />
Deklaration eines Verbundtyps Student mit<br />
Name und Matrikelnummer.<br />
class Student {<br />
String name;<br />
int matNr;<br />
}<br />
Deklaration einer Prozedur zum Drucken der<br />
Daten von Studenten:<br />
studentDrucken( Student s ) {<br />
println("Name: " + s.name );<br />
println("Matriklenummer: " + s.matNr );<br />
}<br />
Erzeugen eines Verbunds vom Typ Student,<br />
Zuweisung an eine Variable, Initialisieren der<br />
Komponenten und Ausdrucken:<br />
Student sv = new Student();<br />
sv.name = "Max Planck";<br />
sv.matNr = 12390573;<br />
studentDrucken( sv );<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
68
Rekursive referenzierte Verbunde:<br />
Verbunddeklarationen können rekursiv sein, z.B.:<br />
class IntList {<br />
int head;<br />
IntList tail;<br />
}<br />
Mit rekursiven Verbunden lassen sich Listen,<br />
Bäume und allgemeine Graphen realisieren.<br />
Beispiel: (Einfachverkettete Listen)<br />
Bei einfachverketteten Listen wird für jedes<br />
Listenelement ein Verbund mit zwei Komponenten<br />
angelegt:<br />
- zum Speichern des Elements<br />
- zum Speichern der Referenz auf den Rest der Liste.<br />
Die Liste [6,-3,84] erhält also folgende Repräsentation:<br />
IntList<br />
head: 6<br />
tail:<br />
IntList IntList<br />
head: -3<br />
tail:<br />
head: 84<br />
tail:<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
69
Beispielprogramm:<br />
/* Prozedur sortiert prüft, ob die<br />
übergebenen List l sortiert ist.<br />
Vorbedingung: l darf keinen Zyklus enthalten<br />
*/<br />
boolean sortiert( IntList l ){<br />
if( l == null || l.tail == null ) {<br />
return true;<br />
} else if( l.head
Begriffsklärung: (Geflecht)<br />
Eine Menge von Verbunden, die sich gegenseitig<br />
referenzieren, nennen wir ein Geflecht.<br />
Insbesondere können Geflechte Zyklen enthalten.<br />
Beispiel: (Geflecht)<br />
: KA<br />
a1:<br />
a2: 8<br />
: KA<br />
a1:<br />
a2: 97<br />
: KA<br />
a1:<br />
a2: -6<br />
Notation:<br />
: KB<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
71<br />
b:<br />
b:<br />
b:<br />
: KB<br />
: KB<br />
Graphisch stellen wir null durch einen ausgefüllten<br />
kleinen Kreis dar.
Verändern von Geflechten:<br />
Anders als die Datentypen in reinen funktionalen<br />
Programmierung lassen sich Geflechte lokal<br />
modifizieren.<br />
Veränderung an einem Verbund x wirken sich auf<br />
alle Verbunde und Variablen aus, die x direkt oder<br />
indirekt referenzieren.<br />
Als Beispiel für Veränderungen betrachten wir eine<br />
Implementierung binärer Suchbäume:<br />
/* Verbundtyp für Binärbäume mit<br />
Markierung vom Typ int<br />
*/<br />
class BinTree {<br />
int elem;<br />
BinTree left, right;<br />
}<br />
Den leeren Binärbaum repräsentieren wir durch die<br />
Konstante null. Wir betrachten die folgenden<br />
Prozeduren:<br />
- Prüfen, ob ein Element in einem Baum enthalten ist.<br />
- Ausdrucken aller Markierungen eines Baumes.<br />
- Konstruktorprozedur mit Initialisierung<br />
- Sortiertes Einfügen<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
72
* Prozedur contains prüft, ob Markierung e<br />
in b enthalten ist.<br />
Vorbedingung: b ist binärer Suchbaum<br />
*/<br />
boolean contains( BinTree b, int e ) {<br />
if( b == null ) {<br />
return false;<br />
} else if( e < b.elem ){<br />
return contains(b.left,e);<br />
} else if( b.elem < e ){<br />
return contains(b.right,e);<br />
} else {<br />
return true;<br />
}<br />
}<br />
/* Prozedur printTree durchläuft den Baum in<br />
Inorder-Reihenfolge und druckt die Markierungen<br />
aus, jede in eine Zeile.<br />
*/<br />
void printTree( BinTree b ) {<br />
if( b != null ) {<br />
if( b.left != null ) printTree(b.left);<br />
println(b.elem);<br />
if( b.right != null ) printTree(b.right);<br />
}<br />
}<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
73
* Konstruktorprozedur<br />
*/<br />
BinTree mkBinTree( int e ) {<br />
BinTree newbt = new BinTree();<br />
newbt.elem = e;<br />
return newbt;<br />
}<br />
/* Sortiertes Einfügen. Erhält Suchbaumeigenschaft<br />
*/<br />
BinTree sorted_insert( BinTree b, int e ) {<br />
if( b == null ) {<br />
return mkBinTree(e);<br />
} else {<br />
modifying_sorted_ins(b,e);<br />
return b;<br />
} }<br />
void modifying_sorted_ins( BinTree b, int e){<br />
if( e < b.elem ) {<br />
if( b.left == null ) {<br />
b.left = mkBinTree(e);<br />
} else {<br />
modifying_sorted_ins(b.left,e);<br />
}<br />
} else if( b.elem < e ) {<br />
if( b.right == null ) {<br />
b.right = mkBinTree(e);<br />
} else {<br />
modifying_sorted_ins(b.right,e);<br />
}<br />
} }<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
74
Hauptprozedur zum Testen:<br />
void main( String[] argf ){<br />
}<br />
Bemerkung:<br />
BinTree bt = mkBinTree(12);<br />
sorted_insert(bt,3);<br />
sorted_insert(bt,12);<br />
sorted_insert(bt,233);<br />
sorted_insert(bt,11);<br />
sorted_insert(bt,12343);<br />
sorted_insert(bt,-2343);<br />
sorted_insert(bt,233);<br />
printTree(bt);<br />
• Mit referenzierten Verbundstypen lassen sich<br />
nicht nur Listen und Bäume in unterschiedlicher<br />
Form realisieren, sondern auch allgemeine<br />
Graphen.<br />
• Anspruchsvollere Algorithmen werden in<br />
Abschnitt <strong>4.</strong>3 behandelt.<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
75
<strong>4.</strong>2.7 Sichtbarkeit von Bindungen<br />
Deklarationen binden Bezeichner an Programmelemente<br />
(z.B. an Variablen oder an Prozeduren).<br />
Die Sichtbarkeitsregeln bestimmen, welche Bindungen<br />
wo zulässig und benutzbar sind.<br />
Beispiel: (Sichtbarkeit von Bindungen)<br />
Folgender Programmtext ist kein Java-Programm,<br />
da nur eine Variablenbindung für den Bezeichner m<br />
erlaubt ist:<br />
public class Sichtbarkeiten {<br />
}<br />
static int m;<br />
static void p( int p ) {<br />
int m;<br />
int a = 7;<br />
{<br />
int m = 0; // unzulässig<br />
if( p > 0 ) {<br />
p(p-1);<br />
}<br />
}<br />
}<br />
public static void main(String[] a ){<br />
m = 1;<br />
p(m);<br />
}<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
76
Begriffsklärung: (zur Sichtbarkeit)<br />
Programme sind in Deklarationsbereiche (engl.<br />
scopes) eingeteilt. In der betrachteten Java-Teilsprache<br />
gibt es die folgenden Deklarationsbereiche:<br />
- das gesamte Programm<br />
- jede Prozedur/Methode<br />
- jeder Anweisungsblock<br />
Die Deklarationsbereiche sind geschachtelt<br />
(engl. nested). Ein Deklarationsbereich darf auf<br />
seiner äußersten Schachtelungsebene für jeden<br />
Bezeichner nur eine Deklaration enthalten.<br />
Zu jeder Deklaration gibt es einen Gültigkeitsbereich.<br />
In der betrachteten Java-Teilsprache gilt:<br />
Der Gültigkeitsbereich einer Deklaration erstreckt<br />
sich bei<br />
- Prozeduren und globalen Variablen über den<br />
gesamten direkt umfassenden Deklarationsbereich;<br />
- lokalen Variablen von der Deklarationsstelle bis zum<br />
Ende des direkt umfassenden Deklarationsbereichs.<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
77
Grundsätzlich gilt:<br />
Eine Bindung B, die in einem Deklarationsbereich D<br />
deklariert wurde, verschattet (engl. to shadow, to hide)<br />
andere Bindungen mit gleichem Bezeichner, die<br />
außerhalb von D vereinbart wurden, in dem<br />
Gültigkeitsbereich von B.<br />
Eine Bindung ist an den Stellen ihres Gültigkeitsbereichs<br />
sichtbar, an denen sie nicht verschattet ist.<br />
Den Bereich, in dem eine Bindung B sichtbar ist,<br />
nennt man den Sichtbarkeitsbereich von B.<br />
Bemerkung:<br />
• Statt von Deklarationsbereichen spricht man<br />
teilweise von Blöcken und dementsprechend von<br />
der Blockstruktur eines Programms.<br />
• Die Begriffe Sichtbarkeits-, Gültigkeitsbereich und<br />
Lebensdauer werden leider häufig auch anders<br />
verwendet.<br />
• Die erläuterten Bereiche sind Bereiche des Programmtexts.<br />
Dementsprechend beschreiben alle oben<br />
eingeführten Begriffe statische Aspekte.<br />
• Sichtbarkeitsregeln sind im Allg. sprachspezifisch.<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
78
Beispiel: (Deklarationsbereiche)<br />
1<br />
1.1<br />
1.2<br />
1.1.1<br />
1.2.1<br />
int m;<br />
void p<br />
( int p )<br />
{<br />
int m;<br />
int a = 7;<br />
}<br />
{<br />
}<br />
int m = 0; //unzulässig<br />
if( p > 0 )<br />
{<br />
p(p-1);<br />
}<br />
void main<br />
( String[] a )<br />
{<br />
m = 1;<br />
p(m);<br />
}<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
79
Beispiel: (Sichtbarkeitsanalyse)<br />
1<br />
1.1<br />
1.2<br />
1.1.1<br />
1.2.1<br />
// Sichtbarkeiten2<br />
void p<br />
( int p )<br />
{<br />
}<br />
int a = 7;<br />
{<br />
}<br />
{<br />
}<br />
int m = 1;<br />
println("m:"+ m);<br />
println("m:"+ m);<br />
int m = 2;<br />
println("m:"+ m);<br />
int m = 0;<br />
void main<br />
( String[] p )<br />
{<br />
}<br />
int m = 3;<br />
p(m);<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
80
Beispiel: (Gültigkeitsbereiche)<br />
Der Gültigkeitsbereich der globalen Variablen m<br />
erstreckt sich über das ganze Programm.<br />
Der Gültigkeitsbereich der lokalen Variablen m<br />
jeweils von der Deklarationstelle bis zum Ende des<br />
direkt umfassenden Anweisungsblocks:<br />
// Sichtbarkeiten2<br />
void p<br />
( int p )<br />
{<br />
}<br />
int a = 7;<br />
{<br />
}<br />
{<br />
}<br />
int m = 1;<br />
println("m:"+ m);<br />
println("m:"+ m);<br />
int m = 2;<br />
println("m:"+ m);<br />
int m = 0;<br />
...<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
81
Beispiel: (Sichtbarkeitsbereiche)<br />
Eine Bindung B, die in einem Deklarationsbereich D<br />
deklariert wurde, verschattet andere Bindungen mit<br />
gleichem Bezeichner, die außerhalb von D vereinbart<br />
wurden, im Gültigkeitsbereich von B.<br />
Also wird das globale m von den lokalen verschattet:<br />
// Sichtbarkeiten2<br />
void p<br />
( int p )<br />
{<br />
}<br />
int a = 7;<br />
{<br />
}<br />
{<br />
}<br />
int m = 1;<br />
println("m:"+ m);<br />
println("m:"+ m);<br />
int m = 2;<br />
println("m:"+ m);<br />
int m = 0;<br />
...<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
82
Bemerkung:<br />
• In realistischen Programmiersprachen können die<br />
Sichtbarkeitsregeln insgesamt recht komplex sein.<br />
• Die Kenntnis der Sichtbarkeitsregeln ist wichtig,<br />
um Fehlermeldungen vom Übersetzer zu verstehen<br />
und um Namensprobleme beim Importieren<br />
von Modulen geeignet behandeln zu können.<br />
• Sichbarkeitsregeln bieten eine erste Einführung in<br />
die Behandlung von Namensräumen.<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
83
<strong>4.</strong>2.8 Iteration und Rekursion<br />
Rekursive Prozeduren lassen sich in vielen Fällen<br />
in iterative Programme transformieren, d.h. in<br />
Programme, die keine Rekursion, sondern nur<br />
Schleifen enthalten.<br />
Die Lösung mit rekursiven Prozeduren kann eleganter<br />
und einfacher sein. Demgegenüber ist eine iterative<br />
Lösung meist effizienter.<br />
Die Transformation rekursiver Prozeduren in iterative<br />
Programme ist ein klassisches Beispiel für<br />
optimierende Programmtransformationen.<br />
Wir verzichten auf eine allgemeine Behandlung<br />
und betrachten nur ein Beispiel (vgl. Goos, Band 3,<br />
S. 152 ff)<br />
Beispiel:<br />
Berechne das Maximum einer Liste ganzer Zahlen.<br />
Für die leere Liste liefere 0 als Ergebnis.<br />
Wir gehen davon aus, dass es einen Typ IntList mit<br />
Funktionsprozeduren head, tail und isempty gibt sowie:<br />
int max( int m, int n ) {<br />
return (m > n) ? m : n;<br />
}<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
84
1. Rekursive Ausgangsfassung:<br />
int maxl( IntList il ) {<br />
}<br />
if( isempty(il) ) {<br />
return 0;<br />
} else if( isempty(tail(il)) ) {<br />
return head(il);<br />
} else { // (Laenge von il) >= 2<br />
}<br />
return max(head(il),maxl(tail(il)));<br />
void main( String[] args ) {<br />
}<br />
IntList l = ... ;<br />
println("maxl: "+ maxl(l) );<br />
Die Funktionsprozedur maxl ist linear rekursiv, aber<br />
nicht repetitiv. Führe zur Einbettung einen zusätzlichen<br />
Parameter ein, der das Maximum des gesehenen<br />
Präfixes mitführt (vgl. Folien 3.39 ff).<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
85
2. Repetitive Fassung durch Einbettung:<br />
int maxlrep( int aktmax, IntList il ) {<br />
}<br />
if( isempty(il) ) {<br />
return aktmax;<br />
} else {<br />
}<br />
return maxlrep( max(aktmax,head(il)),<br />
int maxl( IntList il ) {<br />
}<br />
if( isempty(il) ) {<br />
return 0;<br />
} else {<br />
}<br />
tail(il) );<br />
return maxlrep( head(il), tail(il) );<br />
void main( String[] arges ) {<br />
}<br />
IntList l = ... ;<br />
println("maxl: "+ maxl(l) );<br />
Die Funktionsprozedur maxl ist nicht mehr rekursiv<br />
und kann expandiert werden.<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
86
3. Repetitive Fassung nach Expansion von maxl:<br />
int maxlrep( int aktmax, IntList il ) {<br />
}<br />
if( isempty(il) ) {<br />
return aktmax;<br />
} else {<br />
}<br />
return maxlrep( max(aktmax,head(il)),<br />
tail(il) );<br />
void main( String[] args ) {<br />
}<br />
IntList l = ... ;<br />
int mx;<br />
if( isempty(l) ) {<br />
mx = 0;<br />
} else {<br />
}<br />
mx = maxlrep( head(l), tail(l) );<br />
println("maxl: " + mx );<br />
Transformiere die Funktionsprozedur maxlrep in eine<br />
Prozedur, die ihr Ergebnis in globaler Variable abliefert.<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
87
<strong>4.</strong> Repetitive Fassung nach Einführung einer<br />
Ergebnisvariablen:<br />
int res;<br />
void maxlrep( int aktmax, IntList il ) {<br />
}<br />
if( isempty(il) ) {<br />
res = aktmax;<br />
} else {<br />
}<br />
maxlrep( max(aktmax,head(il)), tail(il));<br />
void main( String[] args ) {<br />
}<br />
IntList l = ... ;<br />
int mx;<br />
if( isempty(l) ) {<br />
mx = 0;<br />
} else {<br />
}<br />
maxlrep( head(l), tail(l) );<br />
mx = res;<br />
println("maxl: " + mx );<br />
Nun folgt der zentrale Schritt der Transformation der<br />
repetitiven Fassung in eine iterative Fassung:<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
88
5. Iterative Fassung mit Prozeduraufruf:<br />
int res;<br />
void maxlrep( int aktmaxpar, IntList ilpar ){<br />
}<br />
int aktmax = aktmaxpar;<br />
IntList il = ilpar;<br />
while( !isempty(il) ) {<br />
}<br />
aktmax = max(aktmax,head(il));<br />
il = tail(il);<br />
res = aktmax;<br />
void main( String[] args ) {<br />
}<br />
IntList l = ... ;<br />
int mx;<br />
if( isempty(l) ) {<br />
mx = 0;<br />
} else {<br />
}<br />
maxlrep( head(l), tail(l) );<br />
mx = res;<br />
println("maxl: " + mx );<br />
Expansion von der Prozedur maxlrep:<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
89
6. Iterative Fassung:<br />
int res;<br />
void main( String[] args ) {<br />
}<br />
IntList l = ... ;<br />
int mx;<br />
if( isempty(l) ) {<br />
mx = 0;<br />
} else {<br />
}<br />
int aktmax = head(l);<br />
IntList il = tail(l);<br />
while( !isempty(il) ) {<br />
}<br />
aktmax = max(aktmax,head(il));<br />
il = tail(il);<br />
res = aktmax;<br />
mx = res;<br />
println("maxl: " + mx );<br />
Vereinfachung durch Elimination unnötiger Variablen.<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
90
7. Vereinfachte iterative Fassung:<br />
void main( String[] args ) {<br />
}<br />
IntList l = ... ;<br />
int mx;<br />
if( isempty(l) ) {<br />
mx = 0;<br />
} else {<br />
}<br />
mx = head(l);<br />
IntList il = tail(l);<br />
while( !isempty(il) ) {<br />
}<br />
mx = max(mx,head(il));<br />
il = tail(il);<br />
println("maxl: " + mx );<br />
Bemerkung:<br />
• Formal kann man Programmtransformation mit<br />
Regelsystemen beschreiben.<br />
• Programmtransformationen sind nicht nur wichtig zur<br />
Effizienzsteigerung, sondern auch um Programme<br />
- verständlicher zu machen (Redesign);<br />
- an neue Anforderungen anzupassen.<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
91
Beispiel: (Transformationsregel)<br />
Transformationsregel: repetitiv � iterativ<br />
Seien p ein Prozedurname, T1,...,Tn Typbezeichner,<br />
A1,..,An und B seiteneffektfreie-Ausdrücke,<br />
C und D Anweisungen, die keinen Aufruf<br />
von p enthalten:<br />
void p( T1 x1, ... , Tn xn) {<br />
if( B ) {<br />
C<br />
} else {<br />
D<br />
p( A1(x1,...xn),..., An(x1,...,xn) );<br />
}<br />
}<br />
void p( T1 y1, ... , Tn yn) {<br />
T1 x1 = y1;<br />
...<br />
Tn xn = yn;<br />
while( !B ) {<br />
D<br />
x1 = A1(x1,...,xn);<br />
...<br />
xn = An(x1,...,xn);<br />
}<br />
C<br />
}<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
92
Für obiges Transformationsbeispiel (Folie <strong>4.</strong>88)<br />
ergibt sich mit der Regel:<br />
p ≡ maxlrep ,<br />
T1 ≡ int, x1 ≡ aktmax,<br />
T2 ≡ IntList , x2 ≡ il,<br />
B ≡ isempty(il)<br />
C ≡ res = aktmax;<br />
D ≡ // leere Anweisung<br />
A1 ≡ max(aktmax,head(il))<br />
A2 ≡ tail(il)<br />
A1, A2 und B sind seiteneffektfrei;<br />
C und D enthalten keinen Aufruf von maxlrep;<br />
die Regel ist also auf maxlrep anwendbar und<br />
liefert:<br />
void maxlrep( int y1, IntList y2) {<br />
int aktmax = y1;<br />
IntList il = y2;<br />
while( !isempty(il) ) {<br />
aktmax = max(aktmax,head(il));<br />
il = tail(il);<br />
}<br />
res = aktmax;<br />
}<br />
Durch konsistente Parameterumbenennung erhält<br />
man die Prozedur von Folie <strong>4.</strong>89.<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
93
Bemerkung:<br />
• Im Prinzip lassen sich Programme mit<br />
Transformationsregeln genauso umformen wie<br />
mathematische Ausdrücke.<br />
• Die Transformationsregeln für realistische<br />
Programmiersprachen sind sehr komplex.<br />
Dies beginnt bereits bei einer präzisen Behandlung<br />
von Namen (vgl. Sichtbarkeitsregeln).<br />
<strong>4.</strong>2.9 Weitere prozedurale Sprachkonzepte<br />
Wir haben hier nur den Sprachkern prozeduraler<br />
Sprachen betrachtet. Weitere Sprachkonzepte:<br />
• Verbünde, variante Verbünde<br />
• Zeiger ggf. mit Zeigerarithmetik<br />
• Prozedurparameter<br />
• Generische/parametrische Typen<br />
• Modul-/Paketkonzepte<br />
• Kapselung/Schnittstellen<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
94
<strong>4.</strong>3 Algorithmen in prozeduraler<br />
Formulierung<br />
Dieser Abschnitt behandelt Algorithmen in prozeduraler<br />
Formulierung und deren Analyse bzgl.<br />
- Speicherbedarf<br />
- Laufzeit<br />
Er liefert einen Einblick in die Speicherverwaltung<br />
bei prozeduralen Programmen und gibt eine<br />
Ausblick auf weitere algorithmische Problemstellungen<br />
und Methoden.<br />
Vorgehen:<br />
- Einführung in die Algorithmenanalyse<br />
- Speicherverwaltung<br />
- Laufzeitverhalten<br />
- Prozedurale Algorithmen und deren Analyse<br />
- Klassifizierung und Entwicklung von Algorithmen<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
95
<strong>4.</strong>3.1 Einführung in die Algorithmenanalyse<br />
Zwei zentrale Größen zur Beurteilung von Effizienz:<br />
- Speicher(platz)bedarf (in kByte od. Speicherzellen)<br />
- Ausführungs-/Laufzeit (in Sekunden od. Anzahl<br />
ausgeführter Operationen)<br />
Bei einem installierten Programm kann man<br />
diese Größen zu gegebenen Eingaben messen<br />
bzw. berechnen.<br />
Wir betrachten im Folgenden nur den Fall, dass alle<br />
Eingaben am Anfang der Ausführung erfolgen und<br />
alle Ausgaben am Ende.<br />
Begriffsklärung: (Speicherbedarf/Laufzeit)<br />
Sei P ein installiertes Programm und M die Menge der<br />
zulässigen Eingaben von P.<br />
Der (max.) Speicherbedarf von P ist eine Funktion<br />
sb : M � kByte<br />
Man spricht von der Raumkomplexität von P.<br />
Die Laufzeit von P ist eine Funktion:<br />
lz : M � Sekunden<br />
Man spricht von der Zeitkomplexität von P.<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
96
Bemerkung:<br />
In der Praxis ist die Bestimmung von Speicherbedarf<br />
und Laufzeit im Allg. nicht einfach:<br />
1. Messen:<br />
- kann nur endlich viele Funktionswerte liefern;<br />
- wird ggf. durch Systemgegebenheiten beeinflusst.<br />
2. Berechnen ausgehend vom Programm:<br />
- setzt Kenntnisse der Übersetzung und des<br />
Systems voraus.<br />
Beispiel: (Messen der Laufzeit)<br />
fun primfaktoren (n:int) =<br />
if n
Ausgewählte Messergebnisse:<br />
Eingabe Laufzeit in Sek.<br />
12345678 < 1<br />
10000000 < 1<br />
123456789 < 1<br />
100000000 < 1<br />
1234567890 < 1<br />
1000000000 < 1<br />
12345678901 ~ 2<br />
10000000000 < 1<br />
123456789012 > 20000<br />
100000000000 < 1<br />
1234567890123 ~ 15<br />
1000000000000 < 1<br />
12345678901234 > 20000<br />
10000000000000000000000 < 1<br />
Gemessen<br />
- unter PolyML<br />
- für Betriebssystem XX<br />
- auf einem Rechner der Bauart YY.<br />
Beobachtung:<br />
Laufzeit hängt im Allg. in komplexer Weise von<br />
der Eingabe ab.<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
98
Begriffsklärung: (Kriterien zur Effizienz)<br />
Die Gesamteffizienz eines installierten Programms<br />
hängt ab von<br />
- der Speicherbedarfsfunktion sb,<br />
- der Laufzeitfunktion lz und<br />
- der Häufigkeit, mit der bestimmte Eingaben auftreten.<br />
Bei häufig auftretenden Eingabewerten ist effizientes<br />
Verhalten wichtiger als bei seltenen Eingabewerten.<br />
Bemerkung:<br />
• Ein präziser Umgang mit dem obigen Effizienzbegriff<br />
ist in der Praxis schwierig:<br />
- sb und lz sind kaum zu bestimmen;<br />
- die Häufigkeitsverteilung der Eingaben ist oft nicht<br />
genau bekannt;<br />
- meist möchte man die Effizienz eines Programms<br />
unabhängig von seiner Installation betrachten.<br />
• Die Kriterien bilden aber die Grundlage für:<br />
- den Effizienzvergleich von Programmen<br />
- die informelle Beurteilung von Effizienz<br />
- abstraktere Effizienzbegriffe<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
99
Abstraktere Effizienzbegriffe:<br />
Statt ein installiertes Programm zu betrachten,<br />
sieht man üblicherweise von Details ab und<br />
betrachtet Effizienz nur näherungsweise.<br />
Vereinfachungen:<br />
1. Betrachte nicht die Eingabewerte selbst, sondern<br />
nur ihre Größe (z.B. Länge von Listen, Stellenanzahl<br />
bei Zahlen).<br />
2. Vernachlässige die Häufigkeitsverteilung der Daten.<br />
3. Vernachlässige konstanten Aufwand, also<br />
Aufwand, der unabhängig von den Eingabedaten<br />
entsteht.<br />
<strong>4.</strong> Betrachte nur das Wachstum von sb und lz und<br />
vernachlässige konstante Faktoren (Abstraktion<br />
von der Leistung eines Rechners und den<br />
Implementierungseigenschaften einer Programmiersprache).<br />
5. Betrachte nur obere und untere Schranken für<br />
sb und lz.<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
100
Beispiel: (Analyse der Laufzeit)<br />
Wir analysieren eine Implementierung des Algorithmus<br />
„Sortieren durch Auswahl“ (engl. selection sort) .<br />
Eingabe: Feld f von ganzen Zahlen.<br />
Aufgabe: Sortiere das Feld f aufsteigend.<br />
Algorithmische Idee:<br />
- Bestimme eine Komponente mit Index ixmin von f,<br />
die ein minimales Element von f[1] ... f[f.length]<br />
enthält.<br />
- Vertausche f[ixmin] und f[1].<br />
- Sortiere dann den Bereich f[2] ... f[f.length] analog.<br />
Mögliche Hauptprozedur:<br />
void main( String[] arg ) {<br />
int[] feld = new int[arg.length];<br />
for( int i = 0; i
Rekursive Fassung des Sortierens durch Auswahl:<br />
void sortieren(/*nonnull*/ int[] f) {<br />
bereichsort(f,0,f.length-1);<br />
}<br />
void bereichsort( int[] f, int ug, int og) {<br />
if (ug >= og) return;<br />
int ixmin = auswaehlen(f,ug,og);<br />
// Vertauschen<br />
int temp = f[ug];<br />
f[ug] = f[ixmin];<br />
f[ixmin] = temp;<br />
// Sortieren des restlichen Felds:<br />
bereichsort(f, ug+1, og);<br />
}<br />
/* Liefert Index mit minimalem Element im<br />
Bereich f[ug] .. f[og] von Feld f */<br />
int auswaehlen( int[] f, int ug, int og) {<br />
int ixmin = ug;<br />
for( int j = ug+1; j
Iterative Fassung des Sortierens durch Auswahl:<br />
void sortieren( /*nonnull*/ int[] f ) {<br />
}<br />
}<br />
for( int i = 0; i
Analyse der Laufzeit von sortieren:<br />
In Abhängigkeit von der Größe N des Feldes<br />
schätzen wir die Anzahl A(N) der Operationen/<br />
Rechenschritte für den ungünstigsten Fall ab.<br />
Eine Operation ist:<br />
- ein Vergleich<br />
- eine Zuweisung<br />
- eine Addition/Subtraktion<br />
Vereinfachende Annahme:<br />
- Alle Operationen brauchen die gleiche Zeit.<br />
- Andere Aspekte der Ausführung werden<br />
vernachlässigt (Speicherverwaltung, Sprünge)<br />
Aufwand B(i,N) der inneren Schleife:<br />
B(i,N) ≤ (2+1) + (N-i-1) * (2+2)<br />
Aufwand A(N) des gesamten Rumpfes für N ≥ 2:<br />
A(N) = 3 + Σ (1 + B(i,N) + 3 + 2 )<br />
N-1<br />
≤ 3 + Σ (9 + (N-i) * 4 ) = 3 + (N-1)*9 + 4* Σ i<br />
i=1<br />
N-2<br />
i=0<br />
= 9*N – 6 + 2*N*(N-1) = 2*N + 7*N – 6<br />
A(0) = A(1) = 3<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
104<br />
2<br />
N-1<br />
i=1
O-Notation<br />
Häufig interessiert man sich nur für die<br />
Größenordnung des Wachstums der<br />
Aufwandsfunktion A(N), die den Aufwand an<br />
Speicher bzw. Zeit in Abhängigkeit von der<br />
Problemgröße N beschreibt.<br />
Begriffsklärung: (obere Schranke)<br />
Eine Funktion f: Nat � R heißt obere Schranke<br />
einer Aufwandsfunktion A, wenn gilt:<br />
Es gibt c,d in Nat, sodass für alle N in Nat gilt:<br />
A(N) ≤ c * f(N) + d<br />
Man sagt auch, A wächst wie f bzw. ist von der<br />
Größenordnung f. Die Menge aller Funktionen<br />
von der Größenordnung f bezeichnet man mit O(f) :<br />
O(f) = { g | ∃ c,d in Nat: ∀N in Nat: g(N) ≤ c* f(N) + d } .<br />
Bemerkung:<br />
• Entsprechend definiert man auch untere Schranken.<br />
• Vertiefung in „Entwurf und Analyse von Algorithmen“<br />
• Meist schreibt man O(N) statt O(λN.N), O(N ) statt<br />
2<br />
O(λN.N ), O(log N) statt O(log), usw.<br />
+<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
105<br />
2
Beispiel: (Bestimmung oberer Schranken)<br />
Der Zeitaufwand A vom Sortieren durch Auswahl ist<br />
2<br />
A(N) ≤ 2*N + 7*N - 6<br />
und damit in O(N 2 ); denn mit c= 3 und d = 6 gilt:<br />
A(N) ≤ 3 * N + 6<br />
Wichtige Komplexitätsklassen:<br />
Kompl.klasse Bezeichnung Beispiel<br />
O(1) konstant Hashverfahren<br />
2<br />
O(log N) logarithmisch binäre Suche in Bäumen<br />
O(N) linear sequentielle Suche<br />
O(N * log N) n log n gute Sortierverfahren<br />
2<br />
O(N ) quadratisch einfache Sortierverfahren<br />
3<br />
O(N ) kubisch Matrixmultiplikation<br />
N<br />
O(2 ) exponentiell Optimierungverfahren<br />
Algorithmen mit einem Aufwand in O(N k<br />
) , k ≥ 2,<br />
nennt man polynomisch oder engl. polynomial.<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
106
Diskussion der O-Notation:<br />
• Die O-Notation liefert eine grobe Klassifikation.<br />
• In der Praxis können konstante Faktoren<br />
und Programmiersprachen-spezifische Aspekte<br />
entscheidend sein (siehe Beispiel unten).<br />
• Weitere Aspekte für die Effizienzbetrachtung:<br />
- Antwortzeiten interaktiver Softwaresysteme<br />
- Kommunikationszeiten<br />
Beispiel: (zur obigen Diskussion)<br />
Wir betrachten zwei Versionen eines Programms,<br />
das eine Datei liest und wieder ausgibt.<br />
Die Versionen illustrieren insbesondere:<br />
• Abhängigkeit von programmiersprachlichen<br />
Aspekten (hier: Komplexität von scheinbaren<br />
und wirklichen Grundoperationen)<br />
• Notwendigkeit des Verständnisses technischer<br />
Aspekte zur Beurteilung nicht-funktionaler Eigenschaften<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
107
Abstrakte Schnittstelle zum Lesen von Dateien:<br />
/* Ein Verbund vom Typ DateiLesePosition<br />
repräsentiert eine Position in einer Datei.<br />
An dieser Position kann man das nächste<br />
Zeichen lesen.<br />
*/<br />
class DateiLesePosition {<br />
...<br />
}<br />
/* Öffnet Datei mit Namen dname und liefert<br />
Referenz vom Typ DateiLesePosition, die die<br />
Anfangsposition in der Datei repräsentiert.<br />
Gibt es Fehler beim Öffnen, wird null<br />
zurückgegeben.<br />
*/<br />
DateiLesePosition oeffneDatei( String dname ){<br />
...<br />
}<br />
/* Ist die aktuelle Position am Dateiende, wird<br />
das EOT-Zeichen ‘\4‘ geliefert. Andernfalls<br />
wird das Zeichen an aktueller Position geliefert<br />
und die Position eins weiter geschaltet.<br />
*/<br />
char naechstesZeichen( DateiLesePosition d ) {<br />
...<br />
}<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
108
Bemerkung:<br />
Obige Schnittstelle arbeitet nur für Dateien korrekt,<br />
die das EOT-Zeichen (end of transmission) nicht<br />
enthalten.<br />
Ineffiziente Programmversion: Datei-Lesen Version 1<br />
public static void main( String[] a ) {<br />
if( arg.length == 1 ) {<br />
DateiLesePosition dlp = oeffneDatei(a[0]);<br />
if( dlp == null ) {<br />
println("Fehler: Datei existiert nicht");<br />
return;<br />
}<br />
String s = "";<br />
char c = naechstesZeichen(dlp);<br />
int count = 1;<br />
while( c != '\4' ){ // '\4' ist EOT<br />
s = s + c;<br />
c = naechstesZeichen(dlp);<br />
count++;<br />
if( count%1000==0 ) println(count);<br />
}<br />
println("Datei Inhalt:");<br />
println( s );<br />
} else {<br />
println("Usage: java DateiLesen ");<br />
}<br />
}<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
109
Effizientere Programmversion: Datei-Lesen Version 2<br />
public static void main( String[] a ) {<br />
if( arg.length == 1 ) {<br />
DateiLesePosition dlp = oeffneDatei(a[0]);<br />
if( dlp == null ) {<br />
println("Fehler: Datei existiert nicht");<br />
return;<br />
}<br />
final int feldgroesse = 100000;<br />
String s = "";<br />
char[] cfeld = new char[feldgroesse];<br />
char c = naechstesZeichen(dlp);<br />
int count = 1;<br />
int index = 0;<br />
while( c != '\4' ){ // '\4' ist EOT<br />
cfeld[index] = c;<br />
c = naechstesZeichen(dlp);<br />
count++;<br />
index++;<br />
if( count%1000==0 ) println(count);<br />
if( index == feldgroesse ) {<br />
index = 0;<br />
s = s + new String(cfeld);<br />
}<br />
}<br />
println("Datei Inhalt:");<br />
println( s );<br />
} else {<br />
println("Usage: java DateiLesen ");<br />
} }<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
110
Gemessene Zeit zum Lesen einer 150 kByte großen<br />
Datei auf einem AMD Opteron Dual Core 270<br />
Rechner:<br />
V1: 76,1 s<br />
V2: 0,4 s<br />
Grund:<br />
„+“ auf String kopiert Argumente; dadurch ergibt<br />
sich insgesamt für V1 eine quadratische Laufzeitkomplexität<br />
in Abhängigkeit von der Seitengröße.<br />
V2 hat zwar theoretisch die gleiche Komplexität,<br />
aber diese wirkt sich nur bei sehr großen Dateien<br />
aus.<br />
Bemerkung:<br />
Bei der Betrachtung der Komplexität vergisst man<br />
leicht, dass sich die Komplexitätsklassen nur auf<br />
das asymptotische Verhalten beziehen.<br />
Für kleine N (die man in der Realität oft hat) kann<br />
das ganz anders aussehen.<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
111
<strong>4.</strong>3.2 Speicherverwaltung<br />
Speicher ist eine wichtige Ressource für<br />
Softwaresysteme. Viele nicht-funktionale<br />
Eigenschaften hängen vom angemessenen<br />
Umgang mit Speicher ab.<br />
Wir betrachten grundlegende Aspekte der<br />
Speicherverwaltung:<br />
- Einführung: Speicher in der Programmierung<br />
- Automatische Speicherbereinigung<br />
- Deallokation von Objekten<br />
Einführung<br />
Wir betrachten hier nur den Speicher, der für<br />
die Ausführung von Programmen benötigt wird,<br />
und zwar in Form eines Byte-adressierbaren<br />
virtuellen Adressraums.<br />
Wichtige Fragen:<br />
- Wofür wird Speicher benötigt?<br />
- Wie ist der Speicher organisiert?<br />
- Wie wird der Speicher verwaltet?<br />
- Wie viel Speicher braucht ein Programm?<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
112
Wofür Speicher benötigt wird:<br />
Speicher wird benötigt für:<br />
- das Programm (unabhängig von Eingabedaten)<br />
- Konstanten<br />
- Variablen (global, prozedurlokal, objektlokal)<br />
- Verwaltung von Prozedur-/Methodenaufrufen<br />
Beispiele: (Verwendung von Speicher)<br />
1. Programm, Konstanten und globale Variable:<br />
String s;<br />
String ss;<br />
public class SpeicherIllustration1 {<br />
public static void main( String[] ins ) {<br />
s = ins[0] + " war die Eingabe";<br />
ss = "Zu Seiteneffekten lesen "<br />
+ "Sie die Dokumentation\n"<br />
+ "und fragen Sie Ihren Tutor "<br />
+ "oder Professor";<br />
println( s + "\n" + ss );<br />
}<br />
}<br />
Speicherbedarf ist relativ einfach zu bestimmen, da<br />
unabhängig von der Eingabe.<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
113
2. Verbundkomponenten/Objektlokale Variablen:<br />
class IntList {<br />
int fst;<br />
IntList rst;<br />
}<br />
IntList empty() {<br />
return new IntList();<br />
}<br />
IntList append( int i, IntList xl ) {<br />
IntList il = new IntList();<br />
il.fst = i;<br />
il.rst = xl;<br />
return il;<br />
}<br />
public class SpeicherIllustration2 {<br />
public static void main( String[] ins ) {<br />
IntList il = append(3,empty());<br />
il = append( 134, il );<br />
il = append( 9, il );<br />
int i = Integer.parseInt( ins[0] );<br />
while( i > 0 ) {<br />
il = append( i, il );<br />
i--;<br />
}<br />
} }<br />
Speicherbedarf hängt von der Eingabe ab. Lebensdauer<br />
der Verbunde erstreckt sich von der Erzeugung<br />
bis zum Ende des Programmablaufs.<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
114
3. Lokale Variablen und Prozeduraufrufverwaltung:<br />
Als Beispiel betrachten wir die rekursive Fassung<br />
des Sortierens durch Auswahl (vgl. Folie <strong>4.</strong>102).<br />
Speicher wird benötigt für jede Prozedurinkarnation<br />
(vgl. Begriffsklärung auf Folie <strong>4.</strong>53) und zwar<br />
- für den Rückgabewert und die aktuellen Parameter,<br />
- für die Aufrufverwaltung (z.B. Aufrufstelle),<br />
- für die lokalen Variablen.<br />
Speicherbereich für eine Prozedurinkarnation:<br />
Rückgabewert<br />
Aktuelle Parameter<br />
Verwaltungsinformation<br />
Lokale Variable<br />
Speicherbedarf hängt von der Eingabe ab. Lebensdauer<br />
der lokalen Variablen erstreckt sich vom<br />
Prozeduraufruf bis zum Ende der Prozedurausführung.<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
115
Speicherorganisation:<br />
Die Speicherorganisation ist bei den meisten<br />
prozeduralen bzw. objektorientierten Programmiersprachen<br />
ähnlich:<br />
virtueller<br />
Adressraum<br />
BS-Kern<br />
Programm<br />
globale Größen<br />
Halde<br />
Laufzeitkeller<br />
globale, statische<br />
Variablen, Konstanten, ...<br />
(dynamische) Verbunde,<br />
Objekte, ...<br />
Zwischenergebnisse,<br />
prozedurlokale<br />
Größen, Objekte mit beschränkter<br />
Lebensdauer<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
116
Wie Speicher verwaltet wird:<br />
1. Globaler Speicher und Keller werden automatisch<br />
verwaltet (der Übersetzer erzeugt dafür Code).<br />
2. Je nach Programmiersprache wird die Halde<br />
(engl. heap) unterschiedlich verwaltet:<br />
- mit automatischer Speicherbereinigung,<br />
- durch den Programmierer (Deallokation).<br />
Operationen zur Verwaltung der Halde:<br />
� Anfordern von Speicher bei Objekterzeugung:<br />
liefere Speicherbereich ausreichender Größe.<br />
� Freigabe von Speicher:<br />
- Wenn kein Speicher mehr verfügbar, gebe die<br />
Speicherbereiche von Objekten frei, die nicht<br />
mehr erreichbar sind.<br />
- Gebe Speicher von Objekten auf Anweisung<br />
des Programms frei (Deallokation).<br />
Beachte:<br />
Die Speicherverwaltung kostet auch Laufzeit.<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
117
Fazit:<br />
1. Wie viel Speicher ein Programm in Abhängigkeit<br />
von der Eingabe genau braucht, hängt von Details<br />
der Sprachimplementierung und Plattform ab.<br />
2. Mit einem generellen Verständnis der relevanten<br />
Techniken lässt sich der Speicherbedarf aber<br />
gut abschätzen.<br />
Automatische Speicherbereinigung<br />
Begriffsklärung: (Autom. Speicherbereinigung)<br />
Verfahren zur automatischen Speicherbereinigung<br />
(engl. automatic garbage collection) ermitteln periodisch<br />
oder bei Bedarf, welche Objekte nicht mehr erreichbar<br />
(s.u.) sind und geben deren Speicherplatz frei.<br />
Weiteres Ziel ist es, den freien Speicher zu<br />
kompaktifizieren.<br />
Immer mehr Programmiersprachen bieten<br />
automatische Speicherbereinigung (insbesondere<br />
funktionale, logische und objektorientierte Sprachen):<br />
• Vereinfachung der Programmierung<br />
• Aufwand an Speicher und Zeit ist vertretbar.<br />
• Sicherheitsaspekte<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
118
Beispiel: (Autom. Speicherbereinigung)<br />
1. Programm, das unerreichbare Objekte erzeugt:<br />
void main( String[] ins ) {<br />
int count = 1;<br />
while( true ) {<br />
int[] feld = new int[1000000];<br />
println(count++);<br />
}<br />
}<br />
Dies Programm bekommt keine Speicherprobleme.<br />
2. Programm, dessen erzeugte Objekte erreichbar sind:<br />
class ListOfArray {<br />
int[] elem;<br />
ListOfArray next;<br />
}<br />
void main( String[] ins ) {<br />
ListOfArray la = null;<br />
int count = 1;<br />
while( true ) {<br />
ListOfArray tmp = new ListOfArray();<br />
tmp.elem = new int[1000000];<br />
tmp.next = la;<br />
la = tmp;<br />
println(count++);<br />
} }<br />
Dies Programm terminiert mit OutOfMemoryError.<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
119
Begriffsklärung: (Erreichbarkeit)<br />
Ein Verbund bzw. Objekt X heißt von einer Variablen v<br />
direkt erreichbar, wenn v eine Referenz auf X enthält.<br />
X heißt von v erreichbar, wenn es von v direkt<br />
erreichbar ist oder wenn es einen Verbund/ ein Objekt<br />
Y mit Komponente w gibt, so dass X von w direkt<br />
erreichbar ist und Y von v erreichbar ist.<br />
Die Menge der Wurzelvariablen zu einem Ausführungszustand<br />
A umfasst alle globalen Variablen sowie<br />
die aktuell im Keller vorhandenen lokalen Variablen<br />
und Parameter.<br />
Ein Objekt heißt erreichbar in einem Ausführungszustand<br />
A, wenn es von einer Wurzelvariablen zu A<br />
erreichbar ist.<br />
Bemerkung:<br />
Verbunde/Objekte können nur von Wurzelvariablen<br />
oder von Verbundkomponenten/Instanzvariablen<br />
referenziert werden.<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
120
Deallokation von Verbunden/Objekten<br />
Begriffsklärung: (De-/Allokation)<br />
Die Bereitstellung des Speicherbereichs bei der<br />
Erzeugung von Verbunden und Objekten nennt<br />
man Allokation (engl. allocation). Die Freigabe<br />
solcher Speicherbereiche Deallokation (engl.<br />
deallocation).<br />
Die meisten prozeduralen Programmiersprachen<br />
unterstützen De-/Allokation durch den Programmierer:<br />
• Vorteil:<br />
- ermöglicht effiziente Benutzung von Speicher<br />
• Nachteile:<br />
- zusätzlicher Programmieraufwand<br />
- potentielle Fehlerquelle<br />
- führt leicht zu Sicherheitslücken<br />
Wir betrachten hier De-/Allokation von Objekten<br />
in C++.<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
121
Verbunde und Zeiger in C++:<br />
C++ unterscheidet zwischen Verbunden und<br />
und Zeigern/Referenzen auf Verbunde.<br />
Beispiel: (Verbunde und Zeiger)<br />
#include <br />
class Punkt {<br />
public:<br />
int x;<br />
int y;<br />
};<br />
Punkt* puenktchen() {<br />
}<br />
Punkt* p = new Punkt();<br />
p->x = 1;<br />
p->y = 2;<br />
Punkt q;<br />
q.x = 3;<br />
q.y = 4;<br />
return p;<br />
int main() {<br />
Punkt* r = puenktchen();<br />
cout
Wie in Java, alloziert der Operator „new“ in C++<br />
Speicher für neue Verbunde. Als Ergebnis<br />
liefert er einen Zeiger auf den neuen Verbund.<br />
Ist K ein Verbundtyp, dann bezeichnet in C++<br />
K* den Typ der Zeiger auf Verbunde vom Typ K.<br />
Den Speicherplatz, den man mit new alloziert hat,<br />
kann man durch Aufruf des Operators delete wieder<br />
freigeben, wenn er nicht mehr gebraucht wird.<br />
Beispiel: (Verbunde und Zeiger, Fortsetzung)<br />
// ... wie auf Folie <strong>4.</strong>122<br />
int main() {<br />
}<br />
Punkt* r = puenktchen();<br />
cout
Beispiel: (Wirkung der Deallokation)<br />
Zum Vergleich mit Java (s. Folie <strong>4.</strong>119) betrachten wir<br />
zwei Varianten eines C++ Programms mit und ohne<br />
Deallokation. Sei Klasse Vektor gegeben:<br />
class Vektor {<br />
public:<br />
int elems [1000000];<br />
};<br />
Folgendes Programm führt zu einem Abbruch wegen<br />
Speicherüberlaufs:<br />
int main() {<br />
while( true ) {<br />
Vektor* vp = new Vektor();<br />
}<br />
return 0;<br />
}<br />
Deallokation der Vektorverbunde verhindert den<br />
Speicherüberlauf:<br />
int main() {<br />
while( true ) {<br />
Vektor* vp = new Vektor();<br />
// mache irgendwas mit dem Vektor:<br />
delete vp;<br />
}<br />
return 0;<br />
}<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
124
Verwendung von Deallokation:<br />
Ein Programmteil P kann einen Speicherbereich<br />
freigeben, wenn:<br />
- P den Speicherbereich nicht mehr benötigt,<br />
- P den Speicherbereich kontrolliert, d.h. sicher sein<br />
kann, dass er von keiner anderen Stelle benötigt wird.<br />
Weitere Aspekte der Deallokation:<br />
• Deallokation wird in einigen Sprachen durch weitere<br />
Sprachmittel unterstützt (z.B. Destruktoren in C++).<br />
• Deallokation und automatische Speicherbereinigung<br />
lassen sich kombinieren:<br />
- Anweisungen an den Garbage Collector<br />
- Soft und weak references in Java<br />
• Deallokation bezieht sich nicht nur auf Speicher-,<br />
sondern auch auf andere Ressourcen.<br />
• Ein systematischer Umgang mit einer Ressource<br />
bedeutet zu klären,<br />
- wer die Ressource kontrollieren soll,<br />
- wer Zugriff auf die Ressource erhält.<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
125
<strong>4.</strong>3.3 Laufzeitverhalten<br />
Dieser Abschnitt ergänzt die in <strong>4.</strong>3.1 vorgestellten<br />
Aspekte zum Laufzeitverhalten.<br />
Das Laufzeitverhalten eines Programms wird<br />
bestimmt durch:<br />
- die Anweisungen des Programms<br />
- den Übersetzer<br />
- die Laufzeitumgebung, insbesondere die<br />
die Speicherverwaltung<br />
- die Systemumgebung.<br />
Speicherverwaltung kostet Zeit:<br />
Speicherverwaltung ist aufwendig, wenn der verfügbare<br />
Speicher knapp wird:<br />
- Garbage Collector muss häufig aufgerufen werden.<br />
- Das Aufsuchen ausreichend großer freier Speicherbereiche<br />
wird aufwendiger.<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
126
Insgesamt ist der Aufwand in der Praxis nicht<br />
leicht abzuschätzen, weil<br />
- der Speicherverbrauch der Bibliotheksklassen<br />
und anderer fremder Programmteile häufig nicht<br />
klar spezifiziert ist;<br />
- die Details der Speicherverwaltung eine wichtige<br />
Rolle spielen.<br />
Problematisch ist das insbesondere bei Echtzeitanforderungen.<br />
Systemumgebung beeinflusst das<br />
Laufzeitverhalten:<br />
Zur Gesamtbeurteilung des Laufzeitverhaltens eines<br />
Softwaresystems muss auch die Systemumgebung<br />
berücksichtigt werden:<br />
- Benutzerinteraktion<br />
- Anzahl von Benutzern<br />
- Kommunikationszeiten<br />
- Laufzeitverhalten der Plattform<br />
- Interaktion mit anderen Systemen<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
127
Fazit:<br />
- Präzise Bestimmung der Effizienz ist im Allg.<br />
schwierig und von vielen technischen Aspekten<br />
abhängig; aber auch nur bei ausgewählten<br />
Anwendungen nötig.<br />
- Durch geeignete Abstraktion kann man nachvollziehbare<br />
Aussagen über die Effizienz eines<br />
Algorithmus‘, Programms oder Softwaresystems<br />
machen.<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
128
<strong>4.</strong>3.4 Prozedurale Algorithmen und<br />
deren Analyse<br />
Vorgehen:<br />
Wir betrachten prozedurale Formulierungen und<br />
die Analyse von drei Sortieralgorithmen:<br />
- Sortieren durch Einfügen<br />
- Quicksort<br />
- Heapsort<br />
Bei allen Algorithmen gehen wir davon aus, dass<br />
die zu sortierenden Daten in einem Feld vorliegen,<br />
das verändert werden darf.<br />
Datensätze stellen wir durch folgenden<br />
Datentypen dar:<br />
class DataSet {<br />
int key;<br />
String data;<br />
}<br />
DataSet mkDataSet( int k, String s ) {<br />
DataSet ds = new DataSet();<br />
ds.key = k;<br />
ds.data = s;<br />
return ds;<br />
}<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
129
Bemerkung:<br />
Beachte bei den folgenden Beispielen:<br />
1. Die algorithmische Grundidee ist unabhängig<br />
vom verwendeten Programmierparadigma.<br />
2. Die Verwendung von Feldern statt Listen kann<br />
die Komplexität ändern.<br />
Sortieren durch Einfügen<br />
Algorithmische Grundidee:<br />
Sortiere zunächst eine Teilliste (Terminierungsfall:<br />
leere Liste). Füge dann die verbleibenden Elemente<br />
nacheinander in die bereits sortierte Teilliste ein.<br />
Funktionale Fassung:<br />
fun sortieren nil = nil<br />
| sortieren (x::xl) =<br />
einfuegen x (sortieren xl)<br />
and einfuegen x [] = [x]<br />
| einfuegen (kx,sx) ((ky,sy)::yl) =<br />
if kx
Nachteil der rekursiven Fassung:<br />
- Aufwand durch Listendarstellung<br />
- Aufwand durch rekursive Aufrufe<br />
Ideen zur prozeduralen Realisierung:<br />
- Speichere die Datensätze in einem Feld<br />
- Realisiere das Einfügen durch schrittweises<br />
Verschieben (ausgehend vom größten Element)<br />
- Eliminiere die Rekursion durch Beginn mit der<br />
einelementigen Liste in die nacheinander Elemente<br />
eingefügt werden.<br />
einfügen<br />
sortiert unsortiert<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
131
Prozedurale Fassung in Java:<br />
void sortieren(/*nonnull*/ DataSet[] f) {<br />
DataSet tmp; // einzufuegender Datensatz<br />
for( int i = 1; i=1 && f[j-1].key > tmp.key ) {<br />
// Verschiebe groessere Saetze mit<br />
// groesseren Schluesseln<br />
f[j] = f[j-1];<br />
j--;<br />
}<br />
// Setze tmp an neue Position<br />
f[j] = tmp;<br />
}<br />
}<br />
public static void main( String[] arg ) {<br />
DataSet[] feld = new DataSet[arg.length];<br />
for( int i = 0; i
Laufzeitabschätzung:<br />
Wir betrachten die Anzahl der Schlüsselvergleiche<br />
C und der Zuweisungen M von Datensätzen in<br />
Abhängigkeit von der Anzahl N der Datensätze.<br />
Günstigster Fall:<br />
Liste ist bereits aufsteigend sortiert.<br />
� pro Schleifendurchlauf ein Schlüsselvergleich<br />
� pro Durchlauf zwei Datensatzzuweisungen<br />
Schlüsselvergleiche: C (N) = N -1;<br />
Datensatzzuweisungen: M (N) = 2*(N –1);<br />
Ungünstigster Fall:<br />
Liste ist absteigend sortiert.<br />
min<br />
min<br />
� pro Schleifendurchlauf i Schlüsselvergleiche<br />
� pro Durchlauf (i+2) Datensatzzuweisungen<br />
Schlüsselvergleiche: C (N) = Σ i ∈ O(N )<br />
Datensatzzuweisungen: M (N) = Σ (i+2) ∈ O(N )<br />
Durchschnitt:<br />
max<br />
N-1<br />
max i=1<br />
N-1 2<br />
Im Durchschnitt ergibt sich quadratische Komplexität.<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
133<br />
i=1<br />
2
Quicksort<br />
Algorithmische Grundidee:<br />
• Wähle einen beliebigen Datensatz mit Schlüssel k<br />
aus, das sogenannte Pivotelement.<br />
• Teile die Liste in zwei Teile:<br />
- 1. Teil enthält alle Datensätze mit Schlüsseln < k<br />
- 2. Teil enthält die Datensätze mit Schlüsseln ≥ k<br />
• Wende quicksort rekursiv auf die Teillisten an.<br />
• Hänge die resultierenden Listen und das Pivotelement<br />
zusammen.<br />
Funktionale Fassung:<br />
fun qsort [] = nil<br />
| qsort ((pk,ps)::rest) =<br />
let val (below,above) = split pk rest in<br />
end<br />
qsort below @[(pk,ps)]@ qsort above<br />
and split p [] = ([],[])<br />
| split p ((xk,xs)::xr) =<br />
let val (below, above) = split p xr in<br />
end<br />
if xk < p then ((xk,xs)::below,above)<br />
else (below,(xk,xs)::above)<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
134
Umsetzung in prozedurale Fassung:<br />
- Speichere die Datensätze in einem Feld und<br />
bearbeite rekursiv Teilbereiche des Feldes<br />
- Realisiere das Teilen der Liste durch Vertauschen:<br />
� Indexzähler left, right laufen von links bzw.<br />
rechts bis f[left].key ≥ pivot.key<br />
&& f[right].key < pivot.key<br />
Es gilt:<br />
Für alle i in [ug,left-1] : f[i].key < pivot.key<br />
Für alle i in [right+1,og] : pivot.key ≤ f[i].key<br />
1. Fall:<br />
left > right : Teilung vollzogen:<br />
2. Fall:<br />
ug og<br />
left right<br />
left ≤ right : Vertausche f[left] und f[right],<br />
inkrementiere left und right und fahre fort.<br />
ug og<br />
left right<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
135
Prozedurale Fassung in Java:<br />
void sortieren(/*nonnull*/ DataSet[] f) {<br />
quicksort(f,0,f.length-1);<br />
}<br />
void quicksort( DataSet[] f, int ug, int og){<br />
if( ug < og ) {<br />
int ixsplit = partition(f,ug,og);<br />
/* ug
int partition( DataSet[] f, int ug, int og){<br />
DataSet dtmp;<br />
int left = ug;<br />
int pk = f[og].key;<br />
int right = og-1;<br />
boolean b = true;<br />
while( b ) {<br />
while( f[left].key < pk ) { left++; }<br />
while( left=pk ){<br />
right--; }<br />
if( left > right ) {<br />
b = false;<br />
} else {<br />
dtmp = f[left];<br />
f[left] = f[right];<br />
f[right] = dtmp;<br />
left++;<br />
right--;<br />
}<br />
}<br />
dtmp = f[left];<br />
f[left] = f[og];<br />
f[og] = dtmp;<br />
return left;<br />
}<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
137
Grobe Laufzeitabschätzung von Quicksort:<br />
Seien C, M und N definiert wie auf Folie <strong>4.</strong>133.<br />
Vorüberlegung:<br />
Betrachte die Ebenen gleicher Tiefe im Aufrufbaum<br />
von quicksort. Das Zerlegen aller Teillisten auf einer<br />
Ebene verursacht schlimmstenfalls linearen Aufwand:<br />
C (N) = O(N)<br />
part<br />
M (N) = O(N)<br />
part<br />
Ungünstigster Fall:<br />
Beim Zerlegen der Listen ist jeweils eine der Teillisten<br />
leer. Dann hat der Aufrufbaum die Tiefe N, also gilt:<br />
C (N) = N * C (N) = O(N )<br />
max<br />
M (N) = N * M (N) = O(N )<br />
max<br />
Günstigster Fall:<br />
part<br />
part<br />
Beim Zerlegen der Liste entstehen jeweils zwei etwa<br />
gleich große Teillisten. Dann hat der Aufrufbaum die<br />
Tiefe log N, also gilt:<br />
C (N) = log N * C (N) = O(N log N)<br />
min<br />
M (N) = log N * M (N) = O(N log N)<br />
min<br />
part<br />
part<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
138<br />
2<br />
2
Bemerkung:<br />
• Die mittlere Laufzeit von Quicksort ist auch von<br />
der Größenordnung O(N log N) (siehe Ottmann,<br />
Widmayer: Abschn. 2.2)<br />
• Die vorgestellte Quicksort-Fassung arbeitet<br />
schlecht auf schon sortierten Listen.<br />
• Verbesserungen der vorgestellten Variante ist<br />
möglich durch geeignetere Auswahl des Pivotelementes<br />
und durch Elimination der Rekursion.<br />
Heapsort<br />
Zur Einführung siehe Folie 3.105 ff. Zur Erinnerung:<br />
Heap wird verwendet, um schnell einen Datensatz<br />
mit maximalem Schlüssel zu finden.<br />
Algorithmische Idee:<br />
• 1. Schritt: Erstelle den Heap zur Eingabefolge.<br />
• 2. Schritt:<br />
- Entferne Maximumelement aus Heap ( O(1) )<br />
und hänge es vorne an die schon sortierte Liste.<br />
- Stelle Heap-Bedingung wieder her ( O(log N) ).<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
139
Vorgehen:<br />
- Entwicklung einer prozeduralen Datenstruktur<br />
FVBinTree für fast vollständige Binärbäume<br />
- Heapsort unter Nutzung von FVBinTree<br />
- Elimination der Schnittstelle<br />
Prozedurale Datenstruktur für fast vollstän-<br />
dige, markierte, indizierte Binärbäume:<br />
class FVBinTree {<br />
DataSet[] a;<br />
int currsize;<br />
}<br />
/* f ist nicht null; uebernimmt f, d.h.<br />
Modifikationen an dem Ergebnis ändern<br />
moeglicherweise auch f<br />
*/<br />
FVBinTree mkFVBinTree( DataSet[] f ){<br />
FVBinTree t = new FVBinTree();<br />
t.a = f;<br />
t.currsize = f.length;<br />
return t;<br />
}<br />
/* liefert Groesse von t; lesend */<br />
int size( FVBinTree t ){ return t.currsize; }<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
140
* lesend */<br />
DataSet get( FVBinTree t, int ix ) {<br />
return t.a[ix];<br />
}<br />
/* modifizier t */<br />
void swap( FVBinTree t, int ix1, int ix2 ) {<br />
DataSet dtmp = t.a[ix1];<br />
t.a[ix1] = t.a[ix2];<br />
t.a[ix2] = dtmp;<br />
}<br />
/* modifizier t */<br />
void removeLast( FVBinTree t ){t.currsize--;}<br />
/* lesend */<br />
boolean hasLeft( FVBinTree t, int ix ){<br />
return left(t,ix) < t.currsize;<br />
}<br />
/* lesend */<br />
boolean hasRight( FVBinTree t, int ix ) {<br />
return right(t,ix) < t.currsize;<br />
}<br />
/* lesend */<br />
int left( FVBinTree t, int ix ) {<br />
return 2*(ix+1)-1;<br />
}<br />
/* lesend */<br />
int right( FVBinTree t, int ix ) {<br />
return 2*(ix+1);<br />
}<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
141
Bemerkung:<br />
Bei einer prozeduralen Datenstruktur muss man<br />
sich genau merken, welche Operationen<br />
- Referenzen übernehmen bzw.<br />
- Änderungen vornehmen.<br />
/* Stellt Heap-Eigenschaft her, wobei die<br />
Kinder des Knotens ix die Eigenschaft<br />
bereits erfuellen muessen; modifiziert t<br />
*/<br />
void heapify( FVBinTree t, int ix ) {<br />
} }<br />
int ixk = get(t,ix).key;<br />
if( hasLeft(t,ix) && !hasRight(t,ix) ) {<br />
int lx = left(t,ix);<br />
if( ixk < get(t,lx).key ) {<br />
}<br />
swap(t,ix,lx);<br />
} else if( hasRight(t,ix) ) {<br />
int lx = left(t,ix);<br />
int rx = right(t,ix);<br />
int largerChild =<br />
get(t,lx).key > get(t,rx).key ? lx : rx;<br />
if( ixk < get(t,largerChild).key ) {<br />
}<br />
swap( t, ix, largerChild );<br />
heapify( t, largerChild );<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
142
void sortieren(/*nonnull*/ DataSet[] f) {<br />
FVBinTree t = mkFVBinTree( f );<br />
// Herstellen der Heap-Bedingung<br />
}<br />
for( int i = size(t)/2 - 1; i >= 0; i-- ){<br />
heapify(t,i);<br />
}<br />
// Sortieren<br />
while( size(t) > 0 ) {<br />
}<br />
swap( t, 0, size(t)-1 );<br />
removeLast(t);<br />
heapify(t,0);<br />
In einem Optimierungsschritt:<br />
- Eliminieren wir den Datentyp FVBinTree und<br />
arbeiten direkt auf dem übergebenen Feld, wobei<br />
wir die aktuelle Größe in einer lokalen Variable<br />
speichern.<br />
- Ersetzen wir die Operationen der Datenstruktur<br />
durch deren Rümpfe.<br />
- Benutzen wir eine swap-Prozedur für Felder:<br />
void swap( DataSet[] f, int i1, int i2 ){<br />
DataSet dtmp = f[i1];<br />
f[i1] = f[i2];<br />
f[i2] = dtmp;<br />
}<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
143
* Kommentar siehe oben; modifiziert f<br />
*/<br />
void heapify( DataSet[] f, int size, int ix ){<br />
int ixk = f[ix].key;<br />
int rx = 2*(ix+1);<br />
int lx = rx – 1;<br />
if( lx < size && rx >= size ) {<br />
if( ixk < f[lx].key ) {<br />
swap(f,ix,lx);<br />
}<br />
} else if( rx < size) {<br />
int largerChild =<br />
f[lx].key > f[rx].key ? lx : rx;<br />
if( ixk < f[largerChild].key ) {<br />
swap(f,ix,largerChild);<br />
heapify( f, size, largerChild );<br />
}<br />
} }<br />
void sortieren(/*nonnull*/ DataSet[] f) {<br />
int size = f.length;<br />
// Herstellen der Heap-Bedingung<br />
for( int i = size/2 - 1; i >= 0; i-- ) {<br />
heapify(f,size,i);<br />
}<br />
// Sortieren<br />
while( size > 0 ) {<br />
size--;<br />
swap( f, 0, size );<br />
heapify(f,size,0);<br />
}<br />
}<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
144
Grobe Laufzeitabschätzung von Heapsort:<br />
Seien C, M und N definiert wie auf Folie <strong>4.</strong>133. Wir<br />
betrachten nur den ungünstigsten Fall.<br />
Ungünstigster Fall:<br />
1. Herstellen der Heap-Eigenschaft:<br />
Bezeichne j die Anzahl der Niveaus im Heap,<br />
also 2 ≤ N ≤ 2 -1.<br />
Dann gibt es auf Niveau k höchstens 2 Schlüssel<br />
und C und M sind proportional zu j-k .<br />
Insgesamt gilt dann für die Anzahl der Operationen<br />
zur Herstellung der Heap-Eigenschaft:<br />
j-1 k<br />
max<br />
Σ 2 * (j-k) = Σ i * 2 = 2 * Σ i ≤ 2*N*2 ∈O(N)<br />
i=1<br />
i=1 2<br />
k=1<br />
j-1 j<br />
max<br />
j-1 j-i j<br />
2. Auswahl des Wurzelelements und Versickern:<br />
Da die Höhe eines fast vollständigen Binärbaums<br />
von Ordnung O(log N) ist, führt heapify O(log N)<br />
Operationen aus. Damit ergibt sich für diese Teile<br />
die Komplexität O(N log N).<br />
3. Komplexität des gesamten Algorithmus:<br />
O(N) + O(N log N) = O(N log N)<br />
j-1 i<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
145<br />
k
Übersicht über die Komplexität von<br />
Sortierverfahren:<br />
O(N k )<br />
k ≤ 2<br />
Sortierverfahren<br />
intern (im HSP) extern (nicht im HSP)<br />
Auswählen<br />
Einfügen<br />
Bubblesort<br />
Shellsort<br />
Baumsortierung<br />
Quicksort<br />
O( N log N)<br />
2<br />
Baumsortierung (AVL)<br />
Heapsort<br />
Mergesort<br />
Mergesort<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
146
<strong>4.</strong>3.5 Algorithmenklassen & -entwicklung<br />
Dieser Abschnitt skizziert:<br />
• wichtige weitere Problem- und Algorithmenklassen<br />
• einen Weg zur Entwicklung von Algorithmen<br />
anhand eines Beispiels<br />
Problem- und Algorithmenklassen<br />
Neben dem klassischen Bereichen des Sortierens<br />
und Suchens von Datensätzen gibt es eine Vielzahl<br />
von Algorithmen für unterschiedliche Aufgabenund<br />
Problembereiche.<br />
Beispiele: (Algorithmische Probleme)<br />
• Optimaler Einsatz der Flugzeugflotte einer<br />
Fluggesellschaft.<br />
• Ermittlung der Schnittfläche zweier Flächen gegeben<br />
durch ihre Punkte<br />
• Erfüllbarkeit/Allgemeingültigkeit logischer Formeln.<br />
• Auffinden aller Web-Seiten, die eine Menge von<br />
Schlüsselwörter enthalten<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
147
Klassifikation der Algorithmen gemäß:<br />
• verwendeter Datenstrukturen<br />
• algorithmischer Kriterien (z.B. Art der Parallelität)<br />
• spezieller Aufgabenbereiche<br />
Datenstrukturen:<br />
Mengen, Listen, Warteschlangen, etc.:<br />
Ziele:<br />
Effiziente Speicherung und effiziente Operationen<br />
zum Einfügen, Suchen und Löschen.<br />
Zeichenreihen, Textsuche:<br />
Ziele:<br />
Effiziente Suche von Wort- oder Textmustern in<br />
Texten.<br />
Beispiel:<br />
Finde alle Vorkommen von „S%Haffner“ in den letzten<br />
5 Jahrgängen der Frankfurter Allgemeinen Zeitung.<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
148
Graphen:<br />
Begriffsklärung: (Graph)<br />
Ein gerichteter Graph (engl. digraph) G = (V, E)<br />
besteht aus<br />
- einer endlichen Menge V von Knoten (engl. vertices)<br />
- einer Menge E ⊆ VxV von Kanten (engl. edges)<br />
Ist (va,ve) eine Kante, dann nennt man<br />
- va den Anfangs- oder Startknoten oder die Quelle<br />
- ve den Endknoten oder das Ziel<br />
der Kante. ve heißt von va direkt erreichbar und<br />
Nachfolger von va; va Vorgänger von ve.<br />
Graphen bieten für eine große Klasse von<br />
Problemen ein geeignetes abstraktes Modell.<br />
Beispiele:<br />
- Was ist die beste Verbindung von A nach B?<br />
- Wie transportiere ich Waren von mehreren Anbietern<br />
am billigsten zu mehreren Nachfragern?<br />
- Wie gestalte ich einen Arbeitsablauf mit mehreren<br />
Maschinen und Arbeitskräften optimal?<br />
- Welche Wassermenge kann maximal durch die<br />
Kanalisation von KL abgeleitet werden?<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
149
Speicherdarstellungen von Graphen:<br />
Sei G = (V,E) ein gerichteter Graph, V = {1,...,n}.<br />
G lässt sich speichern als:<br />
- Adjazenzmatrix: boolesche nxn-Matrix, wobei<br />
das Element (x,y) true ist genau dann, wenn es<br />
in G eine Kante von x nach y gibt.<br />
- Adjazenzlisten: Speichere für jeden Knoten die<br />
Liste der durch eine Kante erreichbaren Knoten.<br />
Algorithmische Kriterien:<br />
Wir haben bisher nur sequentielle Algorithmen<br />
betrachtet, deren Daten alle im Hauptspeicher<br />
Platz finden. In der Praxis sind häufig komplexere<br />
Anforderungen zu berücksichtigen:<br />
- Daten auf anderen Speichermedien ohne wahlfreies<br />
Zugriffsverhalten<br />
- Parallelisierung für gegebene Rechner, um<br />
akzeptable Antwortzeiten zu erhalten bzw. große<br />
Datenmengen rechnen zu können.<br />
- Arbeiten mit verteilten, sich dynamisch<br />
entwickelnden Daten<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
150
Aufgabenbereiche:<br />
Viele Teilgebiete der Informatik und anderer Fächer<br />
haben mittlerweile für ihre speziellen Aufgaben<br />
umfangreiches algorithmisches Wissen erarbeitet.<br />
Zwei Beispiele:<br />
- Algorithmische Geometrie<br />
- Übersetzertechnik/Compilerbau<br />
Algorithmische Geometrie:<br />
Beispielproblem:<br />
Gegeben eine Menge von Rechtecken; ermittle<br />
alle Paare von Rechtecken, die sich schneiden.<br />
Anwendungsbereiche:<br />
• Computergraphik, Visualisierung<br />
• Geometrische Modellierung, CAD<br />
• Schaltungsentwurf<br />
• Wegeplanung von Robotern<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
151
Übersetzertechnik:<br />
Aufgabenbereiche:<br />
• Parsen gemäß einer kontextfreien Grammatik:<br />
- Eingabe: Zeichenreihe (Programm)<br />
- Ausgabe: Syntaxbaum<br />
• Optimierende Übersetzung, zum Beispiel:<br />
- Konstante Ausdrücke zur Übersetzungszeit<br />
berechnen<br />
- Prozeduraufrufe durch ihre Rümpfe ersetzen<br />
- Speicherbedarf verringern<br />
Beispiel: (Konstantenfaltung)<br />
Übersetze das Programmfragment<br />
int a = 7;<br />
int b = a * 3;<br />
int c = a + b;<br />
so als hätte der Programmierer geschrieben:<br />
int a = 7;<br />
int b = 21;<br />
int c = 28;<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
152
Beispiel: (Escape-Analysis)<br />
Aufgabe:<br />
Ermittele die Objekte, die auf dem Keller alloziert<br />
werden können, da ihre Referenzen den Methodenaufruf,<br />
der sie erzeugt hat, nicht verlassen.<br />
Beispielfragment:<br />
void m( String s ) {<br />
String t = "" + s ; // neuer Verbund<br />
t = doSomething(t); // Modifikation von t<br />
println(t);<br />
}<br />
Der/das von t referenzierte Verbund/Objekt<br />
könnte auf dem Keller verwaltet werden.<br />
Ziel:<br />
Entlastung der Speicherbereinigung.<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
153
Algorithmenentwicklung<br />
Abschließend zu <strong>4.</strong>3 betrachten wir wichtige Phasen<br />
der Algorithmenentwicklung an einem Beispiel.<br />
Phasen der Algorithmenentwicklung:<br />
1. Problemabstraktion und -formulierung<br />
2. Entwickeln einer algorithmischen Idee<br />
3. Ermitteln wichtiger Eigenschaften des Problems<br />
<strong>4.</strong> Grobentwurf eines Algorithmus‘ mit Abstützung<br />
auf existierende Teillösungen<br />
5. Entwickeln bzw. Festlegen der Datenstrukturen<br />
6. Ausarbeiten des Algorithmus<br />
Algorithmenentwicklung an einem Beispiel:<br />
Wir erläutern die Phasen der Algorithmenentwicklung<br />
an einem Beispiel (vgl. Phasen der Softwareentwicklung).<br />
0. Problem:<br />
Routenplaner für Fahrradfahrer in einer Großstadt:<br />
Wie ist die beste Verbindung zwischen zwei<br />
Straßenkreuzungen?<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
154
1. Problemabstraktion und -formulierung:<br />
Modelliere die Straßen und Wege durch einen<br />
gerichteten Graphen mit bewerteten Kanten:<br />
- Straßenkreuzungen entsprechen Knoten<br />
- Kante entspricht einer direkten Straßenverbindung<br />
zwischen Kreuzungen A und B, die von A nach B<br />
befahrbar ist (ggf. auch Kante für umgekehrte<br />
Richtung).<br />
- Jede Kante bekommt als Bewertung die Zeit in<br />
Sekunden, die man im Durchschnitt für den Weg<br />
von A nach B braucht.<br />
Die Bewertung ist eine Funktion c: E � R<br />
Bewertete gerichtete Graphen nennt man<br />
Distanzgraphen.<br />
Damit lässt sich das Problem wie folgt formulieren:<br />
- Gegeben ein Distanzgraph, der die Straßenverbindungen<br />
modelliert, sowie zwei Knoten s und z.<br />
- Gesucht ist ein Weg s, v1, ... , vn , z mit minimaler<br />
Länge lg :<br />
lg = c( (s,v1) ) + c( (v1,v2) ) + ... + c( (vn,z) )<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
155<br />
+
2. Entwickeln einer algorithmischen Idee:<br />
Eine verbreitete Strategie zur Algorithmenentwicklung<br />
versucht ein Problem auf ähnlich geartete<br />
Teilprobleme zu reduzieren. Hier:<br />
Reduziere die Suche des kürzesten Wegs von<br />
s nach z auf kürzeste Wege zwischen anderen<br />
Knotenpaaren.<br />
Ansatz:<br />
(a) Errechne schrittweise Knotenmengen B, sodass<br />
der kürzeste Weg von s zu allen Knoten von B<br />
bekannt ist. Anfangs ist B = { s }.<br />
(b) Betrachte alle Knoten R außerhalb von B, die von<br />
Knoten in B direkt erreichbar sind. (R wird meist<br />
der Rand von B genannt.)<br />
(c) Bestimme den kürzesten Weg zu einem Knoten<br />
in R und erweitere B entsprechend.<br />
Unter welchen Bedingungen lassen sich (a)-(c)<br />
algorithmisch lösen? Was sind die Einzelschritte?<br />
Sei r ∈ R ein Randknoten und w 1 ,...,w r ∈ B alle<br />
Knoten mit (w i<br />
,r) ∈ E . Lässt sich damit der<br />
kürzeste Weg von s nach r bestimmen und seine<br />
Länge spl(s,r) ?<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
156
3. Ermitteln wichtiger Eigenschaften des Problems:<br />
Um unseren Ansatz umsetzen zu können, brauchen<br />
wir eine Eigenschaft, die es uns ermöglicht,<br />
B schrittweise um Randknoten zu erweitern.<br />
Verschärfung des Ansatzes:<br />
1. Bestimme für jeden Knoten r des Randes einen<br />
Vorgänger w r in B, so dass<br />
d(r) = spl( s, w ) + c((w ,r)) minimal ist.<br />
2. Wähle unter allen Knoten r von R denjenigen mit<br />
minimalem d(r) aus. Sei dieser Knoten mit p<br />
bezeichnet. Erweitere B um p.<br />
Behauptung:<br />
spl( s, w ) + c( (w ,p) ) = spl( s, p ) , d.h. der<br />
kürzeste Weg von s zu p wurde gefunden.<br />
Beweis:<br />
r<br />
p p<br />
Mit Induktion über den kürzesten Weg (siehe<br />
Vorlesung).<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
157<br />
r
<strong>4.</strong> Grobentwurf eines Algorithmus‘ mit Abstützung<br />
auf existierende Teillösungen:<br />
Wir setzen die obigen Ansätze in einen Grobentwurf<br />
um, der auf Dijkstra zurückgeht (vgl. Ottmann,<br />
Widmayer: 8.5.1):<br />
Jeder Knoten erhält drei zusätzliche Komponenten:<br />
pred: Vorgänger auf dem kürzesten „Rückweg“ zu s.<br />
dist: die kürzeste bisher ermittelte Entfernung zu s.<br />
inB: Ist genau dann true, wenn Knoten in der<br />
Menge ist, für die der kürzeste Weg bekannt ist.<br />
Algorithmus: kürzeste Wege in bewerteten<br />
Graphen G = (V,E) mit Bewertungsfunktion c.<br />
Startknoten ist s.<br />
// Initialisieren der Knoten und von B:<br />
for all v∈V\{s} do {<br />
v.pred = null;<br />
v.dist = ∞ ;<br />
v.inB = false ;<br />
}<br />
s.pred = s ;<br />
s.dist = 0 ;<br />
s.inB = true ;<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
158
Initialisieren des Randes R:<br />
R = ∅ ;<br />
// R initialisieren, d.h. die Nachfolger von s eintragen:<br />
ergänzeRand(s,R);<br />
// Auswählen von Knoten aus R und R ergänzen:<br />
while R != ∅ do {<br />
// wähle nächst gelegenen Randknoten aus:<br />
wähle v∈R mit v.dist minimal ;<br />
entferne v aus R ;<br />
v.inB = true ;<br />
ergänzeRand(v,R);<br />
}<br />
where<br />
procedure ergänzeRand( v, R ) {<br />
}<br />
for all (v,w)∈E do {<br />
}<br />
if not w.inB and<br />
}<br />
( v.dist + c((v,w)) < w.dist ){<br />
// w ist (kürzer) über v erreichbar<br />
w.pred = v ;<br />
w.dist = v.dist + c((v,w)) ;<br />
R = R ∪ {w} ;<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
159
5. Entwickeln bzw. Festlegen der Datenstrukturen:<br />
In dieser Phase ist zu entscheiden, welche<br />
Datenstrukturen für die Realisierung<br />
- des Graphen und<br />
- des Randes<br />
benutzt werden sollen.<br />
Benötigte Operationen auf der Graphdatenstruktur:<br />
- Iterieren über die Knotenmenge<br />
- Iterieren über die Kantenmenge zu einem Knoten<br />
- Bewertung der Kanten auslesen<br />
Benötigte Operationen auf dem Rand:<br />
- Rand als leer initialisieren<br />
- Prüfen, ob Rand leer ist<br />
- Wählen des Knotens mit minimaler Entfernung<br />
- Entfernen eines Knotens aus dem Rand<br />
- Knoten zum Rand hinzufügen bzw. Knoten im Rand<br />
modifizieren<br />
Als Graphdatenstruktur könnten z.B. Adjazenzlisten<br />
verwendet werden. Der Rand kann als Heap realisiert<br />
werden.<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
160
6. Ausarbeiten des Algorithmus:<br />
Aus den Entscheidungen der 5. Phase entsteht<br />
ein Feinentwurf, der präzise zu formulieren und,<br />
wo möglich, zu optimieren ist.<br />
Schließlich kann der Feinentwurf ausprogrammiert<br />
und getestet werden.<br />
Bemerkung:<br />
• Bis auf den letzten Schritt sind alle Phasen der<br />
Algorithmenentwicklung unabhängig von<br />
Programmiersprachen. Üblicherweise rechnet man<br />
die Algorithmenimplementierung auch nicht mehr<br />
zum Bereich Algorithmen und Datenstrukturen.<br />
• Softwareentwicklung im Allg. hat viele Parallelen<br />
zur Algorithmenentwicklung. Auch hier hat die<br />
Programmierung eine nachgeordnete Bedeutung.<br />
Dafür liegt der Schwerpunkt nicht so sehr<br />
auf der Lösung gut eingrenzbarer Probleme,<br />
sondern stärker auf der Bewältigung der vielen<br />
Aspekte und des Umfangs der Aufgabenstellung.<br />
0<strong>4.</strong>12.08 © A. Poetzsch-Heffter, TU Kaiserslautern<br />
161