17.12.2012 Aufrufe

4. Prozedurales Programmieren

4. Prozedurales Programmieren

4. Prozedurales Programmieren

MEHR ANZEIGEN
WENIGER ANZEIGEN

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

Hurra! Ihre Datei wurde hochgeladen und ist bereit für die Veröffentlichung.

Erfolgreich gespeichert!

Leider ist etwas schief gelaufen!