• Keine Ergebnisse gefunden

4.3 Algorithmen in prozeduraler Formulierung

N/A
N/A
Protected

Academic year: 2022

Aktie "4.3 Algorithmen in prozeduraler Formulierung"

Copied!
67
0
0

Wird geladen.... (Jetzt Volltext ansehen)

Volltext

(1)

4.3 Algorithmen in prozeduraler Formulierung

Dieser Abschnitt behandelt Algorithmen in proze- duraler Formulierung und deren Analyse bzgl.

- Speicherbedarf - Laufzeit

Er liefert einen Einblick in die Speicherverwaltung bei prozeduralen Programmen und gibt eine

Ausblick auf weitere algorithmische Problemstellungen und Methoden.

Vorgehen:

- Einführung in die Algorithmenanalyse - Speicherverwaltung

- Laufzeitverhalten

- Prozedurale Algorithmen und deren Analyse

- Klassifizierung und Entwicklung von Algorithmen

(2)

4.3.1 Einführung in die Algorithmenanalyse

Zwei zentrale Größen zur Beurteilung von Effizienz:

- Speicher(platz)bedarf (in kByte od. Speicherzellen) - Ausführungs-/Laufzeit (in Sekunden od. Anzahl

ausgeführter Operationen) Bei einem installierten Programm kann man

diese Größen zu gegebenen Eingaben messen bzw. berechnen.

Wir betrachten im Folgenden nur den Fall, dass alle Eingaben am Anfang der Ausführung erfolgen und alle Ausgaben am Ende.

Begriffsklärung: (Speicherbedarf/Laufzeit)

Sei P ein installiertes Programm und M die Menge der zulässigen Eingaben von P.

Der (max.) Speicherbedarf von P ist eine Funktion sb : M Æ kByte

Man spricht von der Raumkomplexität von P.

Die Laufzeit von P ist eine Funktion:

lz : M Æ Sekunden

Man spricht von der Zeitkomplexität von P.

(3)

Bemerkung:

In der Praxis ist die Bestimmung von Speicherbedarf und Laufzeit im Allg. nicht einfach:

1. Messen:

- kann nur endlich viele Funktionswerte liefern;

- wird ggf. durch Systemgegebenheiten beeinflusst.

2. Berechnen ausgehend vom Programm:

- setzt Kenntnisse der Übersetzung und des Systems voraus.

Beispiel: (Messen der Laufzeit)

fun primfaktoren (n:int) =

if n<0 then primfaktoren (~n) else if n<=1 then []

else primfakteinb n 2 and primfakteinb n f =

(* 2 <= f <= n *)

if n = f then [f]

else if n mod f = 0

then f::(primfakteinb (n div f) f) else primfakteinb n (f+1)

(4)

Ausgewählte Messergebnisse:

Eingabe Laufzeit in Sek.

Gemessen

- unter PolyML

- für Betriebssystem XX

- auf einem Rechner der Bauart YY.

12345678 < 1 10000000 < 1 123456789 < 1 100000000 < 1 1234567890 < 1 1000000000 < 1 12345678901 ~ 2 10000000000 < 1

123456789012 > 20000 100000000000 < 1

1234567890123 ~ 15 1000000000000 < 1

12345678901234 > 20000 10000000000000000000000 < 1

Beobachtung:

Laufzeit hängt im Allg. in komplexer Weise von der Eingabe ab.

(5)

Begriffsklärung: (Kriterien zur Effizienz)

Die Gesamteffizienz eines installierten Programms hängt ab von

- der Speicherbedarfsfunktion sb, - der Laufzeitfunktion lz und

- der Häufigkeit, mit der bestimmte Eingaben auftreten.

Bei häufig auftretenden Eingabewerten ist effizientes Verhalten wichtiger als bei seltenen Eingabewerten.

Bemerkung:

• Ein präziser Umgang mit dem obigen Effizienzbegriff ist in der Praxis schwierig:

- sb und lz sind kaum zu bestimmen;

- die Häufigkeitsverteilung der Eingaben ist oft nicht genau bekannt;

- meist möchte man die Effizienz eines Programms unabhängig von seiner Installation betrachten.

• Die Kriterien bilden aber die Grundlage für:

- den Effizienzvergleich von Programmen - die informelle Beurteilung von Effizienz - abstraktere Effizienzbegriffe

(6)

Abstraktere Effizienzbegriffe:

Statt ein installiertes Programm zu betrachten, sieht man üblicherweise von Details ab und betrachtet Effizienz nur näherungsweise.

Vereinfachungen:

1. Betrachte nicht die Eingabewerte selbst, sondern nur ihre Größe (z.B. Länge von Listen, Stellen- anzahl bei Zahlen).

2. Vernachlässige die Häufigkeitsverteilung der Daten.

3. Vernachlässige konstanten Aufwand, also

Aufwand, der unabhängig von den Eingabedaten entsteht.

4. Betrachte nur das Wachstum von sb und lz und vernachlässige konstante Faktoren (Abstraktion von der Leistung eines Rechners und den

Implementierungseigenschaften einer Programmier- sprache).

5. Betrachte nur obere und untere Schranken für sb und lz.

(7)

Beispiel: (Analyse der Laufzeit)

Wir analysieren eine Implementierung des Algorithmus

„Sortieren durch Auswahl“ (engl. selection sort) .

Eingabe: Feld f von ganzen Zahlen.

Aufgabe: Sortiere das Feld f aufsteigend.

Algorithmische Idee:

- Bestimme eine Komponente mit Index ixmin von f, die ein minimales Element von f[1] ... f[f.length]

enthält.

- Vertausche f[ixmin] und f[1].

- Sortiere dann den Bereich f[2] ... f[f.length] analog.

void main( String[] arg ) {

int[] feld = new int[arg.length];

for( int i = 0; i<feld.length; i++) { feld[i] = Integer.parseInt( arg[i] );

}

sortieren( feld );

for( int i = 0; i<feld.length; i++) { println( feld[i] );

} } }

Mögliche Hauptprozedur:

(8)

void sortieren(/*nonnull*/ int[] f) { bereichsort(f,0,f.length-1);

}

void bereichsort( int[] f, int ug, int og) { if (ug >= og) return;

int ixmin = auswaehlen(f,ug,og);

// Vertauschen int temp = f[ug];

f[ug] = f[ixmin];

f[ixmin] = temp;

// Sortieren des restlichen Felds:

bereichsort(f, ug+1, og);

}

/* Liefert Index mit minimalem Element im Bereich f[ug] .. f[og] von Feld f */

int auswaehlen( int[] f, int ug, int og) { int ixmin = ug;

for( int j = ug+1; j <= og; j++ ) { if( f[j] < f[ixmin] ) {

ixmin = j;

} }

return ixmin;

}

Rekursive Fassung des Sortierens durch Auswahl:

(9)

void sortieren( /*nonnull*/ int[] f ) { for( int i = 0; i<f.length-1; i++ ) {

// bestimme Komponente mit kleinstem // Element in f[i] ... f[f.length-1]

int ixmin = i;

for( int j = i+1; j<f.length; j++ ) { if( f[j] < f[ixmin] ) {

ixmin = j;

} }

// vertausche die Elemente an den // Positionen i und ixmin

int temp = f[i];

f[i] = f[ixmin];

f[ixmin] = temp;

} } }

Iterative Fassung des Sortierens durch Auswahl:

(10)

Analyse der Laufzeit von sortieren:

In Abhängigkeit von der Größe N des Feldes schätzen wir die Anzahl A(N) der Operationen/

Rechenschritte für den ungünstigsten Fall ab.

Eine Operation ist:

- ein Vergleich - eine Zuweisung

- eine Addition/Subtraktion Vereinfachende Annahme:

- Alle Operationen brauchen die gleiche Zeit.

- Andere Aspekte der Ausführung werden

vernachlässigt (Speicherverwaltung, Sprünge) Aufwand B(i,N) der inneren Schleife:

B(i,N) ≤ (2+1) + (N-i-1) * (2+2)

Aufwand A(N) des gesamten Rumpfes für N ≥ 2:

A(N) = 3 + Σ (1 + B(i,N) + 3 + 2 )

≤ 3 + Σ (9 + (N-i) * 4 ) = 3 + (N-1)*9 + 4* Σ i

= 9*N – 6 + 2*N*(N-1) = 2*N + 7*N – 6 A(0) = A(1) = 3

i=0 N-2

i=1 N-1

i=1 N-1

2

(11)

O-Notation

Häufig interessiert man sich nur für die Größenordnung des Wachstums der

Aufwandsfunktion A(N), die den Aufwand an Speicher bzw. Zeit in Abhängigkeit von der Problemgröße N beschreibt.

Begriffsklärung: (obere Schranke)

einer Aufwandsfunktion A, wenn gilt:

Es gibt c,d in Nat, sodass für alle N in Nat gilt:

A(N) ≤ c * f(N) + d

Man sagt auch, A wächst wie f bzw. ist von der Größenordnung f. Die Menge aller Funktionen

von der Größenordnung f bezeichnet man mit O(f) : O(f) = { g | ∃ c,d in Nat: ∀N in Nat: g(N) ≤ c* f(N) + d } .

Bemerkung:

• Entsprechend definiert man auch untere Schranken.

• Vertiefung in „Entwurf und Analyse von Algorithmen“

• Meist schreibt man O(N) statt O(λN.N), O(N ) statt O(λN.N ), O(log N) statt O(log), usw.

2 2

Eine Funktion f: Nat Æ

R

+ heißt obere Schranke

(12)

Der Zeitaufwand A vom Sortieren durch Auswahl ist

und damit in O(N ); denn mit c= 3 und d = 6 gilt:

A(N) ≤ 3 * N + 6 A(N) ≤ 2*N + 7*N - 62

Wichtige Komplexitätsklassen:

Beispiel: (Bestimmung oberer Schranken)

2

2

Kompl.klasse Bezeichnung Beispiel

O(1) konstant Hashverfahren

O(log N) logarithmisch binäre Suche in Bäumen O(N) linear sequentielle Suche

O(N * log N) n log n gute Sortierverfahren

O(N ) quadratisch einfache Sortierverfahren O(N ) kubisch Matrixmultiplikation

O(2 ) exponentiell Optimierungverfahren

2 3 N

Algorithmen mit einem Aufwand in O(N ) , k ≥ 2, nennt man polynomisch oder engl. polynomial.

k

(13)

Diskussion der O-Notation:

• Die O-Notation liefert eine grobe Klassifikation.

• In der Praxis können konstante Faktoren

und Programmiersprachen-spezifische Aspekte entscheidend sein (siehe Beispiel unten).

• Weitere Aspekte für die Effizienzbetrachtung:

- Antwortzeiten interaktiver Softwaresysteme - Kommunikationszeiten

Beispiel: (zur obigen Diskussion)

Wir betrachten zwei Versionen eines Programms, das eine Datei liest und wieder ausgibt.

Die Versionen illustrieren insbesondere:

• Abhängigkeit von programmiersprachlichen Aspekten (hier: Komplexität von scheinbaren

und wirklichen Grundoperationen)

• Notwendigkeit des Verständnisses technischer Aspekte zur Beurteilung nicht-funktionaler Eigen-

schaften

(14)

Abstrakte Schnittstelle zum Lesen von Dateien:

/* Ein Verbund vom Typ DateiLesePosition repräsentiert eine Position in einer Datei.

An dieser Position kann man das nächste Zeichen lesen.

*/

class DateiLesePosition { ...

}

/* Öffnet Datei mit Namen dname und liefert Referenz vom Typ DateiLesePosition, die die Anfangsposition in der Datei repräsentiert.

Gibt es Fehler beim Öffnen, wird null zurückgegeben.

*/

DateiLesePosition oeffneDatei( String dname ){

...

}

/* Ist die aktuelle Position am Dateiende, wird das EOT-Zeichen ‘\4‘ geliefert. Andernfalls wird das Zeichen an aktueller Position geliefert und die Position eins weiter geschaltet.

*/

char naechstesZeichen( DateiLesePosition d ) { ...

}

(15)

Ineffiziente Programmversion: Datei-Lesen Version 1 public static void main( String[] a ) {

if( arg.length == 1 ) {

DateiLesePosition dlp = oeffneDatei(a[0]);

if( dlp == null ) {

println("Fehler: Datei existiert nicht");

return;

}

String s = "";

char c = naechstesZeichen(dlp);

int count = 1;

while( c != '\4' ){ // '\4' ist EOT s = s + c;

c = naechstesZeichen(dlp);

count++;

if( count%1000==0 ) println(count);

}

println("Datei Inhalt:");

println( s );

} else {

println("Usage: java DateiLesen <file>");

} }

Bemerkung:

Obige Schnittstelle arbeitet nur für Dateien korrekt, die das EOT-Zeichen (end of transmission) nicht enthalten.

(16)

public static void main( String[] a ) { if( arg.length == 1 ) {

DateiLesePosition dlp = oeffneDatei(a[0]);

if( dlp == null ) {

println("Fehler: Datei existiert nicht");

return;

}

final int feldgroesse = 100000;

String s = "";

char[] cfeld = new char[feldgroesse];

char c = naechstesZeichen(dlp);

int count = 1;

int index = 0;

while( c != '\4' ){ // '\4' ist EOT cfeld[index] = c;

c = naechstesZeichen(dlp);

count++;

index++;

if( count%1000==0 ) println(count);

if( index == feldgroesse ) { index = 0;

s = s + new String(cfeld);

} }

println("Datei Inhalt:");

println( s );

} else {

println("Usage: java DateiLesen <file>");

Effizientere Programmversion: Datei-Lesen Version 2

(17)

Gemessene Zeit zum Lesen einer 150 kByte großen Datei auf einem AMD Opteron Dual Core 270

Rechner:

V1: 76,1 s V2: 0,4 s Grund:

„+“ auf String kopiert Argumente; dadurch ergibt sich insgesamt für V1 eine quadratische Laufzeit- komplexität in Abhängigkeit von der Seitengröße.

V2 hat zwar theoretisch die gleiche Komplexität, aber diese wirkt sich nur bei sehr großen Dateien aus.

Bemerkung:

Bei der Betrachtung der Komplexität vergisst man leicht, dass sich die Komplexitätsklassen nur auf das asymptotische Verhalten beziehen.

Für kleine N (die man in der Realität oft hat) kann das ganz anders aussehen.

(18)

4.3.2 Speicherverwaltung

Speicher ist eine wichtige Ressource für Softwaresysteme. Viele nicht-funktionale Eigenschaften hängen vom angemessenen Umgang mit Speicher ab.

Wir betrachten grundlegende Aspekte der Speicherverwaltung:

- Einführung: Speicher in der Programmierung - Automatische Speicherbereinigung

- Deallokation von Objekten

Einführung

Wir betrachten hier nur den Speicher, der für die Ausführung von Programmen benötigt wird, und zwar in Form eines Byte-adressierbaren virtuellen Adressraums.

Wichtige Fragen:

- Wofür wird Speicher benötigt?

- Wie ist der Speicher organisiert?

- Wie wird der Speicher verwaltet?

- Wie viel Speicher braucht ein Programm?

(19)

Wofür Speicher benötigt wird:

Beispiele: (Verwendung von Speicher)

Speicher wird benötigt für:

- das Programm (unabhängig von Eingabedaten) - Konstanten

- Variablen (global, prozedurlokal, objektlokal) - Verwaltung von Prozedur-/Methodenaufrufen

1. Programm, Konstanten und globale Variable:

Speicherbedarf ist relativ einfach zu bestimmen, da unabhängig von der Eingabe.

String s;

String ss;

public class SpeicherIllustration1 {

public static void main( String[] ins ) { s = ins[0] + " war die Eingabe";

ss = "Zu Seiteneffekten lesen "

+ "Sie die Dokumentation\n"

+ "und fragen Sie Ihren Tutor "

+ "oder Professor";

println( s + "\n" + ss );

} }

(20)

2. Verbundkomponenten/Objektlokale Variablen:

Speicherbedarf hängt von der Eingabe ab. Lebens- dauer der Verbunde erstreckt sich von der Erzeugung bis zum Ende des Programmablaufs.

class IntList { int fst;

IntList rst;

}

IntList empty() {

return new IntList();

}

IntList append( int i, IntList xl ) { IntList il = new IntList();

il.fst = i;

il.rst = xl;

return il;

}

public class SpeicherIllustration2 {

public static void main( String[] ins ) { IntList il = append(3,empty());

il = append( 134, il );

il = append( 9, il );

int i = Integer.parseInt( ins[0] );

while( i > 0 ) {

il = append( i, il );

i--;

} } }

(21)

3. Lokale Variablen und Prozeduraufrufverwaltung:

Speicherbedarf hängt von der Eingabe ab. Lebens- dauer der lokalen Variablen erstreckt sich vom

Prozeduraufruf bis zum Ende der Prozedur- ausführung.

Als Beispiel betrachten wir die rekursive Fassung des Sortierens durch Auswahl (vgl. Folie 370).

Speicher wird benötigt für jede Prozedurinkarnation (vgl. Begriffsklärung auf Folie 321) und zwar

- für den Rückgabewert und die aktuellen Parameter, - für die Aufrufverwaltung (z.B. Aufrufstelle),

- für die lokalen Variablen.

Speicherbereich für eine Prozedurinkarnation:

Rückgabewert

Aktuelle Parameter

Verwaltungsinformation Lokale Variable

(22)

Speicherorganisation:

Die Speicherorganisation ist bei den meisten

prozeduralen bzw. objektorientierten Programmier- sprachen ähnlich:

virtueller Adressraum

BS-Kern

Programm

Halde

Laufzeitkeller

globale Größen globale, statische

Variablen, Konstanten, ...

Zwischenergebnisse, prozedurlokale

Größen, Objekte mit be- schränkter Lebensdauer (dynamische) Verbunde, Objekte, ...

(23)

Wie Speicher verwaltet wird:

1. Globaler Speicher und Keller werden automatisch verwaltet (der Übersetzer erzeugt dafür Code).

2. Je nach Programmiersprache wird die Halde (engl. heap) unterschiedlich verwaltet:

- mit automatischer Speicherbereinigung, - durch den Programmierer (Deallokation).

Operationen zur Verwaltung der Halde:

Æ Anfordern von Speicher bei Objekterzeugung:

liefere Speicherbereich ausreichender Größe.

Æ Freigabe von Speicher:

- Wenn kein Speicher mehr verfügbar, gebe die Speicherbereiche von Objekten frei, die nicht mehr erreichbar sind.

- Gebe Speicher von Objekten auf Anweisung des Programms frei (Deallokation).

Beachte:

Die Speicherverwaltung kostet auch Laufzeit.

(24)

Fazit:

Automatische Speicherbereinigung

1. Wie viel Speicher ein Programm in Abhängigkeit von der Eingabe genau braucht, hängt von Details der Sprachimplementierung und Plattform ab.

2. Mit einem generellen Verständnis der relevanten Techniken lässt sich der Speicherbedarf aber gut abschätzen.

Begriffsklärung: (Autom. Speicherbereinigung)

Verfahren zur automatischen Speicherbereinigung (engl. automatic garbage collection) ermitteln periodisch oder bei Bedarf, welche Objekte nicht mehr erreichbar (s.u.) sind und geben deren Speicherplatz frei.

Weiteres Ziel ist es, den freien Speicher zu kompaktifizieren.

Immer mehr Programmiersprachen bieten

automatische Speicherbereinigung (insbesondere funktionale, logische und objektorientierte Sprachen):

• Vereinfachung der Programmierung

• Aufwand an Speicher und Zeit ist vertretbar.

• Sicherheitsaspekte

(25)

Beispiel: (Autom. Speicherbereinigung)

void main( String[] ins ) { int count = 1;

while( true ) {

int[] feld = new int[1000000];

println(count++);

} }

1. Programm, das unerreichbare Objekte erzeugt:

Dies Programm bekommt keine Speicherprobleme.

2. Programm, dessen erzeugte Objekte erreichbar sind:

Dies Programm terminiert mit OutOfMemoryError.

class ListOfArray { int[] elem;

ListOfArray next;

}

void main( String[] ins ) { ListOfArray la = null;

int count = 1;

while( true ) {

ListOfArray tmp = new ListOfArray();

tmp.elem = new int[1000000];

tmp.next = la;

la = tmp;

println(count++);

} }

(26)

Begriffsklärung: (Erreichbarkeit)

Ein Verbund bzw. Objekt X heißt von einer Variablen v direkt erreichbar, wenn v eine Referenz auf X enthält.

X heißt von v erreichbar, wenn es von v direkt

erreichbar ist oder wenn es einen Verbund/ ein Objekt Y mit Komponente w gibt, so dass X von w direkt

erreichbar ist und Y von v erreichbar ist.

Die Menge der Wurzelvariablen zu einem Ausfüh- rungszustand A umfasst alle globalen Variablen sowie die aktuell im Keller vorhandenen lokalen Variablen und Parameter.

Ein Objekt heißt erreichbar in einem Ausführungs- zustand A, wenn es von einer Wurzelvariablen zu A erreichbar ist.

Verbunde/Objekte können nur von Wurzelvariablen oder von Verbundkomponenten/Instanzvariablen referenziert werden.

Bemerkung:

(27)

Deallokation von Verbunden/Objekten

Begriffsklärung: (De-/Allokation)

Die Bereitstellung des Speicherbereichs bei der Erzeugung von Verbunden und Objekten nennt man Allokation (engl. allocation). Die Freigabe solcher Speicherbereiche Deallokation (engl.

deallocation).

Die meisten prozeduralen Programmiersprachen

unterstützen De-/Allokation durch den Programmierer:

• Vorteil:

- ermöglicht effiziente Benutzung von Speicher

• Nachteile:

- zusätzlicher Programmieraufwand - potentielle Fehlerquelle

- führt leicht zu Sicherheitslücken

Wir betrachten hier De-/Allokation von Objekten in C++.

(28)

Verbunde und Zeiger in C++:

C++ unterscheidet zwischen Verbunden und und Zeigern/Referenzen auf Verbunde.

#include <iostream>

class Punkt { public:

int x;

int y;

};

Punkt* puenktchen() {

Punkt* p = new Punkt();

p->x = 1;

p->y = 2;

Punkt q;

q.x = 3;

q.y = 4;

return p;

}

int main() {

Punkt* r = puenktchen();

cout << "X-Koordinate: " << r->x << '\n';

cout << "Y-Koordinate: " << r->y << '\n';

return 0;

Beispiel: (Verbunde und Zeiger)

(29)

Wie in Java, alloziert der Operator „new“ in C++

Speicher für neue Verbunde. Als Ergebnis

liefert er einen Zeiger auf den neuen Verbund.

Ist K ein Verbundtyp, dann bezeichnet in C++

K* den Typ der Zeiger auf Verbunde vom Typ K.

Den Speicherplatz, den man mit new alloziert hat,

kann man durch Aufruf des Operators delete wieder freigeben, wenn er nicht mehr gebraucht wird.

// ... wie auf Folie 390 int main() {

Punkt* r = puenktchen();

cout << "X-Koordinate: " << r->x << '\n';

cout << "Y-Koordinate: " << r->y << '\n';

delete r;

return 0;

}

Beispiel: (Verbunde und Zeiger, Fortsetzung)

(30)

Zum Vergleich mit Java (s. Folie 387) betrachten wir zwei Varianten eines C++ Programms mit und ohne Deallokation. Sei Klasse Vektor gegeben:

Beispiel: (Wirkung der Deallokation)

class Vektor { public:

int elems [1000000];

};

int main() {

while( true ) {

Vektor* vp = new Vektor();

}

return 0;

}

Folgendes Programm führt zu einem Abbruch wegen Speicherüberlaufs:

Deallokation der Vektorverbunde verhindert den Speicherüberlauf:

int main() {

while( true ) {

Vektor* vp = new Vektor();

// mache irgendwas mit dem Vektor:

delete vp;

}

return 0;

}

(31)

Verwendung von Deallokation:

Ein Programmteil P kann einen Speicherbereich freigeben, wenn:

- P den Speicherbereich nicht mehr benötigt,

- P den Speicherbereich kontrolliert, d.h. sicher sein kann, dass er von keiner anderen Stelle benötigt wird.

Weitere Aspekte der Deallokation:

• Deallokation wird in einigen Sprachen durch weitere Sprachmittel unterstützt (z.B. Destruktoren in C++).

• Deallokation und automatische Speicherbereinigung lassen sich kombinieren:

- Anweisungen an den Garbage Collector - Soft und weak references in Java

• Deallokation bezieht sich nicht nur auf Speicher-, sondern auch auf andere Ressourcen.

• Ein systematischer Umgang mit einer Ressource bedeutet zu klären,

- wer die Ressource kontrollieren soll, - wer Zugriff auf die Ressource erhält.

(32)

4.3.3 Laufzeitverhalten

Dieser Abschnitt ergänzt die in 4.3.1 vorgestellten Aspekte zum Laufzeitverhalten.

Das Laufzeitverhalten eines Programms wird bestimmt durch:

- die Anweisungen des Programms - den Übersetzer

- die Laufzeitumgebung, insbesondere die die Speicherverwaltung

- die Systemumgebung.

Speicherverwaltung kostet Zeit:

Speicherverwaltung ist aufwendig, wenn der ver- fügbare Speicher knapp wird:

- Garbage Collector muss häufig aufgerufen werden.

- Das Aufsuchen ausreichend großer freier Speicher- bereiche wird aufwendiger.

(33)

Systemumgebung beeinflusst das Laufzeitverhalten:

Insgesamt ist der Aufwand in der Praxis nicht leicht abzuschätzen, weil

- der Speicherverbrauch der Bibliotheksklassen und anderer fremder Programmteile häufig nicht klar spezifiziert ist;

- die Details der Speicherverwaltung eine wichtige Rolle spielen.

Problematisch ist das insbesondere bei Echtzeit- anforderungen.

Zur Gesamtbeurteilung des Laufzeitverhaltens eines Softwaresystems muss auch die Systemumgebung berücksichtigt werden:

- Benutzerinteraktion - Anzahl von Benutzern - Kommunikationszeiten

- Laufzeitverhalten der Plattform - Interaktion mit anderen Systemen

(34)

Fazit:

- Präzise Bestimmung der Effizienz ist im Allg.

schwierig und von vielen technischen Aspekten abhängig; aber auch nur bei ausgewählten

Anwendungen nötig.

- Durch geeignete Abstraktion kann man nachvoll- ziehbare Aussagen über die Effizienz eines

Algorithmus‘, Programms oder Softwaresystems machen.

(35)

Vorgehen:

Wir betrachten prozedurale Formulierungen und die Analyse von drei Sortieralgorithmen:

- Sortieren durch Einfügen - Quicksort

- Heapsort

Bei allen Algorithmen gehen wir davon aus, dass die zu sortierenden Daten in einem Feld vorliegen, das verändert werden darf.

4.3.4 Prozedurale Algorithmen und deren Analyse

class DataSet { int key;

String data;

}

DataSet mkDataSet( int k, String s ) { DataSet ds = new DataSet();

ds.key = k;

ds.data = s;

return ds;

Datensätze stellen wir durch folgenden Datentypen dar:

(36)

Bemerkung:

Beachte bei den folgenden Beispielen:

1. Die algorithmische Grundidee ist unabhängig vom verwendeten Programmierparadigma.

2. Die Verwendung von Feldern statt Listen kann die Komplexität ändern.

Sortieren durch Einfügen

Algorithmische Grundidee:

Sortiere zunächst eine Teilliste (Terminierungsfall:

leere Liste). Füge dann die verbleibenden Elemente nacheinander in die bereits sortierte Teilliste ein.

fun sortieren nil = nil

| sortieren (x::xl) =

einfuegen x (sortieren xl) and einfuegen x [] = [x]

| einfuegen (kx,sx) ((ky,sy)::yl) = if kx <= ky

then (kx,sx)::(ky,sy)::yl

else (ky,sy)::(einfuegen (kx,sx) yl) Funktionale Fassung:

(37)

Nachteil der rekursiven Fassung:

- Aufwand durch Listendarstellung - Aufwand durch rekursive Aufrufe

Ideen zur prozeduralen Realisierung:

- Speichere die Datensätze in einem Feld

- Realisiere das Einfügen durch schrittweises Verschieben (ausgehend vom größten Element) - Eliminiere die Rekursion durch Beginn mit der

einelementigen Liste in die nacheinander Elemente eingefügt werden.

sortiert unsortiert einfügen

(38)

Prozedurale Fassung in Java:

void sortieren(/*nonnull*/ DataSet[] f) { DataSet tmp; // einzufuegender Datensatz for( int i = 1; i<=(f.length-1); i++) {

int j = i;

tmp = f[j];

// Finde neue Position fuer // aktuellen Datensatz tmp

while( j>=1 && f[j-1].key > tmp.key ) { // Verschiebe groessere Saetze mit // groesseren Schluesseln

f[j] = f[j-1];

j--;

}

// Setze tmp an neue Position f[j] = tmp;

} }

public static void main( String[] arg ) { DataSet[] feld = new DataSet[arg.length];

for( int i = 0; i<feld.length; i++ ) { feld[i] = mkDataSet(

Integer.parseInt(arg[i]),arg[i]);

}

sortieren( feld );

for( int i = 0; i<feld.length; i++) { println( feld[i].key );

} } }

(39)

Laufzeitabschätzung:

Günstigster Fall:

Liste ist bereits aufsteigend sortiert.

Æ pro Schleifendurchlauf ein Schlüsselvergleich Æ pro Durchlauf zwei Datensatzzuweisungen Schlüsselvergleiche: C (N) = N -1;

Datensatzzuweisungen: M (N) = 2*(N –1);

Wir betrachten die Anzahl der Schlüsselvergleiche C und der Zuweisungen M von Datensätzen in Abhängigkeit von der Anzahl N der Datensätze.

min min

Ungünstigster Fall:

Liste ist absteigend sortiert.

Æ pro Schleifendurchlauf i Schlüsselvergleiche Æ pro Durchlauf (i+2) Datensatzzuweisungen Schlüsselvergleiche: C (N) = Σ i ∈ O(N )

Datensatzzuweisungen: M (N) = Σ (i+2) ∈ O(N )

max

max i=1

N-1 i=1

N-1 2

2

Durchschnitt:

Im Durchschnitt ergibt sich quadratische Komplexität.

(40)

Quicksort

fun qsort [] = nil

| qsort ((pk,ps)::rest) =

let val (below,above) = split pk rest in qsort below @[(pk,ps)]@ qsort above

end

and split p [] = ([],[])

| split p ((xk,xs)::xr) =

let val (below, above) = split p xr in if xk < p then ((xk,xs)::below,above)

else (below,(xk,xs)::above) end

Funktionale Fassung:

Algorithmische Grundidee:

• Wähle einen beliebigen Datensatz mit Schlüssel k aus, das sogenannte Pivotelement.

• Teile die Liste in zwei Teile:

- 1. Teil enthält alle Datensätze mit Schlüsseln < k - 2. Teil enthält die Datensätze mit Schlüsseln ≥ k

• Wende quicksort rekursiv auf die Teillisten an.

• Hänge die resultierenden Listen und das Pivotelement zusammen.

(41)

Umsetzung in prozedurale Fassung:

- Speichere die Datensätze in einem Feld und bearbeite rekursiv Teilbereiche des Feldes

- Realisiere das Teilen der Liste durch Vertauschen:

Æ Indexzähler left, right laufen von links bzw.

rechts bis f[left].key ≥ pivot.key

&& f[right].key < pivot.key Es gilt:

Für alle i in [ug,left-1] : f[i].key < pivot.key Für alle i in [right+1,og] : pivot.key ≤ f[i].key 1. Fall:

left > right : Teilung vollzogen:

2. Fall:

left ≤ right : Vertausche f[left] und f[right],

inkrementiere left und right und fahre fort.

left right

ug og

right

ug og

(42)

Prozedurale Fassung in Java:

void sortieren(/*nonnull*/ DataSet[] f) { quicksort(f,0,f.length-1);

}

void quicksort( DataSet[] f, int ug, int og){

if( ug < og ) {

int ixsplit = partition(f,ug,og);

/* ug <= ixsplit <= og

&& f[ixsplit].key == pivotKey

&& ( fuer alle i: ug<=i<ixsplit

==> f[i].key<=pivotKey )

&& ( fuer i: ixsplit<i<=og

==> f[i].key>=pivotKey )

*/

quicksort( f, ug, ixsplit-1 );

quicksort( f, ixsplit+1, og );

} }

int partition( DataSet[] f, int ug, int og){

... // siehe naechste Folie }

public static void main( String[] arg ) { ... // siehe Folie 400

}

(43)

int partition( DataSet[] f, int ug, int og){

DataSet dtmp;

int left = ug;

int pk = f[og].key;

int right = og-1;

boolean b = true;

while( b ) {

while( f[left].key < pk ) { left++; }

while( left<=right && f[right].key>=pk ){

right--; }

if( left > right ) { b = false;

} else {

dtmp = f[left];

f[left] = f[right];

f[right] = dtmp;

left++;

right--;

} }

dtmp = f[left];

f[left] = f[og];

f[og] = dtmp;

return left;

}

(44)

Grobe Laufzeitabschätzung von Quicksort:

Ungünstigster Fall:

Beim Zerlegen der Listen ist jeweils eine der Teillisten leer. Dann hat der Aufrufbaum die Tiefe N, also gilt:

C (N) = N * C (N) = O(N ) M (N) = N * M (N) = O(N )

Seien C, M und N definiert wie auf Folie 401.

part part

Vorüberlegung:

Betrachte die Ebenen gleicher Tiefe im Aufrufbaum von quicksort. Das Zerlegen aller Teillisten auf einer Ebene verursacht schlimmstenfalls linearen Aufwand:

C (N) = O(N) M (N) = O(N)

max max

part part

2 2

Günstigster Fall:

Beim Zerlegen der Liste entstehen jeweils zwei etwa gleich große Teillisten. Dann hat der Aufrufbaum die Tiefe log N, also gilt:

C (N) = log N * C (N) = O(N log N) M (N) = log N * M (N) = O(N log N)

min min

part part

(45)

Bemerkung:

• Die mittlere Laufzeit von Quicksort ist auch von der Größenordnung O(N log N) (siehe Ottmann, Widmayer: Abschn. 2.2)

• Die vorgestellte Quicksort-Fassung arbeitet schlecht auf schon sortierten Listen.

• Verbesserungen der vorgestellten Variante ist möglich durch geeignetere Auswahl des Pivot- elementes und durch Elimination der Rekursion.

Heapsort

Zur Einführung siehe Folie 168ff. Zur Erinnerung:

Heap wird verwendet, um schnell einen Datensatz mit maximalem Schlüssel zu finden.

Algorithmische Idee:

• 1. Schritt: Erstelle den Heap zur Eingabefolge.

• 2. Schritt:

- Entferne Maximumelement aus Heap ( O(1) ) und hänge es vorne an die schon sortierte Liste.

- Stelle Heap-Bedingung wieder her ( O(log N) ).

(46)

Prozedurale Datenstruktur für fast vollstän- dige, markierte, indizierte Binärbäume:

Vorgehen:

- Entwicklung einer prozeduralen Datenstruktur FVBinTree für fast vollständige Binärbäume - Heapsort unter Nutzung von FVBinTree

- Elimination der Schnittstelle

class FVBinTree { DataSet[] a;

int currsize;

}

/* f ist nicht null; uebernimmt f, d.h.

Modifikationen an dem Ergebnis ändern moeglicherweise auch f

*/

FVBinTree mkFVBinTree( DataSet[] f ){

FVBinTree t = new FVBinTree();

t.a = f;

t.currsize = f.length;

return t;

}

/* liefert Groesse von t; lesend */

int size( FVBinTree t ){ return t.currsize; }

(47)

/* lesend */

DataSet get( FVBinTree t, int ix ) { return t.a[ix];

}

/* modifizier t */

void swap( FVBinTree t, int ix1, int ix2 ) { DataSet dtmp = t.a[ix1];

t.a[ix1] = t.a[ix2];

t.a[ix2] = dtmp;

}

/* modifizier t */

void removeLast( FVBinTree t ){t.currsize--;}

/* lesend */

boolean hasLeft( FVBinTree t, int ix ){

return left(t,ix) < t.currsize;

}

/* lesend */

boolean hasRight( FVBinTree t, int ix ) { return right(t,ix) < t.currsize;

}

/* lesend */

int left( FVBinTree t, int ix ) { return 2*(ix+1)-1;

}

/* lesend */

int right( FVBinTree t, int ix ) { return 2*(ix+1);

(48)

Bemerkung:

Bei einer prozeduralen Datenstruktur muss man sich genau merken, welche Operationen

- Referenzen übernehmen bzw.

- Änderungen vornehmen.

/* Stellt Heap-Eigenschaft her, wobei die Kinder des Knotens ix die Eigenschaft bereits erfuellen muessen; modifiziert t

*/

void heapify( FVBinTree t, int ix ) { int ixk = get(t,ix).key;

if( hasLeft(t,ix) && !hasRight(t,ix) ) { int lx = left(t,ix);

if( ixk < get(t,lx).key ) { swap(t,ix,lx);

}

} else if( hasRight(t,ix) ) { int lx = left(t,ix);

int rx = right(t,ix);

int largerChild =

get(t,lx).key > get(t,rx).key ? lx : rx;

if( ixk < get(t,largerChild).key ) { swap( t, ix, largerChild );

heapify( t, largerChild );

}

(49)

void sortieren(/*nonnull*/ DataSet[] f) { FVBinTree t = mkFVBinTree( f );

// Herstellen der Heap-Bedingung

for( int i = size(t)/2 - 1; i >= 0; i-- ){

heapify(t,i);

}

// Sortieren

while( size(t) > 0 ) {

swap( t, 0, size(t)-1 );

removeLast(t);

heapify(t,0);

} }

void swap( DataSet[] f, int i1, int i2 ){

DataSet dtmp = f[i1];

f[i1] = f[i2];

f[i2] = dtmp;

In einem Optimierungsschritt:

- Eliminieren wir den Datentyp FVBinTree und

arbeiten direkt auf dem übergebenen Feld, wobei wir die aktuelle Größe in einer lokalen Variable speichern.

- Ersetzen wir die Operationen der Datenstruktur durch deren Rümpfe.

- Benutzen wir eine swap-Prozedur für Felder:

(50)

/* Kommentar siehe oben; modifiziert f

*/

void heapify( DataSet[] f, int size, int ix ){

int ixk = f[ix].key;

int rx = 2*(ix+1);

int lx = rx – 1;

if( lx < size && rx >= size ) { if( ixk < f[lx].key ) {

swap(f,ix,lx);

}

} else if( rx < size) { int largerChild =

f[lx].key > f[rx].key ? lx : rx;

if( ixk < f[largerChild].key ) { swap(f,ix,largerChild);

heapify( f, size, largerChild );

} } }

void sortieren(/*nonnull*/ DataSet[] f) { int size = f.length;

// Herstellen der Heap-Bedingung

for( int i = size/2 - 1; i >= 0; i-- ) { heapify(f,size,i);

}

// Sortieren

while( size > 0 ) { size--;

swap( f, 0, size );

heapify(f,size,0);

} }

(51)

Grobe Laufzeitabschätzung von Heapsort:

Ungünstigster Fall:

1. Herstellen der Heap-Eigenschaft:

Bezeichne j die Anzahl der Niveaus im Heap, also 2 ≤ N ≤ 2 -1.

Dann gibt es auf Niveau k höchstens 2 Schlüssel und C und M sind proportional zu j-k .

Insgesamt gilt dann für die Anzahl der Operationen zur Herstellung der Heap-Eigenschaft:

Σ 2 (j-k) = Σ i * 2 = 2 * Σ ≤ N*2 ∈ O(N)

2. Auswahl des Wurzelelements und Versickern:

Da die Höhe eines fast vollständigen Binärbaums von Ordnung O(log N) ist, führt heapify O(log N) Operationen aus. Damit ergibt sich für diese Teile die Komplexität O(N log N).

3. Komplexität des gesamten Algorithmus:

O(N) + O(N log N) = O(N log N)

Seien C, M und N definiert wie auf Folie 401. Wir betrachten nur den ungünstigsten Fall.

max

i

j-1 j

k-1 max

k=1

j-1 k-1

i=1

j-1 j-i-1 j-1 i=1

j-1 i 2

(52)

Sortierverfahren

Übersicht über die Komplexität von Sortierverfahren:

intern (im HSP) extern (nicht im HSP)

Auswählen Einfügen Bubblesort Shellsort

Baumsortierung Quicksort

O(N )

k ≤ 2

O( N log N) k 2

Baumsortierung (AVL) Heapsort

Mergesort

Mergesort

(53)

4.3.5 Algorithmenklassen & -entwicklung

Dieser Abschnitt skizziert:

• wichtige weitere Problem- und Algorithmenklassen

• einen Weg zur Entwicklung von Algorithmen anhand eines Beispiels

Problem- und Algorithmenklassen

Neben dem klassischen Bereichen des Sortierens und Suchens von Datensätzen gibt es eine Vielzahl von Algorithmen für unterschiedliche Aufgaben-

und Problembereiche.

Beispiele: (Algorithmische Probleme)

• Optimaler Einsatz der Flugzeugflotte einer Fluggesellschaft.

• Ermittlung der Schnittfläche zweier Flächen gegeben durch ihre Punkte

• Erfüllbarkeit/Allgemeingültigkeit logischer Formeln.

• Auffinden aller Web-Seiten, die eine Menge von Schlüsselwörter enthalten

(54)

Klassifikation der Algorithmen gemäß:

• verwendeter Datenstrukturen

• algorithmischer Kriterien (z.B. Art der Parallelität)

• spezieller Aufgabenbereiche

Datenstrukturen:

Mengen, Listen, Warteschlangen, etc.:

Ziele:

Effiziente Speicherung und effiziente Operationen zum Einfügen, Suchen und Löschen.

Zeichenreihen, Textsuche:

Ziele:

Effiziente Suche von Wort- oder Textmustern in Texten.

Beispiel:

Finde alle Vorkommen von „S%Haffner“ in den letzten 5 Jahrgängen der Frankfurter Allgemeinen Zeitung.

(55)

Begriffsklärung: (Graph)

Ein gerichteter Graph (engl. digraph) G = (V, E) besteht aus

- einer endlichen Menge V von Knoten (engl. vertices) - einer Menge E ⊆ VxV von Kanten (engl. edges)

Ist (va,ve) eine Kante, dann nennt man

- va den Anfangs- oder Startknoten oder die Quelle - ve den Endknoten oder das Ziel

der Kante. ve heißt von va direkt erreichbar und Nachfolger von va; va Vorgänger von ve.

Graphen:

Graphen bieten für eine große Klasse von Problemen ein geeignetes abstraktes Modell.

Beispiele:

- Was ist die beste Verbindung von A nach B?

- Wie transportiere ich Waren von mehreren Anbie- tern am billigsten zu mehreren Nachfragern?

- Wie gestalte ich einen Arbeitsablauf mit mehreren Maschinen und Arbeitskräften optimal?

- Welche Wassermenge kann maximal durch die

(56)

Speicherdarstellungen von Graphen:

Sei G = (V,E) ein gerichteter Graph, V = {1,...,n}.

G lässt sich speichern als:

- Adjazenzmatrix: boolesche nxn-Matrix, wobei das Element (x,y) true ist genau dann, wenn es in G eine Kante von x nach y gibt.

- Adjazenzlisten: Speichere für jeden Knoten die Liste der durch eine Kante erreichbaren Knoten.

Algorithmische Kriterien:

Wir haben bisher nur sequentielle Algorithmen betrachtet, deren Daten alle im Hauptspeicher Platz finden. In der Praxis sind häufig komplexere Anforderungen zu berücksichtigen:

- Daten auf anderen Speichermedien ohne wahlfreies Zugriffsverhalten

- Parallelisierung für gegebene Rechner, um

akzeptable Antwortzeiten zu erhalten bzw. große Datenmengen rechnen zu können.

- Arbeiten mit verteilten, sich dynamisch entwickelnden Daten

(57)

Viele Teilgebiete der Informatik und anderer Fächer haben mittlerweile für ihre speziellen Aufgaben

umfangreiches algorithmisches Wissen erarbeitet.

Zwei Beispiele:

- Algorithmische Geometrie

- Übersetzertechnik/Compilerbau

Aufgabenbereiche:

Algorithmische Geometrie:

Beispielproblem:

Gegeben eine Menge von Rechtecken; ermittle alle Paare von Rechtecken, die sich schneiden.

Anwendungsbereiche:

• Computergraphik, Visualisierung

• Geometrische Modellierung, CAD

• Schaltungsentwurf

• Wegeplanung von Robotern

(58)

Übersetzertechnik:

Aufgabenbereiche:

Parsen gemäß einer kontextfreien Grammatik:

- Eingabe: Zeichenreihe (Programm) - Ausgabe: Syntaxbaum

• Optimierende Übersetzung, zum Beispiel:

- Konstante Ausdrücke zur Übersetzungszeit berechnen

- Prozeduraufrufe durch ihre Rümpfe ersetzen - Speicherbedarf verringern

Beispiel: (Konstantenfaltung)

Übersetze das Programmfragment int a = 7;

int b = a * 3;

int c = a + b;

so als hätte der Programmierer geschrieben:

int a = 7;

int b = 21;

int c = 28;

(59)

Beispiel: (Escape-Analysis)

Aufgabe:

Ermittele die Objekte, die auf dem Keller alloziert werden können, da ihre Referenzen den Methoden- aufruf, der sie erzeugt hat, nicht verlassen.

Beispielfragment:

void m( String s ) {

String t = "" + s ; // neuer Verbund

t = doSomething(t); // Modifikation von t println(t);

}

Der/das von t referenzierte Verbund/Objekt könnte auf dem Keller verwaltet werden.

Ziel:

Entlastung der Speicherbereinigung.

(60)

Phasen der Algorithmenentwicklung:

1. Problemabstraktion und -formulierung 2. Entwickeln einer algorithmischen Idee

3. Ermitteln wichtiger Eigenschaften des Problems 4. Grobentwurf eines Algorithmus‘ mit Abstützung

auf existierende Teillösungen

5. Entwickeln bzw. Festlegen der Datenstrukturen 6. Ausarbeiten des Algorithmus

Algorithmenentwicklung an einem Beispiel:

0. Problem:

Routenplaner für Fahrradfahrer in einer Großstadt:

Wie ist die beste Verbindung zwischen zwei Straßenkreuzungen?

Wir erläutern die Phasen der Algorithmenentwicklung an einem Beispiel (vgl. Phasen der Software-

entwicklung).

Algorithmenentwicklung

Abschließend zu 4.3 betrachten wir wichtige Phasen der Algorithmenentwicklung an einem Beispiel.

(61)

- Straßenkreuzungen entsprechen Knoten

- Kante entspricht einer direkten Straßenverbindung zwischen Kreuzungen A und B, die von A nach B befahrbar ist (ggf. auch Kante für umgekehrte Richtung).

- Jede Kante bekommt als Bewertung die Zeit in Sekunden, die man im Durchschnitt für den Weg von A nach B braucht.

Die Bewertung ist eine Funktion c: E Æ R Bewertete gerichtete Graphen nennt man Distanzgraphen.

Damit lässt sich das Problem wie folgt formulieren:

- Gegeben ein Distanzgraph, der die Straßenverbin- dungen modelliert, sowie zwei Knoten s und z.

- Gesucht ist ein Weg s, v1, ... , vn , z mit minimaler Länge lg :

lg = c( (s,v1) ) + c( (v2,v3) ) + ... + c( (vn,z) ) 1. Problemabstraktion und -formulierung:

Modelliere die Straßen und Wege durch einen gerichteten Graphen mit bewerteten Kanten:

+

(62)

Eine verbreitete Strategie zur Algorithmenentwicklung versucht ein Problem auf ähnlich geartete

Teilprobleme zu reduzieren. Hier:

Reduziere die Suche des kürzesten Wegs von s nach z auf kürzeste Wege zwischen anderen Knotenpaaren.

Ansatz:

(a) Errechne schrittweise Knotenmengen B, sodass der kürzeste Weg von s zu allen Knoten von B bekannt ist. Anfangs ist B = { s }.

(b) Betrachte alle Knoten R außerhalb von B, die von Knoten in B direkt erreichbar sind. (R wird meist der Rand von B genannt.)

(c) Bestimme den kürzesten Weg zu einem Knoten in R und erweitere B entsprechend.

2. Entwickeln einer algorithmischen Idee:

Unter welchen Bedingungen lassen sich (a)-(c) algorithmisch lösen? Was sind die Einzelschritte?

Sei r ∈ R ein Randknoten und w ,...,w ∈ B alle Knoten mit (w ,r) ∈ E . Lässt sich damit der

kürzeste Weg von s nach r bestimmen und seine Länge spl(s,r) ?

1 r

i

(63)

3. Ermitteln wichtiger Eigenschaften des Problems:

Um unseren Ansatz umsetzen zu können, brauchen wir eine Eigenschaft, die es uns ermöglicht,

B schrittweise um Randknoten zu erweitern.

Verschärfung des Ansatzes:

1. Bestimme für jeden Knoten r des Randes einen Vorgänger w in B, so dass

d(r) = spl( s, w ) + c((w ,r)) minimal ist.

2. Wähle unter allen Knoten r von R denjenigen mit minimalem d(r) aus. Sei dieser Knoten mit p

bezeichnet. Erweitere B um p.

Behauptung:

spl( s, w ) + c( (w ,p) ) = spl( s, p ) , d.h. der kürzeste Weg von s zu p wurde gefunden.

Beweis:

Mit Induktion über den kürzesten Weg (siehe Vorlesung).

r r

p p

r

(64)

4. Grobentwurf eines Algorithmus‘ mit Abstützung auf existierende Teillösungen:

Algorithmus: kürzeste Wege in bewerteten Graphen G = (V,E) mit Bewertungsfunktion c.

Startknoten ist s.

// Initialisieren der Knoten und von B:

for all v∈V\{s} do { v.pred = null;

v.dist = ∞ ;

v.inB = false ; }

s.pred = s ; s.dist = 0 ;

s.inB = true ;

Wir setzen die obigen Ansätze in einen Grobentwurf um, der auf Dijkstra zurückgeht (vgl. Ottmann,

Widmayer: 8.5.1):

Jeder Knoten erhält drei zusätzliche Komponenten:

pred: Vorgänger auf dem kürzesten „Rückweg“ zu s.

dist: die kürzeste bisher ermittelte Entfernung zu s.

inB: Ist genau dann true, wenn Knoten in der

Menge ist, für die der kürzeste Weg bekannt ist.

(65)

// Initialisieren des Randes R:

R = ∅ ;

// R initialisieren, d.h. die Nachfolger von s eintragen:

ergänzeRand(s,R);

// Auswählen von Knoten aus R und R ergänzen:

while R != do {

// wähle nächst gelegenen Randknoten aus:

wähle v∈R mit v.dist minimal ; entferne v aus R ;

v.inB = true ; ergänzeRand(v,R);

}

where

procedure ergänzeRand( v, R ) { for all (v,w)∈E do {

if not w.inB and

( v.dist + c((v,w)) < w.dist ){

// w ist (kürzer) über v erreichbar w.pred = v ;

w.dist = v.dist + c((v,w)) ; R = R ∪ {w} ;

} }

(66)

5. Entwickeln bzw. Festlegen der Datenstrukturen:

In dieser Phase ist zu entscheiden, welche Datenstrukturen für die Realisierung

- des Graphen und - des Randes

benutzt werden sollen.

Benötigte Operationen auf der Graphdatenstruktur:

- Iterieren über die Knotenmenge

- Iterieren über die Kantenmenge zu einem Knoten - Bewertung der Kanten auslesen

Benötigte Operationen auf dem Rand:

- Rand als leer initialisieren - Prüfen, ob Rand leer ist

- Wählen des Knotens mit minimaler Entfernung - Entfernen eines Knotens aus dem Rand

- Knoten zum Rand hinzufügen bzw. Knoten im Rand modifizieren

Als Graphdatenstruktur könnten z.B. Adjazenzlisten verwendet werden. Der Rand kann als Heap realisiert werden.

(67)

6. Ausarbeiten des Algorithmus:

Bemerkung:

Aus den Entscheidungen der 5. Phase entsteht ein Feinentwurf, der präzise zu formulieren und, wo möglich, zu optimieren ist.

Schließlich kann der Feinentwurf ausprogrammiert und getestet werden.

• Bis auf den letzten Schritt sind alle Phasen der Algorithmenentwicklung unabhängig von

Programmiersprachen. Üblicherweise rechnet man die Algorithmenimplementierung auch nicht mehr zum Bereich Algorithmen und Datenstrukturen.

• Softwareentwicklung im Allg. hat viele Parallelen zur Algorithmenentwicklung. Auch hier hat die Programmierung eine nachgeordnete Bedeutung.

Dafür liegt der Schwerpunkt nicht so sehr auf der Lösung gut eingrenzbarer Probleme, sondern stärker auf der Bewältigung der vielen Aspekte und des Umfangs der Aufgabenstellung.

Referenzen

ÄHNLICHE DOKUMENTE

- Wenn kein Speicher mehr verfügbar, gebe die Speicherbereiche von Objekten frei, die nicht mehr erreichbar sind. - Gebe Speicher von Objekten auf Anweisung des Programms

Der große „gescheite“ Hund macht sich über den kleinen „dummen“ Hund lustig!. Eine Fremdsprache zu kennen

Verbinde anschließend das geschriebene Wort mit dem passenden

In welchem Wort kommt der Buchstabe vor dem Kästchen zweimal vor. Kreise das richtige Wort ein und merk es dir

Das Dilemma einer Argumentation bei Tempo 130 ist ja ähnlich wie beim Rauchen: Dagegen kann man eigentlich nicht sein, weil die oberen 50 % des Geschwindigkeits­.. bereichs bis

Damit der N 1ede1schlag nicht zurasch entstehe und unregelmäfsig die ganze Platte bedecke, wendet man eine verdünnte und saure Eisenlösung an (siehe Seite 37)..

Er empfiehlt sich durch seine Beständigkeit für damit angesetzte Entwickler, hält sich lange Zeit, während der gewöhnliche Eisenvitriolentwickler mindestens alle drei Tage

Nach Abschalten der Falle, in der die Wolke auf eine Temperatur um die Fermi­Temperatur abgekühlt wird, breiten sich die Atome in einem La­.. serspeckle­Potential aus