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