FB Informatik Prof. Dr. R.Nitsch Algorithmen und Datenstrukturen (Beispiele in C++) Reiner Nitsch 8471 [email protected] Such-Algorithmen – Lineare Suche FB Informatik Prof. Dr. R.Nitsch Prinzip der linearen Suche: Betrachte jedes Element im Suchbereich Vergleiche jedes Element im Suchbereich mit dem Suchwert Wenn gefunden (Suchtreffer), gib Index oder Zeiger auf Suchtreffer zurück Wenn nicht gefunden (Suchfehler), gib Sentinel zurück. int* find( int* pfirst, int* plast, int val ) { while( pfirst<plast ) if( *pfirst++ == val ) return pfirst; // Suchtreffer return plast; // sentinel für Suchfehler } Funktioniert der Algorithmus auch mit std::string-Containern? ja Funktioniert der Algorithmus auch mit einem C-Array? ja Welche Anforderungen stellt diese Implementierung an den Container? Der den Suchbereich definierende Datentyp (hier: int* )muss - mittels ++ zum nächsten Element navigieren können - mittels * dereferenzieren, d.h. auf das Element lesend zugreifen können. Welche Anforderungen stellt diese Implementierung an den Elementtyp? operator== muß für Elementtyp des Arrays (hier: int) definiert sein. 28.06.2012 2 Such-Algorithmen – Binäre Suche 15.06.2012 FB Informatik Prof. Dr. R.Nitsch Nachteil der linearen Suche: langsam, weil Suche in kleinen Schritten Besser: Suche in großen Schritten Frage: Wie errät man eine Zufalls-Zahl zwischen 0 und 100 am schnellsten? 2. Version des Suchalgorithmus: Binäre Suche Algorithmischer Kern Rateversuch: Mittlere Zahl aus (sortiertem) Suchbereichs wenn gleich: fertig sonst: links oder rechts weitersuchen int* binarySearchI(int* pfirst, int* plast, int val ) { //Binäre Suche (iterativ) im Bereich [pfirst, plast); // Vorbedingung: sortierte Elemente in Bezug auf operator< T* pend = plast; // Position des one-past-the-end Elements merken! while (pfirst<pend) { Neue Fragen: T* pmid = pfirst+(pend-pfirst)/2; if( val<*pmid ) pend = pmid; // links weitersuchen Verhalten bei Duplikaten? else if( *pmid<val ) pfirst = pmid+1; // rechts weitersuchen Welcher Algorithmus ist besser? else return pmid; // gefunden Wie misst man die Effizienz von } Algorithmen? return plast; // nicht gefunden Mögliche Benchmarking-Kriterien? } Anforderungen an Objekte vom Element-Typ? 28.06.2012 Benötigt nur < für Elementtyp Achtung: == wird gar nicht benutzt! 3 Such-Algorithmen – Binäre Suche 15.06.2012 FB Informatik Prof. Dr. R.Nitsch Nachteil der linearen Suche: langsam, weil Suche in kleinen Schritten Besser: Suche in großen Schritten Frage: Wie errät man eine Zufalls-Zahl zwischen 0 und 100 am schnellsten? 2. Version des Suchalgorithmus: Binäre Suche Algorithmischer Kern Rateversuch: Mittlere Zahl aus (sortiertem) Suchbereichs wenn gleich: fertig sonst: links oder rechts weitersuchen int* binarySearchI(int* pfirst, int* plast, int val ) { //Binäre Suche (iterativ) im Bereich [pfirst, plast); // Vorbedingung: sortierte Elemente in Bezug auf operator< int* pend = plast; // Position des one-past-the-end Elements merken! while (pfirst<plast) { Neue Fragen: int* pmid = pfirst+(plast-pfirst)/2; if( val==*pmid ) return pmid; Verhalten bei Duplikaten? // gefunden else if( *pmid<val ) pfirst = pmid+1; // rechts weitersuchen Welcher Algorithmus ist besser? else plast = pmid;; // links weitersuchen Wie misst man die Effizienz von } Algorithmen? return pend; // nicht gefunden Mögliche Benchmarking-Kriterien? } Anforderungen an Objekte vom Element-Typ? 28.06.2012 Benötigt nur < für Elementtyp Achtung: == wird gar nicht benutzt! 4 Aufwand von Algorithmen (Komplexität) FB Informatik Prof. Dr. R.Nitsch Kriterien sind u.a. Speicheraufwand statisch für Programm/Algorithmus für Daten dynamisch, d.h. abhängig von Datenmenge Speicherkomplexität für Aufruf und Initialisierungen für Wiederholungen Zeitkomplexität Zeitaufwand statisch dynamisch Eine präzise detailreiche Bestimmung der Aufwände wird i.A nicht durchgeführt, weil dies mathematisch oft nicht handhabbar ist uninteressant ist: für Vergleichszwecke reicht auch ger. Detailierungsgrad Vereinfachungen (Abstraktionen) bei der Ermittlung des Zeitaufwandes Der tatsächliche Zeitaufwand ist immer prozessorabhängig. Um davon unabhängig zu werden, macht man folgende Vereinfachungen: Jede Anweisung (Schleifen ausgenommen) benötigt den Zeitaufwand 1 Bei Wiederholungen/Schleifen sind wiederholten Anweisungen mit der Anzahl der Wiederholungen zu gewichten, die meist von der Anzahl der Eingabedaten, n , bestimmt ist. 28.06.2012 5 Aufwand von Algorithmen - Abstraktionen FB Informatik Prof. Dr. R.Nitsch Wenn die Algorithmuslaufzeit T(n) nicht für alle Eingaben derselben Länge n gleich ist, sind folgende Grenzfälle interessant: der beste Fall (best case) Tbest der schlimmste Fall (worst case) Tworst das Durchschnittsverhalten (average case) Tavg O-Notation (auch Landau-Notation) der Laufzeitfunktion ist ein mathematisches Verfahren zur Einordnung der Komplexität von Funktionen für großes n. benennt lediglich aus einer Klasse gleich schnell wachsender Funktionen den einfachsten Repräsentanten als obere Schranke (Supremum). Meist reicht diese Abschätzung der Größenordnung, weil Algorithmen sich schon hier unterscheiden. gibt ein Maß für die Anzahl der Elementarschritte in Abhängigkeit von der Größe der Eingangsvariablen an. 28.06.2012 6 Such-Algorithmen – Binäre Suche FB Informatik Prof. Dr. R.Nitsch Lineare Suche 1 1 1 int* find( int* pfirst, int* plast, int val ) { while( pfirst<plast ) if( *pfirst++ == val ) return pfirst; return plast; // sentinel } n: Anzahl Datenelemente R(n): Anzahl Wdhlgn. Laufzeit? 1 1 1 Suchtreffer: T(n)=2·R(n) =2·(n/2)·2=n T(n) O(n) Suchfehler: T(n)=2·R(n)+1 =2·n+1 T(n) O(n) Binäre Suche 1 1 1 1 1 1 int* binarySearchI(int* pfirst, int* plast, int val ) { //Binäre Suche (iterativ) im Bereich [pfirst, plast); // Vorbedingung: sortierte Elemente Laufzeit? { int* pend = plast; 1 while (pfirst<plast) { 1 int* pmid = pfirst+(plast-pfirst)/2; 1 if( val==*pmid ) return pmid; 1 else if( *pmid<val ) pfirst = pmid+1; 1 else plast = pmid; } return pend; } 1 28.06.2012 T(n)=R(n)·3+2 R(n) = ? 7 Komplexität der binären Suche FB Informatik Prof. Dr. R.Nitsch Aufwandsabschätzung für binäre Suche • Jede Iteration benötigt die Zeit 4 • Jede Iteration halbiert den Suchbereich Nach dem 1. Halbieren enthält Suchbereich noch n/2 = n/21 Elemente Nach dem 2. Halbieren enthält Suchbereich noch n/4 = n/22 Elemente … Nach dem R-ten Halbieren enthält Suchbereich noch n/2R Elemente • Worst case: Suche endet, wenn Suchbereich nur noch 1 Element enthält! n Zeitkomplexität T(n) = R(n)·3+2= log2(n) · 3 + 2 = 1 R = log2(n) Wiederholungen 2R d.h. die Zeitkomplexität wächst logarithmisch mit n Dies bringt man abkürzend durch die "Big-O"-Notation zum Ausdruck: Zeitkomplexität = O( log n ) Ergebnis: In sortierten Reihen kann wesentlich schneller gesucht werden! Beispiel: Laufzeitvergleich lineare ↔ binäre Suche (gemessene Werte: Intel X86 Prozessor, 1,66 GHz, Debug-Konfiguration) 28.06.2012 n O(n) O(log n) 1000 106 109 2,6 us 2,6 ms 2,6 s 0,1 us 0,2 us 0,3 us 8 Beispiele zur Schätzung der Zeitkomplexität Anweisung Zeitkomplexität T(n) Big-O FB Informatik Prof. Dr. R.Nitsch Laufzeit Typische Algorithmen x=x+1; 1 O(1) konstant Elementzugriff in phy.Seq. for (int i=1; i<=n; i++) x=x+i; n*1 O(n) linear Elementzugriff in log. Seq. Suche in unsort. Seq. n*n*1 O(n2) quadratisch primitive Sortierverfahren for ( int i=1; i<=n; ++i ) for( int j=1; j<=n; ++j ) x=x+i+j; for ( int i=1; i<=n; ++i ) for( int j=1; j<=n; ++j ) x=x+i+j; int k=0; while( k<=n ) { cout << 2*k+1 << endl; k++; } n*n*1 1 Welche Zeitkomplexität hat die lineare Suche? n2+2n+3 (n+1)*2 Nur der größte Beitrag wird berücksichtigt, wenn lediglich die Größenordnung interessiert. Genauer: n2+2n+3 wächst höchstens so schnell wie c·n2, c>0 ab einem gewissen n0>0 28.06.2012 O(n2) O(nk) polynomiell z.B. Zahlenschlossprojekt: O(n3) O(2n) exponentiell Backtracking-Algorithmen, rekursive Variante der Fibonacci-Folge 9 Weitere Beispiele für O-Notation der Algorithmuslaufzeit FB Informatik Prof. Dr. R.Nitsch Notation Bedeutung Anschauliche Erklärung T(n) є O(1) ist konstant überschreitet einen konstanten Wert nicht Nachschlagen des x-ten Elementes in einem (unabhängig vom Wert des Arguments). Feld. T(n) є O(log n) wächst logarithmisch wächst ca. um einen konstanten Betrag, wenn sich das Argument verzehnfacht. Binäre Suche im sortierten Feld mit n Einträgen T(n) є O(√n) wächst wie die Wurzelfunktion wächst ungefähr auf das Doppelte, wenn sich das Argument vervierfacht naiver Primzahltest mittels Teilen durch jede Zahl ≤n T(n) є O(n) wächst linear wächst ungefähr auf das Doppelte, wenn sich das Argument verdoppelt. Suche im unsortierten Feld mit Einträgen (Bsp. Lineare Suche) T(n) є O(nlog n) hat super-lineares Wachstum T(n) є O(n2) wächst quadratisch wächst ungefähr auf das Vierfache, wenn Einfache Algorithmen zum Sortieren von sich das Argument verdoppelt Zahlen Selectionsort T(n) є O(nk) wächst polynomiell wächst ungefähr auf das Doppelte, wenn sich das Argument um eins erhöht Zahlenschloßprojekt T(n)=O(n3) T(n) є O(n!) wächst faktoriell wächst ungefähr um das n-fache, wenn sich das Argument um eins erhöht. Problem des Handlungsreisenden 28.06.2012 Beispiele für Laufzeiten Fortgeschrittenere Algorithmen zum Sortieren von Zahlen Mergesort, Heapsort 10 Warum ist die Zeitkomplexität eines Algorithmus so wichtig? FB Informatik Prof. Dr. R.Nitsch Häufig ist die erste Idee, die HW zu beschleunigen. Aber: der Geschwindigkeitsvorteil ist dabei auf konstanten Faktor beschränkt Bessere HW ist zudem teuer, bzw. stößt an technische Grenzen Ein schneller Algorithmus auf einer langsamen Maschine wird immer schneller sein als ein langsamer Algorithmus auf einer schnellen Maschine! Supercomputers are for people too rich and too stupid to design efficient algorithms (Steven Skiena) 28.06.2012 11 Sortieren FB Informatik Prof. Dr. R.Nitsch Für ein Feld von n Objekten gibt es n! Permutationen Sortieren ist ein Vorgang, der durch (möglichst wenige) paarweise Vergleiche von Objekten eine dieser Permutationen herausfiltert, in der die Objekte einer bestimmten Ordnungsrelation (z.B. größer, kleiner, …) genügen. Stabile Sortierung Beispiel: 7 2 2 3 5 5 3 7 unsortiert sortiert Kommt ein Schlüsselwert mehrfach vor (Duplikate) ist die Sortierung nicht eindeutig: 1 7 1 3 3 3 Ein Sortierverfahren, bei dem die Reihenfolge von Schlüsselduplikaten nach dem Sortieren unverändert ist, bezeichnet man als "Stabiles Sortierverfahren" 3 1 3 7 3 7 sortiert unsortiert stabil sortiert Schlüsselwerte 28.06.2012 12 Sortierverfahren - Klassifizierungskriterien Zeitverbrauch Anzahl Schlüsselvergleiche (Cmin, Cmax, Cavg) Anzahl Vertauschungen (Mmin, Mmax, Mavg) Sensibilität bezüglich Eingabeverteilung Speicherplatzverbrauch (Programme u. Daten) FB Informatik Prof. Dr. R.Nitsch Zeitkomplexität Speicherkomplexität Speicherplatzbedarf am geringsten für Sortieren am Ort ("in place" oder "in situ") Natürliche Sortierverfahren arbeiten mit vorsortierten Daten schneller als mit unsortierten. Bei adaptiven Sortierverfahren ist der Kontrollfluss, das heißt die Abfolge der Befehle abhängig von den Daten. Nicht-adaptive Verfahren können gut in Hardware implementiert werden. Stabile Sortierverfahren, ändern die Reihenfolge von Duplikaten beim Sortieren nicht. Wie schnell kann man sortieren? Voraussetzung für jedes Sortieren: Auf den zu sortierenden Objekten muss eine Ordnung für die Schlüssel definiert sein. Untere Komplexitätsschranke für Sortierverfahren: Satz: Jedes vergleichsbasierte Sortierverfahren für N Elemente benötigt im Mittel und im schlechtesten Fall eine Laufzeit von wenigstens T(N)O(N·log N) Vergleiche 28.06.2012 13 Sortierverfahren - Klassifizierung von Sortiertechniken Sortieren durch L1 FB Informatik Prof. Dr. R.Nitsch Min. Element von L2 1) Auswählen 2) Einfügen sortiert sortiert Elementare Sortierverfahren 3) Austauschen Ki>K 4) Zerlegen K Kj>K 5) Mischen 10 40 60 … … … bereits sortiert 20 30 50 … … … L1 10 20 30 40 50 60 … … L L2 "Reißverschlußverfahren" 6) …noch viel mehr 28.06.2012 14 Sortieren durch Austauschen (exchange sort, bubble sort) Austauschen lokal first Idee: Beginnend am Anfang der unsortierten Teilreihe werden jeweils Elementpaare gebildet. Die Elemente eines Paares werden verglichen und dann getauscht, wenn das größere Element näher am Anfang der Reihe liegt. Nach N-1 Schritten ist der 1. Sortierlauf beendet und das größte Element zum Ende der Reihe wie eine Blase "aufgestiegen" ( Bubble Sort). Es bildet dort das 1. Element der teilsortierten Reihe. Der Vorgang wird mit der um ein Element kleineren unsortierten Teilreihe wiederholt. Nach N-1 solchen Sortierläufen ist der Sortiervorgang abgeschlossen. last 420 97 97 97 97 97 420 420 420 420 420 420 420 301 301 301 301 301 420 35 35 35 35 35 420 … … … … … teilsortierte Reihe first last unsortierte Reihe 97 97 97 97 420 420 301 301 301 301 420 35 35 35 35 420 420 420 420 420 … … … … Ist der Sortiervorgang stabil? 28.06.2012 FB Informatik Prof. Dr. R.Nitsch ja 15 Implementierung von Bubble-Sort FB Informatik Prof. Dr. R.Nitsch /* BubbleSort - Sorts a subsequence [pfirst,plast) @param pfirst pointer to first element of subsequence @param plast pointer to one-past-the-end element of the subsequence */ // version 1 (with pointer) void bubbleSort( int* pfirst, int* plast ) { for ( int* plastu=plast-1; plastu>pfirst; --plastu ) plastu verweist auf Ende der unsortierten Reihe for ( int* pp=pfirst ; pp<plastu ; ++pp ) if ( *(pp+1)<*pp ) pp verweist auf erstes Element eines Pärchens std::swap( *pp, *(pp+1) ); Wenn Nachfolger kleiner } Plätze wechseln // Version 2 (with pointer) void bubbleSort( int* pfirst, int* plast ) { bool swapped; Version 2 nutzt die Eigenschaft aus, dass nach for( int* plastu=plast-1; plastu>pfirst; --plastu ) einer Iteration, in der keine Vertauschungen { swapped = false; stattfanden auch in den restlichen Iterationen for( int *pp=pfirst ; pp<plastu ; ++pp ) keine Vertauschungen mehr stattfinden. if( *(pp+1) < *pp ) { swap( *pp, *(pp+1) ); swapped= true; } if( !swapped ) return; } 28.06.2012 16 Aufwandsabschätzung für Bubble-Sort Zeitkomplexität Anz.Vgl N 1 (N 2) ... 1 immer gleich! FB Informatik Prof. Dr. R.Nitsch first N 1 1 2 ... (N 1) k k 1 (N 1) N N N 2 2 2 2 O(N2) N2 N Anzahl Vertauschungen O(N2) 4 4 97 97 97 420 420 301 301 301 301 420 35 35 35 35 420 420 420 420 420 … … … … teilsortierte Reihe 97 97 97 420 420 301 301 301 301 420 35 35 35 35 420 420 420 420 420 in-place … … last stabil Vertauschungen: O(N2) im Mittel und im worst case; keine im best case. Vergleiche (Ver.2): O(N2) im Mittel und im worst case; O(N) im best case. Aufwand (Anzahl Vergleiche) ist unabhängig von Eingabeverteilung. Wird in der Praxis kaum eingesetzt. … … Zusammenfassung 28.06.2012 first unsortierte Reihe 97 Im Mittel halb so viele wie Vergleiche. last 97 17 Sortieren durch Auswahl (selection sort, MinSort, ExchangeSort) Idee: Auswahl des kleinsten Elementes im unsortierten Teil (L2) der Reihe Austausch mit dem ersten Element der unsortierten Teilreihe Die teilsortierte Reihe (L1) ist danach um 1 Element gewachsen. Die unsortierte Reihe enthält 1 Element weniger. Nach N solchen Durchläufen ist die Reihe sortiert. first last 420 35 35 35 35 420 420 97 97 97 97 97 420 301 301 301 301 301 420 420 35 420 420 420 420 … … … … … teilsortierte Reihe unsortierte Reihe L1 Min. Element von L2 Auswählen first Ist der Sortiervorgang stabil? FB Informatik Prof. Dr. R.Nitsch firstu min last nein! Beispiel: s. Abb. ( Key 420 ) 28.06.2012 18 Sortieren durch Auswahl - Komplexität und Eigenschaften // Algorithmus in Pseudocode prozedur selectionSort( a, first, last ) [ a[first], a[last] ) : Bereich sortierbarer Elemente Variable: firstu:=first Position d. 1. Elem. der unsort. Reihe wiederhole Variable: min:= firstu für jede Position pos von firstu+1 bis last-1 wiederhole falls a[pos]<a[min] dann min = pos ende falls ende für Vertausche a[min] und a[firstu] firstu = firstu+1 solange firstu<last ende prozedur first 35 35 35 420 420 97 97 97 97 97 420 301 301 301 301 301 420 420 35 420 420 420 420 … … … … … teilsortierte Reihe k 1 unsortierte Reihe Min. Element von L2 L1 a firstu min last Zusammenfassung Anzahl Vertauschungen unabhängig von Eingabeverteilung: N-1 Anzahl Vgl. unabhängig von Eingabeverteilung In-situ-Verfahren O(N2) Vergleiche besser als Bubble-Sort! nicht stabil (N 1) N N N O(N2) 2 2 2 N 1 Max. Anzahl der Vertauschungen: 28.06.2012 (bei invers sortierter Reihe) 2 35 first N 1 1 2 ... (N 1) k 420 last Anzahl der Vergleiche (immer gleich!): N 1 (N 2) ... 1 FB Informatik Prof. Dr. R.Nitsch 19 Sortieren durch Einfügen (insertion sort) FB Informatik Prof. Dr. R.Nitsch Idee: Vorgehen wie beim Sortieren eines first 420 97 97 35 35 Kartenspiels. 97 420 420 97 97 1. Starte mit oberster Karte den sortierten Stapel 420 420 420 420 301 2. Nimm die jeweils nächste Karte vom 35 35 35 420 420 unsort. Stapel 301 301 301 301 420 3. Füge sie an der richtigen Stelle im last … … … … … sortierten Stapel ein In einem Array müssen die größeren Elemente um 1 Indexposition weiter teilsortierte Reihe unsortierte Reihe rücken, um dem kleineren Einfügeelement prozedur insertionSort( a, first, last ) Platz zu machen. [ a[first], a[last] ) : Bereich sortierbarer Elemente Mit jedem Einfügeschritt wird der pos: mögliche Einfügeposition sortierte Stapel um 1 ein Element größer. posC: Position des nächsten Einfügekandidaten Nach N-1 solchen Einfügeschritten ist der valC: Wert des nächsten Einfügekandidaten Sortiervorgang abgeschlossen. für jedes Element von posC=first+1 bis last-1 wiederhole 6 54321 valC a first 28.06.2012 sortiert pos posC last pos = posC valC := a[posC] solange first<pos und valC<a[pos-1] wiederhole a[pos] = a[pos-1] pos = pos-1 > → stabil ende solange // Einfügepos. gefunden >= → instabil a[pos] = valC // Einfügen ende für ende prozedur 20 Sortieren durch Einfügen - Komplexität und Eigenschaften Worst Case bei invers sortierter Reihe first Max. Anzahl der Vergleiche: N 1 1 2 ... (N 1) k k 1 (N 1) N N 2 N 2 2 2 O(N2) last 420 97 97 35 35 97 420 420 97 97 420 420 420 420 301 35 35 35 420 420 301 301 301 301 420 … … … … … teilsortierte Reihe Max. Anzahl der Verschiebungen: FB Informatik Prof. Dr. R.Nitsch unsortierte Reihe 1 2 ... (N 1) N (N 1) / 2 Best Case bei sortierter Reihe Min. Anzahl der Vergleiche: 1 1 ... 1 N 1 Min. Anzahl der Verschiebungen: O(N) 0 Vorteilhaft bei fast sortierten Reihen: O(N) In-place-Verfahren, stabil 28.06.2012 21 Sortieren durch Mischen (Mergesort; John von Neumann 1945) FB Informatik Prof. Dr. R.Nitsch Gegeben folgendes Feld der Größe 10. 3 8 9 11 18 1 7 10 22 32 Die beiden "Hälften" sind hier bereits vorsortiert! Wir können das Feld sortieren, indem wir jeweils von der ersten oder der zweiten Hälfte ein Element wegnehmen, je nachdem, welches "dran" ist, . . . und die weggenommenen Elemente in ein anderes Feld kopieren. 1 3 7 8 3 8 9 11 18 1 7 10 22 32 9 10 11 18 22 32 Verschmelzen (Merge) Vorsortierte Teilreihen Falls die beiden Hälften nicht schon sortiert sind, dann müssen sie eben vorher sortiert werden. Wie? Durch Verschmelzen der jeweiligen Hälften (also Viertel). Und wenn die auch nicht schon sortiert sind? Dann werden eben wiederum die jeweiligen Hälften (also Achtel) verschmolzen, usw. bis man bei Feldern der Größe Eins angelangt ist und die sind ja stets sortiert. Mergesort beruht auf dem "Teile und herrsche" Prinzip 28.06.2012 22 Mergesort - top-down approach (rekursiv) FB Informatik Prof. Dr. R.Nitsch Aufruf: mergesort(a,0,7) 1 8 5 1 8 8 11 5 1 10 2 6 5 2 4 7 8 5 3 0 11 7 14 8 1 5 5 0 11 7 12 13 1 10 3 10 4 3 1 10 5 7 16 7 11 10 1 15 11 9 8 unsortiert, 1. Aufrufebene 1 10 0 11 7 17 2 8 10 0 7 11 algorithm mergeSort (F) -> FS Eingabe: eine zu sortierende Folge F Ausgabe: eine sortierte Folge FS if F einelementig then return F else teile F in der Mitte in F1 und F2; // 1. Aufruf F1:= mergeSort(F1); // 2. Aufruf F2:= mergeSort (F2); F:= merge(F1,F2); fi return F; merge implementiert die Mischfunktion! 18 0 28.06.2012 1 5 7 8 10 11 1 sortiert, 1. Aufrufebene 23 Implementierung in C++ 22.6.2012 FB Informatik Prof. Dr. R.Nitsch Aufruf: mergesort(a,0,7) f[] 0 1 8 5 first 2 3 4 5 6 7 1 10 0 11 7 ? mid last algorithm mergeSort (F) -> FS Eingabe: eine zu sortierende Folge F Ausgabe: eine sortierte Folge FS if F einelementig then return F else teile F in der Mitte in F1 und F2; F1:= mergeSort(F1); F2:= mergeSort (F2); F:= merge(F1,F2); fi return F; 28.06.2012 unsortiert, 1. Aufrufebene void mergeSort(T f[],int first, int last ) { // Eingabe: eine zu sortierende Folge f // Ausgabe: eine sortierte Folge fs // Sortierbereich [first,last); last-1 ist letztes Element. if (last-first <= 1) // einelementig oder leer return; else int mid = (first+last)/2; // teile mergeSort(f, first, mid); mergeSort(f, mid, last); merge(f, first, mid, last); } return; } 24 Algorithmus merge in Pseudocode procedure merge (F1,F2) -> F Eingabe: zwei vorsortierte Folgen F1, F2 Ausgabe: eine sortierte Folge F F:= leere Folge; while F1 oder F2 nicht leer do Entferne das kleinere der Anfangselemente aus F1 bzw. F2 Füge dieses Element an F an od; Füge die verbliebene nichtleere Folge F1 oder F2 an F an; return F; 28.06.2012 FB Informatik Prof. Dr. R.Nitsch void merge( T f[], int first, int mid, int last ) { /* Eingabe: 2 Folgen F1:=F[first...mid), F2:=F[mid...last) Ausgabe: eine sortierte Folge F */ vector<T> tmp(last‐first); // temporary field tmp int i1 = first; // next element in F1 int i2 = mid ; // next element in F2 while ( i1<mid && i2<last) { if ( f[i1]<f[i2] ) tmp.push_back( f[i1++] ); else tmp.push_back( f[i2++] ); } while (i1<mid) tmp.push_back( f[i1++] ); while (i2<last) tmp.push_back( f[i2++] ); // copy back the temporary field tmp for ( int j=0; j<tmp.size(); j++ ) f[first+j] = tmp[j]; return; } 25 Mergesort - Komplexität und Eigenschaften void mergeSort( int f[], int first, int last ) { if (last‐first<=1) return; int mid = (first+last)/2; // sort the first and the second half T(N/2) mergeSort(f, first, mid); T(N/2) mergeSort(f, mid, last); merge(f, first, mid, last); 3N } FB Informatik Prof. Dr. R.Nitsch gesucht: T(N) = ? Merge: je Element 1Vergleich 1 Kopieren in Hilfsarray 1 zurück kopieren = 3·N Annahme: N ist 2er-Potenz T(20) = 0 = 0 einsetzen T(21) = 2·T(20)+3·21 = 2· 0 T(22) 2·(3·21) = 2·T(21)+3·22 = + 3·21 = 3·21 + 3·22 = 3·2·22 T(23) = 2·T(22) + 3·23 = 2·(2·3·22) + 3·23 = 3·3·23 T(24) = 2·T(23) + 3·24 = 2·(3·3·23) + 3·24 = 3·4·24 T(2k) = 3·2k·k 2k=N k=log2N T(N) = 3·N·log2 N 28.06.2012 T(N) = 2·T(N/2) + 3·N Eigenschaften Mergesort Zeitkomplexität: Sensibel bzgl. Eingabevertlg: In-place-Verfahren: Zus. Speicherbedarf: stabil: O(NlogN) nein nein N ja Lösung 26 Quicksort (Nach C.A.R. Hoare, 1960) FB Informatik Prof. Dr. R.Nitsch ist ein Teile-und-herrsche-Verfahren zum Sortieren beruht auf dem Zerlegen (Partitionieren) eines Feldes in 2 Teile, die es dann unabhängig voneinander sortiert sortiert in-place aber ist nicht stabil Grundidee In einem Feld wird (zunächst) willkürlich ein Trennelement (TE) ausgesucht Alle anderen Elemente werden neu angeordnet: Größere Elemente rechts und kleinere bzw. gleich große Elemente links vom Trennelement. Danach steht das Trennelement an der richtigen Position. Zerlegen des Feldes (partition) Trennelement (willkürlich immer rechts aussen) Feld linkes Teilfeld TE TE rechtes Teilfeld vorher nachher Die gleiche Zerlegung wird nacheinander rekursiv auf das linke und rechte Teilfeld angewendet, d.h. es wird wieder je 1 TE an die richtige Stelle gebracht. Dies wird solange wiederholt, bis es nichts mehr zum Zerlegen gibt: 0 oder 1 Element im Teilfeld! Zum Schluss ist das Feld vollständig sortiert. 28.06.2012 27 quicksort - Implementierung FB Informatik Prof. Dr. R.Nitsch Trennelement (willkürlich immer rechts aussen) Feld linkes Teilfeld Prozedur quicksort (pfirst, plast) pfirst,r: definieren Sortierbereich [pfirst, plast) ptrenn: endgültige Position des Trennzeichens falls plast<=pfirst dann tue nichts sonst ptrenn := partition( pfirst, plast ) quicksort ( pfirst, ptrenn ) quicksort ( ptrenn+1, plast ) ende falls ende prozedur 28.06.2012 vorher nachher rechtes Teilfeld TE pfirst TE plast pt template < typename T > void quicksort( T* pfirst, T* plast ) { if( plast<=pfirst ) return; // base case T* pt = partition( pfirst, plast ); quicksort( pfirst , pt ); quicksort( pt+1 , plast ); return; } 28 partition bei der Arbeit vor 1. SL pl pfirst linkes Teilfeld pfirst TE pl pt 6 3 1 5 FB Informatik Prof. Dr. R.Nitsch pr 7 4 6 3 pl 1 5 pr nachher rechtes Teilfeld vor 2.SL plast T* partition( T* pfirst, T* plast ) { T* pl = pfirst‐1; T* pt = plast‐1; T* pr = plast‐1; while (true) { while( *(++pl)<*pt ); while( *pt<*(‐‐pr) && pr!=pfirst ); if( pl>=pr ) break; std::swap(*pl,*pr); } std::swap( *pl, *pt ); return pl; } 28.06.2012 nach 1. SL vorher pr pt plast TE 4 pl Trennelement (willkürlich immer rechts aussen) Feld 7 1 4 6 3 pl nach 2. SL vor 3. SL nach 3. SL 1 1 1 7 5 pr 4 4 4 6 3 pl pr 3 6 pl pr 3 6 pr ≤ pl 7 5 7 5 7 5 Tausche pl und pt Ende partition SL: Suchlauf 1 4 3 5 7 Endposition! 6 Quicksort bei der Arbeit 4 8 2 7 3 2 5 nach 2 4 3 2 5 8 6 7 pt 2 4 3 2 8 6 7 2 4 3 2 6 7 8 pt 4 3 2 6 8 6 8 (1) terminiert nicht ohne (3) 3 4 Ebene 3 3 3 4 4 Ebene 4 28.06.2012 endgültige Position 6 pt Ebene 2 gewähltes Trennelem. vor partition Ebene 1 T* partition( T* pfirst, T* plast ) { T* pl = pfirst‐1; T* pt = *(plast‐1); T* pr = plast‐1; while (true) { while( *(++pl)<*pt ); //(1) while( *pt<*(‐‐pr) ) //(2) if( pr==pfirst ) break; //(3) if( pl>=pr ) break; //(4) std::swap(*pl,*pr); } std::swap( *pl,*pt ); return pl; } Ebene 0 void quicksort( T* pfirst, T* plast ) { if(plast<=pfirst) return; //leere Folge T* pt = partition( pfirst, plast ); quicksort( pfirst , pt ); quicksort( pt+1 , plast ); return; } FB Informatik Prof. Dr. R.Nitsch 2 3 3 pt pt terminiert mit (4) 30 Kleine Teilfolgen FB Informatik Prof. Dr. R.Nitsch Quicksort ruft sich bei vielen kleinen Teilfolgen selbst auf. Wegen des AufrufOverheads ist das nicht sehr effizient. Enthält eine Teilfolge weniger als M Elemente, ist es besser, die restliche Sortierarbeit durch ein Standardsortierverfahren wie InsertionSort erledigen zu lassen. Die optimale Schwelle M hängt von der Implementierung ab. In der Praxis verwendet wird meist 8≤M ≤32. void quicksortM( T* pfirst, T* plast ) { if(plast‐pfirst<16) { // Kleine Teilfolge insertionSort(pfirst,plast); return; } T*pt = partition( pfirst, plast ); quicksort( pfirst , pt ); quicksort( pt+1, plast ); return; } Kombinierter Quicksort (Hier: M=16) Laufzeit ca. 40% kürzer Alternative Implementierung des Kombinierten Quicksort 28.06.2012 void quicksortI( T* pfirst, T* plast ) { if(plast‐pfirst<16) { return; // Interrupt für kleine Teilfolgen } T* pt = partition( pfirst, plast ); quicksort( pfirst , pt ); quicksort( pt+1, plast ); return; } void hybridsort( T* pfirst, T* plast ) { quicksortI(pfirst,plast); insertionSort(pfirst,plast); } 31 Eigenschaften von Quicksort FB Informatik Prof. Dr. R.Nitsch Quicksort ist ein Teile-und-Herrsche-Verfahren. Verarbeitet Daten "in place" Ist nicht stabil benötigt im ungünstigsten Fall (=bereits sortiert) ca. N2/2 Vergleiche O(N2) Idealfall: Trennelement ist stets der Median des Teilfelds Halbierung der Teilfelder mit jeder Rekursion) O(N·logN) Zuviel Overhead für kleine Teilfelder Es gibt viele Varianten 28.06.2012 33 Quickie zum Thema "Graphen" FB Informatik Prof. Dr. R.Nitsch (gerichteter) Graph: besteht aus Knoten und gerichteten Kanten o Knoten: einfaches Objekt, das einen Namen und andere zugeordnete Informationen enthalten kann. o Kante: Stellt eine Verbindung zwischen zwei Knoten her zeigt eine gerichtete Kante von A nach B so istA direkter Vorgänger von B und B direkter Nachfolger von A Ausgangsgrad eines Knotens A: o(A) := Anzahl seiner direkten Nachfolger Eingangsgrad eines Knotens A i(A) := Anzahl seiner direkten Vorgänger o in G (s. Abb.) ist Knoten 0 direkter Nachfolger von Knoten 3; Knoten 2 hat den Ausgangsgrad 1 und den Eingangsgrad 2. Knoten 4 ist isoliert. Pfad: Ist eine endliche Folgen von Kanten o In G ist p:=30 1 2 ein Pfad mit Länge=3, 3 ist Anfangsknoten und 2 ist Endknoten p heißt Zyklus wenn Anfangsknoten = Endknoten o G enthält den Zyklus 0 1 2 0 28.06.2012 Anwendungen für Graphen: o Routing in Computernetzen, o Navigation im Straßenverkehr, o … 34 Quickie zum Thema "Bäume" FB Informatik Prof. Dr. R.Nitsch Wurzelbaum: ist ein gerichteter Graph mit folgenden Einschränkungen: er hat genau 1 Knoten mit dem Eingangsgrad 0 (Wurzelknoten) alle anderen Knoten haben den Eingangsgrad 1, und er enthält keine Zyklen. Wurzel Innerer Knoten Blatt Wurzel-Baum Blatt: Knoten ohne Nachfolger. Alle anderen werden innere Knoten genannt Tiefe d (Ebene) eines Knotens A: d(A):=Länge des Pfades WurzelA Tiefe des Baumes: d(T):= maximale Tiefe seiner Knoten Ebene 0 Wurzel d=0 Ebene 1 d=1 Ebene 2 d=2 Ebene 3 d=3 Wurzel-Baum 28.06.2012 Anwendungen für Bäume: o Familienstammbäume, o Organigramme o Verzeichnisbäume o Satzanalyse mit Parse-Baum o Verarbeitung von Computersprachen 35 Quickie zum Thema binäre Bäume FB Informatik Prof. Dr. R.Nitsch Binärer Baum: ist ein Wurzelbaum, bei dem jeder Knoten maximal 2 Nachfolger hat. Vollständiger Binärer Baum: ist ein Binärer Baum, bei dem Wurzel Binärer Baum jede Ebene bis auf die unterste vollständig besetzt ist auf der untersten Ebene höchstens Blätter ganz rechts fehlen. ein vollständiger Baum mit N Knoten hat die Tiefe d(T)=int(log2(N)) N=7 Knoten N=11 Knoten Wurzel N=12 Knoten d=0 d=1 d=2 d=3 Anwendungen für binäre Bäume: o Prioritätswarteschlangen, o Heapsort, o Symboltabellen, o … Vollständige Binäre Bäume 28.06.2012 36 Datenstruktur Heap FB Informatik Prof. Dr. R.Nitsch Geordnete Bäume, Schlüsselbäume: die Knoten dieser Bäume sind Objekte, die Schlüsselwerte enthalten, für die eine Ordnungsrelation definiert ist (z.B. < oder ≤) 29 10 39 6 27 92 44 43 2 31 33 11 Binärer Schlüsselbaum Heap-geordneter binärer Schlüsselbaum: der Schlüssel in jedem Knoten ist kleiner oder gleich dem Schlüssel seines Vorgängerknotens. Wurzel enthält den größten Schlüsselwert Ein Heap ist die Arraydarstellung eines vollständigen Heap-geordneten binären Schlüsselbaums (siehe nächste Seite). 28.06.2012 92 27 44 33 29 11 43 31 39 6 2 10 Vollständiger Heap-geordneter binärer Schlüsselbaum 37 Arraydarstellung eines vollständigen binären Schlüsselbaums Die Knoten werden Zeile für Zeile von oben beginnend durchnummeriert und ergeben so die Indices des Arrays a[1,N]. Achtung: das erste Element bekommt den Index 1. Vollständige Binärbäume lassen sich ganz einfach in ein Array abbilden (siehe Abb. rechts). Es bekommen die Wurzel den Index 1 Knoten auf der 2. Ebene die Indices 2 und 3 … Knoten auf Ebene d die Indices 2d-1 bis 2d-1 29 1 39 2 6 27 4 8 31 92 44 9 5 10 2 6 33 11 11 12=N 10 3 43 7 a 29 39 10 6 31 2 43 27 92 44 33 11 1 2 3 4 5 6 7 8 9 10 11 12 Arraydarstellung eines vollständigen binären Schlüsselbaums 92 1 für Algorithmen ist es wichtig, zu jedem Knoten k den direkten Vorgänger bzw. Nachfolger zu kennen: Index des direkten Vorgängers: parent(k) = k div 2 Index des linken Nachfolgers: left(k) = k*2 Index des rechten Nachfolgers: right(k) = k*2+1 FB Informatik Prof. Dr. R.Nitsch 44 2 39 27 8 4 33 6 31 9 10 5 29 2 11 12 11 6 43 3 10 7 a 92 44 43 39 33 11 10 27 6 31 29 2 1 2 3 4 5 6 7 8 9 10 11 12 Heap 28.06.2012 38 Heapsort – Phase 1 FB Informatik Prof. Dr. R.Nitsch ist das komplexeste Verfahren arbeitet in zwei Schritten: Daten in eine bestimmte Struktur bringen (Phase 1) Die vorstrukturierten Daten sortieren (Phase 2) Phase 1: Daten in einen Heap umwandeln Man fängt in der vorletzten Zeile an (Feldlänge/2) und "versickert" falsch platzierte Knoten: Vergleiche den ausgesuchten Knoten mit den beiden darunterliegenden (falls die existieren). Ist er der Größte, dann passiert nichts weiter. sonst vertausche ihn mit dem größten darunterliegenden und versickere ihn von dieser Stelle aus weiter. Gehe zum nächstkleineren Index und fange von vorne an. Beispiel folgt! 28.06.2012 29 1 39 6 27 8 4 2 31 92 44 9 10 5 33 11 11 12 2 6 10 3 43 7 a 29 39 10 6 31 2 43 27 92 44 33 11 1 2 3 4 5 6 7 8 9 10 11 12 39 29 1 Beispiel zu Phase 1: Unsortiertes Array in Heap umwandeln FB Informatik Prof. Dr. R.Nitsch 39 Feldlänge N=12 27 4 8 Berechne N/2 => Erster Index ist 6. "Versickere" die 2: 11 ist größer als 2, also vertausche die beiden. Nächster Index ist 5 "Versickere" die 31: 44 ist die größte Zahl, also vertausche die beiden. Nächster Index ist 4 "Versickere" die 6: 92 ist die größte Zahl, also vertausche die beiden. Nächster Index ist 3 "Versickere" die 10: 43 ist die größte Zahl, also vertausche die beiden. Nächster Index ist 2 "Versickere" die 39: 92 ist die größte Zahl, also vertausche die beiden. An dieser Position ist die 39 größer als die beiden Unterknoten. Kein weiterer Tausch. 2 6 5 92 44 9 2 6 31 10 33 11 11 12 29 1 39 2 92 27 4 8 44 6 31 9 10 5 33 2 11 12 11 6 29 1 92 2 39 27 4 8 44 6 31 9 10 5 33 2 11 12 11 6 92 1 44 Letzter Index ist 1 "Versickere" die 29: Vertauschung mit 92, dann mit 44, dann mit 33. 27 8 28.06.2012 2 39 4 33 6 31 9 10 5 29 2 11 12 11 6 10 3 43 7 10 3 43 7 43 3 10 7 43 3 10 Heap a 92 44 43 39 33 11 10 27 6 31 29 2 1 2 3 4 5 6 7 8 9 10 11 12 7 Heapsort – Phase 2 mit Beispiel FB Informatik Prof. Dr. R.Nitsch Phase 2: Daten sortieren In diesem Schritt geht man davon aus, dass das Feld (oder der äquivalente Baum) Heapstruktur besitzt. Algorithmus: Wiederhole bis Baumgröße = 1 Vertausche im Array das erste mit dem letzten Element. Verkürze das Array um ein Element. (Das letzte Element im Array ist bereits das größte). Versickere das neue erste Element im verkleinerten Baum Beispiel zu Phase 2 2 1 44 2 39 27 4 8 1. 33 5 6 31 9 10 29 92 11 12 11 6 44 1 2. 2 2 39 27 8 4 33 6 31 9 10 5 28.06.2012 29 92 11 12 11 6 43 3 3. 10 2 27 4 8 33 6 31 9 10 5 29 92 11 12 11 6 44 1 7 2 4. 27 2 8 4 33 6 31 9 10 5 2 39 27 4 8 33 5 6 31 9 10 29 2 11 12 43 3 29 92 11 12 11 6 43 3 10 2 7 8 4 1. 2 33 6 31 9 10 5 44 92 11 12 2 4. 27 2 8 4 31 6 43 9 10 11 6 39 1 33 3. 7 10 7 2. 43 1 27 10 11 6 43 3 Heap am Ende von Phase 1 2. 39 10 44 39 39 2 7 43 3 44 1 92 1 5 1. 44 92 11 12 11 6 29 3 10 7 29 3 10 7 41 Heapsort - Implementierung FB Informatik Prof. Dr. R.Nitsch Pseudocode prozedur heapsort( a[], l, r ) N:=r-l Feldlänge pr:=a+l Adresse des 1. Objekts Phase 1: Heap erzeugen für k:=N/2 bis k=1 wiederhole versickere Knoten k ende wiederhole Phase 2: Sortieren solange Feldlänge>1 wiederhole vertausche erstes mit letztem Feldelement Reduziere Feldlänge um 1 versickere 1. Element. ende wiederhole ende prozedur 28.06.2012 C++ Quellcode void heapsort( T a[], int l, int r ) { int N=r‐l; T* pr = a+l; // pointer to root // Phase 1: Heap erzeugen for( int k=N/2; k>=1; ‐‐k ) versickern(pr,k,N); // Phase 2: Sortieren while (N>1) { swap( pr[0], pr[N‐1] ); ‐‐N; versickern(pr,1,N); } } 42 Heapsort - Implementierung Pseudocode prozedur versickern( a[], k, N ) a: Adresse des 1.Objekts eines Arrays mit N Objekten k: Position eines ggf. zu versickernden Parent-Knotens solange ein linker Nachfolger existiert wiederhole merke Position des linken Nachfolgers falls auch ein rechter Nachfolger existiert und falls dieser größer als sein linker Bruder ist merke dessen Position Falls der Nachfolger an der gemerkten Position nicht größer als dessen Vorgänger ist fertig sonst vertausche die beiden Mache den Nachfolger zum nächsten Vorgänger ende falls ende wiederhole ende prozedur 28.06.2012 FB Informatik Prof. Dr. R.Nitsch C++ Quellcode // Versickern des Knotens k von N Knoten void versickern( T a[], int k, int N ) { int& parent = k; int left=2*k; while ( left<=N ) { int greater = left; if ( left<N && a[(left)‐1]<a[(left+1)‐1] ) ++greater; if ( !(a[(parent)‐1]<a[(greater)‐1]) ) break; else { swap(a[(parent)‐1],a[(greater)‐1]); parent=greater; left = 2*parent; } // end if } // end while return; } // end versickern 43 Empirischer Laufzeitvergleich der Sortierverfahren FB Informatik Prof. Dr. R.Nitsch Sortieren eines Arrays von 10000 Objekten (4 Byte/Objekt) Sortiermethode median_qsort (M=32) hybridsort (M=32) quicksort std::sort std::stable_sort heapsort mergesort 28.06.2012 Laufzeit/ms 0,49 0,53 0,64 0,66 0,67 0,71 1,87 rel. Performance 100% 108% 131% 135% 137% 145% 382% 44 Absteigend sortieren FB Informatik Prof. Dr. R.Nitsch Alle Sortieralgorithmen brauchen eine Ordnungsrelation. Diese ist im Standardfall (aufsteigende Sortierung) der '<'-Operator (s. Beispiel) Standardfall: Aufsteigende Sortierung //Sortieren im Bereich [first,last) void bubbleSort( int* pfirst, int* plast ) { for ( int* plastu=plast-1; plastu>pfirst; --plastu ) for ( int* pp=pfirst ; pp<plastu ; ++pp ) if ( *(pp+1)<*pp ) std::swap( *pp, *(pp+1) ); } Nachteil dieser Implementierungen: Sie sind nur eingeschränkt wiederverwendbar (nur aufsteigende oder nur absteigende Sortierung) Absteigende Sortierung Was ist zu ändern? //Sortieren im Bereich [first,last) void bubbleSort( int* pfirst, int* plast ) { for ( int* plastu=plast-1; plastu>pfirst; --plastu ) for ( int* pp=pfirst ; pp<plastu ; ++pp ) if ( *(pp+1)<*pp ) std::swap( *pp, *(pp+1) ); } 28.06.2012 45 Vermeidung der Kopplung zwischen Algorithmus und Ordnungsrelation FB Informatik Prof. Dr. R.Nitsch Universell einsetzbare Sortieralgorithmen indirekt indem sie die Entscheidung über die Sortierreihenfolge an eine bestimmte Funktion delegieren. Diese testet, ob die beiden Operanden in der gewünschten Ordnungsrelation zueinander stehen und gibt das Ergebnis als Wahrheitswert zurück. Beispiel: bool compare( int a1, int a2 ) { return (a1>a2); } //Sortieren im Bereich [first,last) void bubbleSort( int* pfirst, int* plast ) { for ( int* plastu=plast-1; plastu>pfirst; --plastu ) for ( int* pp=pfirst ; pp<plastu ; ++pp ) if ( compare(*pp,*(pp+1))==false ) std::swap( *pp, *(pp+1) ); } Vorteil: Sortieralgorithmus und Ordnungsrelation sind entkoppelt. Nachteil: Der Sortieralgorithmus kann (im selben Programm) nicht nach verschiedenen Ordnungsrelationen sortieren. 28.06.2012 46 Sortieralgorithmus für unterschiedliche Ordnungsrelationen FB Informatik Prof. Dr. R.Nitsch Verbesserung: Die Vergleichsfunktion wird als Funktionszeiger oder Funktionsobjekt übergeben. Beispiel mit Funktionszeiger: //Sortieren im Bereich [first,last) void bubbleSort( int* pfirst, int* plast, bool (*cmp)(int,int) ) { for ( int* plastu=plast-1; plastu>pfirst; --plastu ) for ( int* pp=pfirst ; pp<plastu ; ++pp ) if ( cmp(*pp,*(pp+1))==false ) std::swap( *pp, *(pp+1) ); } Hinweis: Funktionen zum Vergleichen zweier Operanden bezeichnet man auch als binäre Predicate-Funktion. Aufgabe: Implementieren sie eine binäre Predicatefunktion, die auf der Menge der Ganzzahlen vomTyp int die Ordnungsrelation "ist betragsmäßig kleiner als" auswertet. Sortieren Sie das Feld a für nach aufsteigenden Beträgen. int a[10] = { ‐5,‐4,‐3,‐2,‐1,1,2,3,4,5 }; for( int i=0; i<MAX_I; ++i ) { cout << a[i] << endl; } 28.06.2012 47 Generischer Sortieralgorithmus FB Informatik Prof. Dr. R.Nitsch Ein generischer Algorithmus erfüllt seine Aufgabe unabhängig vom Typ der Eingabeobjekte. wird in C++ durch Templates realisiert Aufgabe: Wandeln Sie bubblesort in ein Template um. Elementtyp und Typ des Predicate (Funktionszeiger oder Funktionsobjekt) sollen austauschbar sein. Danach wenden Sie es an. //Sortieren im Bereich [first,last) void bubbleSort( int* pfirst, int* plast, bool (*cmp)(int,int) ) { for ( int* plastu=plast‐1; plastu>pfirst; ‐‐plastu ) for ( int* pp=pfirst ; pp<plastu ; ++pp ) if ( cmp(*pp,*(pp+1))==false ) std::swap( *pp, *(pp+1) ); } int a[10] = { ‐5,‐4,‐3,‐2,‐1,1,2,3,4,5 }; Aufruf mit Funktionszeiger Aufruf mit Funktionsobjekt for( int i=0; i<MAX_I; ++i ) { cout << a[i] << endl; } 28.06.2012 48 Sortieralgorithmen der STL FB Informatik Prof. Dr. R.Nitsch STL stellt 4 generische Sortieralgorithmen zur Verfügung: sort, stable_sort, partial_sort und heap_sort. Alle sind als Funktions-Template implementiert. Vorbedingung: physikalisch sequentielle Container (z.B. C-Array, string, vector, … ) Verwendete Ordnungsrelation: operator< für den Elementtyp (default) oder benutzerdefiniert in Form eines PredicateObjekts oder einer Predicate-Funktion Eigenschaft Zeitkomplexität sort partial_sort stable_sort O(N log N) bis O(N2) O(N log N) O(N log N) bis O(N (log N)2) sort_heap O(N log N) in-situ ja ja ja ja Stabilität nein nein ja nein Hybrid-Sort (mit Quicksort) heap-SortPrinzip merge-SortPrinzip • stable_sort ist etwa 40% langsamer als sort 28.06.2012 49 Anwendung von std::sort FB Informatik Prof. Dr. R.Nitsch a) Mit operator< aufsteigend sortieren #include <algorithm> int a[10] = {5,‐4,+3,‐2,+1,‐1,2,‐3,4,‐5 }; std::sort( a, a+10 ); for( int i=0; i<MAX_I; ++i ) { cout << a[i] << endl; } b) Sortieren mit binärem Predicate #include <algorithm> int a[10] = {5,‐4,+3,‐2,+1,‐1,2,‐3,4,‐5 }; std::sort( a, a+10, less_abs ); Aufruf mit Funktionszeiger std::sort( a, a+10, Less_abs ); Aufruf mit Funktionsobjekt for( int i=0; i<MAX_I; ++i ) { cout << a[i] << endl; } Die Aufrufsyntax für sort und stable_sort ist identisch 28.06.2012 50 Anwendung von std::sort_heap FB Informatik Prof. Dr. R.Nitsch Die beiden Phasen von Heapsort sind in der STL getrennt implementiert. std::make_heap(first,last) stellt die Heapstruktur her. std::sort_heap(first,last) setzt einen Heap voraus und übernimmt die Sortieraufgabe. Beide Funktionen, hintereinander angewendet, erzeugen eine aufsteigende Sortierung Die überladenen Varianten der beiden Funktionstemplates akzeptieren ein binäres Predicate als 3. Parameter. a) Mit operator< aufsteigend sortieren #include <algorithm> int a[10] = {5,‐4,+3,‐2,+1,‐1,2,‐3,4,‐5 }; std::make_heap( a, a+10 ); sort_heap( a, a+10 ); b) Sortieren mit binärem Predicate #include <algorithm> int a[10] = {5,‐4,+3,‐2,+1,‐1,2,‐3,4,‐5 }; std::make_heap( a,a+10,less_abs ); std::sort_heap( a,a+10,less_abs ); Aufruf mit Funktionszeiger std::make_heap( a,a+10,Less_abs() ); std::sort_heap( a,a+10,Less_abs ); Aufruf mit Funktionsobjekt 28.06.2012 51 Null-terminierte Zeichenketten (C-Strings) lexikographisch sortieren FB Informatik Prof. Dr. R.Nitsch Die folgende Anwendung erzeugt eine Liste von 10 Wörtern in einem statischen Container std::array und gibt diese aus. Sortieren Sie diese Liste lexikographisch korrekt (unabhängig von Groß-Klein-Schreibung) #include <array> #include <algorithm> #include <iostream> using namespace std; const int MAX_W=10, MAX_L=32; int main() { array< array<char,MAX_L>, MAX_W > words = { "Zahlen", "EUROPA", "Euro","Ente", "Entenbraten", "entern", "Derby", "derb", "Rettung", "Rettungsschirm" }; std::sort( words.data(), words.data()+MAX_W, Lexikographic_less() ); for( int i=0; i<MAX_W; ++i ) cout << words[i].data() << endl; } 28.06.2012 52 Implementierung der lexikographischen Ordnungsrelation FB Informatik Prof. Dr. R.Nitsch class Lexikographic_less { public: bool operator() ( array<char,MAX_L> op1, array<char,MAX_L> op2 ) { int i=0; // Aktuelle Zeichenposition while( op1[i]!='\0' && op2[i]!='\0' ) { // Solange kein Wortende erreicht ist wiederhole char c1 = tolower( op1[i] ); // lies Zeichen c1=op1[i] und c2=op2[i] von aktueller Pos. char c2 = tolower( op2[i] ); // und wandle Groß‐ in Kleinbuchstaben (Header <cctype>) if(c1<c2) // Falls Buchstabe c1 vor c2 im Alphabet steht return true; // steht op1 vor op2 else if (c1>c2) // sonst falls Buchstabe c1 nach c2 im Alphabet steht return false; // steht op2 vor op1 else // sonst noch keine Entscheidung möglich (c1==c2) ++i; // betrachte nächsten Buchstaben } if( op1[i]!='\0' ) // Falls Zeichenkette op1 länger als op2 ist return false; // steht sie hinter op2 else // sonst return true; // steht sie vor op2 } }; 28.06.2012 53