4.3.4 Prozedurale Algorithmen und deren Analyse Bemerkung

Werbung
4.3.4 Prozedurale Algorithmen und
deren Analyse
Bemerkung:
Vorgehen:
1. Die algorithmische Grundidee ist unabhängig
vom verwendeten Programmierparadigma.
Beachte bei den folgenden Beispielen:
Wir betrachten prozedurale Formulierungen und
die Analyse von drei Sortieralgorithmen:
2. Die Verwendung von Feldern statt Listen kann
die Komplexität ändern.
- Sortieren durch Einfügen
- Quicksort
- Heapsort
Sortieren durch Einfügen
Bei allen Algorithmen gehen wir davon aus, dass
die zu sortierenden Daten in einem Feld vorliegen,
das verändert werden darf.
Algorithmische Grundidee:
Sortiere zunächst eine Teilliste (Terminierungsfall:
leere Liste). Füge dann die verbleibenden Elemente
nacheinander in die bereits sortierte Teilliste ein.
Datensätze stellen wir durch folgenden
Datentypen dar:
Funktionale Fassung:
class DataSet {
int key;
String data;
}
fun sortieren nil = nil
| sortieren (x::xl) =
einfuegen x (sortieren xl)
DataSet mkDataSet( int k, String s ) {
DataSet ds = new DataSet();
ds.key = k;
ds.data = s;
return ds;
}
18.12.2006
© A. Poetzsch-Heffter, TU Kaiserslautern
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)
397
18.12.2006
© A. Poetzsch-Heffter, TU Kaiserslautern
398
Prozedurale Fassung in Java:
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.
einfügen
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];
sortiert
unsortiert
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 );
}
} }
18.12.2006
© A. Poetzsch-Heffter, TU Kaiserslautern
399
18.12.2006
© A. Poetzsch-Heffter, TU Kaiserslautern
400
Laufzeitabschätzung:
Quicksort
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.
Algorithmische Grundidee:
• Wähle einen beliebigen Datensatz mit Schlüssel k
aus, das sogenannte Pivotelement.
Bester Fall:
• 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
Liste ist bereits aufsteigend sortiert.
pro Schleifendurchlauf ein Schlüsselvergleich
• Wende quicksort rekursiv auf die Teillisten an.
pro Durchlauf zwei Datensatzzuweisungen
Schlüsselvergleiche:
• Hänge die resultierenden Listen und das Pivotelement
zusammen.
Cmin (N) = N -1;
Datensatzzuweisungen: M min (N) = 2*(N –1);
Funktionale Fassung:
Schlechtester Fall:
Liste ist absteigend sortiert.
pro Schleifendurchlauf i Schlüsselvergleiche
pro Durchlauf (i+2) Datensatzzuweisungen
Schlüsselvergleiche:
N-1
2
Cmax (N) = Σ i ∈ O(N )
i=1
N-1
2
Datensatzzuweisungen: M max (N) = Σ (i+2) ∈ O(N )
i=1
Auch im Durchschnitt ergibt sich quadratische
Komplexität.
© A. Poetzsch-Heffter, TU Kaiserslautern
18.12.2006
401
Umsetzung in prozedurale Fassung:
Indexzähler left, right
laufen von links bzw.
rechts bis f[left].key ≥ pivot.key
&& f[right].key < pivot.key
Es gilt:
402
void quicksort( DataSet[] f, int ug, int og){
if( ug < og ) {
int ixsplit = partition(f,ug,og);
/*
ug <= ixsplit <= og
&& f[ixsplit]==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 );
}
}
f[i] < pivot.key
Für alle i in [right+1,og] : pivot.key ≤ f[i]
1. Fall:
left > right : Teilung vollzogen:
og
left
© A. Poetzsch-Heffter, TU Kaiserslautern
void sortieren(/*nonnull*/ DataSet[] f) {
quicksort(f,0,f.length-1);
}
- Realisiere das Teilen der Liste durch Vertauschen:
ug
18.12.2006
Prozedurale Fassung in Java:
- Speichere die Datensätze in einem Feld und
bearbeite rekursiv Teilbereiche des Feldes
Für alle i in [ug,left-1] :
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
right
2. Fall:
int partition( DataSet[] f, int ug, int og){
... // siehe naechste Folie
}
left ≤ right : Vertausche f[left] und f[right],
inkrementiere left und right und fahre fort.
public static void main( String[] arg ) {
... // siehe Folie 400
ug
left
18.12.2006
}
og
right
© A. Poetzsch-Heffter, TU Kaiserslautern
403
18.12.2006
© A. Poetzsch-Heffter, TU Kaiserslautern
404
Grobe Laufzeitabschätzung von Quicksort:
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;
}
Seien C, M und N definiert wie auf Folie 401.
Vorüberlegung:
Betrachte die Ebenen gleicher Tiefe im Aufrufbaum
von quicksort. Das Zerlegen aller Teillisten auf einer
Ebene verursacht schlimmstenfalls linearen Aufwand:
Cpart (N) = O(N)
Mpart (N) = O(N)
Ungünstigster Fall:
Beim Zerlegen der Listen ist jeweils eine der Teillisten
leer. Dann hat der Aufrufbaum die Tiefe N, also gilt:
2
Cmax(N) = N * Cpart (N) = O(N )
Mmax (N) = N * M part (N) = O(N 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:
Cmin (N) = log N * Cpart (N) = O(N log N)
Mmin (N) = log N * Mpart (N) = O(N log N)
18.12.2006
© A. Poetzsch-Heffter, TU Kaiserslautern
405
18.12.2006
© A. Poetzsch-Heffter, TU Kaiserslautern
Bemerkung:
Vorgehen:
• Die mittlere Laufzeit von Quicksort ist auch von
der Größenordnung O(N log N) (siehe Ottmann,
Widmayer: Abschn. 2.2)
- Entwicklung einer prozeduralen Datenstruktur
FVBinTree für fast vollständige Binärbäume
- Heapsort unter Nutzung von FVBinTree
- Elimination der Schnittstelle
• Die vorgestellte Quicksort-Fassung arbeitet
schlecht auf schon sortierten Listen.
• Verbesserungen der vorgestellten Variante ist
möglich durch geeignetere Auswahl des Pivotelementes und durch Elimination der Rekursion.
Prozedurale Datenstruktur für fast vollständige, markierte, indizierte Binärbäume:
Heapsort
class FVBinTree {
DataSet[] a;
int currsize;
}
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) ).
18.12.2006
© A. Poetzsch-Heffter, TU Kaiserslautern
407
406
/* 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; }
18.12.2006
© A. Poetzsch-Heffter, TU Kaiserslautern
408
Bemerkung:
/* lesend */
DataSet get( FVBinTree t, int ix ) {
return t.a[ix];
}
Bei einer prozeduralen Datenstruktur muss man
sich genau merken, welche Operationen
- Referenzen übernehmen bzw.
/* 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;
}
- Änderungen vornehmen.
/* 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);
}
18.12.2006
© A. Poetzsch-Heffter, TU Kaiserslautern
409
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);
}
}
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.
- Benutzen wir eine swap-Prozedur für Felder:
void swap( DataSet[] f, int i1, int i2 ){
DataSet dtmp = f[i1];
f[i1] = f[i2];
f[i2] = dtmp;
}
© A. Poetzsch-Heffter, TU Kaiserslautern
18.12.2006
© A. Poetzsch-Heffter, TU Kaiserslautern
410
/* 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);
}
}
- Ersetzen wir die Operationen der Datenstruktur
durch deren Rümpfe.
18.12.2006
/* 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 );
}
} }
411
18.12.2006
© A. Poetzsch-Heffter, TU Kaiserslautern
412
Grobe Laufzeitabschätzung von Heapsort:
Übersicht über die Komplexität von
Sortierverfahren:
Seien C, M und N definiert wie auf Folie 401. Wir
betrachten nur den ungünstigsten Fall.
Sortierverfahren
Ungünstigster Fall:
1. Herstellen der Heap-Eigenschaft:
intern (im HSP)
Bezeichne j die Anzahl der Niveaus im Heap,
also 2
j-1
extern (nicht im HSP)
j
≤ N ≤ 2 -1.
Dann gibt es auf Niveau k höchstens 2 k-1 Schlüssel
und Cmax und M max sind proportional zu j-k .
Insgesamt gilt dann für die Anzahl der Operationen
zur Herstellung der Heap-Eigenschaft:
j-1
j-1
j-1
i
k=1
i=1
i=1
2i
Σ 2 k-1 (j-k) = Σ i * 2 j-i-1 = 2 j-1* Σ
≤ N*2 ∈ O(N)
O(N k )
O( N log N)
2
k≤2
Auswählen
Baumsortierung (AVL)
Einfügen
Heapsort
Bubblesort
Mergesort
Shellsort
Baumsortierung
Quicksort
Mergesort
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)
18.12.2006
© A. Poetzsch-Heffter, TU Kaiserslautern
413
4.3.5 Algorithmenklassen & -entwicklung
18.12.2006
© A. Poetzsch-Heffter, TU Kaiserslautern
414
Klassifikation der Algorithmen gemäß:
• verwendeter Datenstrukturen
Dieser Abschnitt skizziert:
• algorithmischer Kriterien (z.B. Art der Parallelität)
• wichtige weitere Problem- und Algorithmenklassen
• spezieller Aufgabenbereiche
• einen Weg zur Entwicklung von Algorithmen
anhand eines Beispiels
Datenstrukturen:
Problem- und Algorithmenklassen
Mengen, Listen, Warteschlangen, etc.:
Neben dem klassischen Bereichen des Sortierens
und Suchens von Datensätzen gibt es eine Vielzahl
von Algorithmen für unterschiedliche Aufgabenund Problembereiche.
Ziele:
Effiziente Speicherung und effiziente Operationen
zum Einfügen, Suchen und Löschen.
Beispiele: (Algorithmische Probleme)
Zeichenreihen, Textsuche:
• Optimaler Einsatz der Flugzeugflotte einer
Fluggesellschaft.
Ziele:
• Ermittlung der Schnittfläche zweier Flächen gegeben
durch ihre Punkte
Effiziente Suche von Wort- oder Textmustern in
Texten.
• Erfüllbarkeit/Allgemeingültigkeit logischer Formeln.
Beispiel:
• Auffinden aller Web-Seiten, die eine Menge von
Schlüsselwörter enthalten
Finde alle Vorkommen von „S%Haffner“ in den letzten
5 Jahrgängen der Frankfurter Allgemeinen Zeitung.
18.12.2006
© A. Poetzsch-Heffter, TU Kaiserslautern
415
18.12.2006
© A. Poetzsch-Heffter, TU Kaiserslautern
416
Graphen:
Speicherdarstellungen von Graphen:
Begriffsklärung: (Graph)
Sei G = (V,E) ein gerichteter Graph, V = {1,...,n}.
G lässt sich speichern als:
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)
- 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.
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.
Algorithmische Kriterien:
Graphen bieten für eine große Klasse von
Problemen ein geeignetes abstraktes Modell.
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:
Beispiele:
- Daten auf anderen Speichermedien ohne wahlfreies
Zugriffsverhalten
- Was ist die beste Verbindung von A nach B?
- Parallelisierung für gegebene Rechner, um
akzeptable Antwortzeiten zu erhalten bzw. große
Datenmengen rechnen zu können.
- Wie transportiere ich Waren von mehreren Anbietern am billigsten zu mehreren Nachfragern?
- Wie gestalte ich einen Arbeitsablauf mit mehreren
Maschinen und Arbeitskräften optimal?
- Arbeiten mit verteilten, sich dynamisch
entwickelnden Daten
- Welche Wassermenge kann maximal durch die
Kanalisation von KL abgeleitet werden?
18.12.2006
© A. Poetzsch-Heffter, TU Kaiserslautern
417
18.12.2006
© A. Poetzsch-Heffter, TU Kaiserslautern
418
Übersetzertechnik:
Aufgabenbereiche:
Aufgabenbereiche:
Viele Teilgebiete der Informatik und anderer Fächer
haben mittlerweile für ihre speziellen Aufgaben
umfangreiches algorithmisches Wissen erarbeitet.
• Parsen gemäß einer kontextfreien Grammatik:
Zwei Beispiele:
• Optimierende Übersetzung, zum Beispiel:
- Eingabe: Zeichenreihe (Programm)
- Ausgabe: Syntaxbaum
- Algorithmische Geometrie
- Konstante Ausdrücke zur Übersetzungszeit
berechnen
- Prozeduraufrufe durch ihre Rümpfe ersetzen
- Übersetzertechnik/Compilerbau
- Speicherbedarf verringern
Algorithmische Geometrie:
Beispielproblem:
Beispiel: (Konstantenfaltung)
Gegeben eine Menge von Rechtecken; ermittle
alle Paare von Rechtecken, die sich schneiden.
Übersetze das Programmfragment
int a = 7;
Anwendungsbereiche:
int b = a * 3;
• Computergraphik, Visualisierung
int c = a + b;
• Geometrische Modellierung, CAD
so als hätte der Programmierer geschrieben:
• Schaltungsentwurf
int a = 7;
• Wegeplanung von Robotern
int b = 21;
int c = 28;
18.12.2006
© A. Poetzsch-Heffter, TU Kaiserslautern
419
18.12.2006
© A. Poetzsch-Heffter, TU Kaiserslautern
420
Algorithmenentwicklung
Beispiel: (Escape-Analysis)
Abschließend zu 4.3 betrachten wir wichtige Phasen
der Algorithmenentwicklung an einem Beispiel.
Aufgabe:
Ermittele die Objekte, die auf dem Keller alloziert
werden können, da ihre Referenzen den Methodenaufruf, der sie erzeugt hat, nicht verlassen.
Phasen der Algorithmenentwicklung:
Beispielfragment:
2. Entwickeln einer algorithmischen Idee
void m( String s ) {
String t = "" + s ;
t = doSomething(t);
println(t);
}
1. Problemabstraktion und -formulierung
3. Ermitteln wichtiger Eigenschaften des Problems
// neuer Verbund
// Modifikation von t
4. Grobentwurf eines Algorithmus‘ mit Abstützung
auf existierende Teillösungen
5. Entwickeln bzw. Festlegen der Datenstrukturen
6. Ausarbeiten des Algorithmus
Der/das von t referenzierte Verbund/Objekt
könnte auf dem Keller verwaltet werden.
Algorithmenentwicklung an einem Beispiel:
Ziel:
Wir erläutern die Phasen der Algorithmenentwicklung
an einem Beispiel (vgl. Phasen der Softwareentwicklung).
Entlastung der Speicherbereinigung.
0. Problem:
Routenplaner für Fahrradfahrer in einer Großstadt:
Wie ist die beste Verbindung zwischen zwei
Straßenkreuzungen?
18.12.2006
© A. Poetzsch-Heffter, TU Kaiserslautern
421
18.12.2006
© A. Poetzsch-Heffter, TU Kaiserslautern
422
1. Problemabstraktion und -formulierung:
2. Entwickeln einer algorithmischen Idee:
Modelliere die Straßen und Wege durch einen
gerichteten Graphen mit bewerteten Kanten:
Eine verbreitete Strategie zur Algorithmenentwicklung
versucht ein Problem auf ähnlich geartete
Teilprobleme zu reduzieren. Hier:
- Straßenkreuzungen entsprechen Knoten
Reduziere die Suche des kürzesten Wegs von
s nach z auf kürzeste Wege zwischen anderen
Knotenpaaren.
- 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).
Ansatz:
(a) Errechne schrittweise Knotenmengen B, sodass
der kürzeste Weg von s zu allen Knoten von B
bekannt ist. Anfangs ist B = { s }.
- Jede Kante bekommt als Bewertung die Zeit in
Sekunden, die man im Durchschnitt für den Weg
von A nach B braucht.
R+
Die Bewertung ist eine Funktion c: E
Bewertete gerichtete Graphen nennt man
Distanzgraphen.
(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.)
Damit lässt sich das Problem wie folgt formulieren:
(c) Bestimme den kürzesten Weg zu einem Knoten
in R und erweitere B entsprechend.
- Gegeben ein Distanzgraph, der die Straßenverbindungen modelliert, sowie zwei Knoten s und z.
Unter welchen Bedingungen lassen sich (a)-(c)
algorithmisch lösen? Was sind die Einzelschritte?
- Gesucht ist ein Weg s, v1, ... , vn , z mit minimaler
Länge lg :
Sei r ∈ R ein Randknoten und w1 ,...,wr ∈ B alle
Knoten mit (wi ,r) ∈ E . Lässt sich damit der
kürzeste Weg von s nach r bestimmen und seine
Länge spl(s,r) ?
lg = c( (s,v1) ) + c( (v2,v3) ) + ... + c( (vn,z) )
18.12.2006
© A. Poetzsch-Heffter, TU Kaiserslautern
423
18.12.2006
© A. Poetzsch-Heffter, TU Kaiserslautern
424
3. Ermitteln wichtiger Eigenschaften des Problems:
4. Grobentwurf eines Algorithmus‘ mit Abstützung
auf existierende Teillösungen:
Um unseren Ansatz umsetzen zu können, brauchen
wir eine Eigenschaft, die es uns ermöglicht,
B schrittweise um Randknoten zu erweitern.
Wir setzen die obigen Ansätze in einen Grobentwurf
um, der auf Dijkstra zurückgeht (vgl. Ottmann,
Widmayer: 8.5.1):
Verschärfung des Ansatzes:
Jeder Knoten erhält drei zusätzliche Komponenten:
• Bestimme für jeden Knoten r des Randes einen
Vorgänger wr in B, so dass
d(r) = spl( s, w r ) + c((w r,r)) minimal ist.
pred: Vorgänger auf dem kürzesten „Rückweg“ zu s.
dist: die kürzeste bisher ermittelte Entfernung zu s.
inB:
• Wähle unter allen Knoten r von R denjenigen mit
minimalem d(r) aus. Sei dieser Knoten mit p
bezeichnet. Erweitere B um p.
Algorithmus: kürzeste Wege in bewerteten
Graphen G = (V,E) mit Bewertungsfunktion c.
Startknoten ist s.
Behauptung:
spl( s, w p) + c( (w,p)
p ) = spl( s, p ) , d.h. der
// Initialisieren der Knoten und von B:
kürzeste Weg von s zu p wurde gefunden.
for all v∈V\{s} do {
v.pred = null;
v.dist = ∞ ;
v.inB = false ;
}
s.pred = s ;
s.dist = 0 ;
s.inB = true ;
Beweis:
Mit Induktion über den kürzesten Weg (siehe
Vorlesung).
18.12.2006
© A. Poetzsch-Heffter, TU Kaiserslautern
Ist genau dann true, wenn Knoten in der
Menge ist, für die der kürzeste Weg bekannt ist.
425
18.12.2006
© A. Poetzsch-Heffter, TU Kaiserslautern
5. Entwickeln bzw. Festlegen der Datenstrukturen:
// Initialisieren des Randes R:
R = ∅;
// R initialisieren, d.h. die Nachfolger von s eintragen:
In dieser Phase ist zu entscheiden, welche
Datenstrukturen für die Realisierung
ergänzeRand(s,R);
- des Graphen und
// Auswählen von Knoten aus R und R ergänzen:
- des Randes
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);
}
benutzt werden sollen.
where
procedure ergänzeRand( v, R ) {
Benötigte Operationen auf dem Rand:
Benötigte Operationen auf der Graphdatenstruktur:
- Iterieren über die Knotenmenge
- Iterieren über die Kantenmenge zu einem Knoten
- Bewertung der Kanten auslesen
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} ;
}
}
}
18.12.2006
© A. Poetzsch-Heffter, TU Kaiserslautern
426
427
- 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.
18.12.2006
© A. Poetzsch-Heffter, TU Kaiserslautern
428
6. Ausarbeiten des Algorithmus:
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.
Bemerkung:
• 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.
18.12.2006
© A. Poetzsch-Heffter, TU Kaiserslautern
429
Herunterladen