Fakultät Informatik, Institut für Technische Informatik, Professur Rechnerarchitektur Effiziente Parallele Algorithmen Vorlesung 1 Zellescher Weg 12 Nöthnitzer Straße 46 Willers-Bau A 205 Raum 1044 Tel. +49 351 - 463 - 35450 Tel. +49 351 - 463 - 38246 Wolfgang E. Nagel ([email protected]) Effiziente Parallele Algorithmen ! Lehrveranstaltung jeweils im Wintersemester ! Zeit und Ort: – Dienstag, 5. Doppelstunde (14:50 - 16:20 Uhr), ab 16. Oktober 2012 – INF / E010 ! Studiengänge: – Studiengänge Informatik (Bachelor, Master, Diplom) – Bachelor-Studiengang Medieninformatik – Studiengang Informationssystemtechnik – Master-Studiengang Mathematik und Technomathematik – Module: INF-B-510, INF-B-520, INF-B-530, INF-B-540 und INF-VERT5 ! Umfang: – 2 SWS Vorlesung – 2 SWS Übung ! Form des Abschlusses: Prüfung (mündlich) Wolfgang E. Nagel Effiziente Parallele Algorithmen: Übung ! Betreuung durch Prof. Nagel / Michael Wagner – Jeweils wöchentlich ! Zeit und Ort: – Freitag, 4. Doppelstunde (13:00 - 14:30 Uhr) – Beginn am 26.10.2012 – INF / E 010 ! Prüfung nach Abschluss der Vorlesungszeit Wolfgang E. Nagel Lehrveranstaltungen der Professur Rechnerarchitektur Wintersemester Hochleistungsrechner und ihre Programmierung I 2/2 Prof. Nagel Einführung in die Technische Informatik 4/2/2 Prof. Nagel Konzepte der parallelen Programmierung 2/0 Dr. Trenkler Leistungsanalyse für Rechnersysteme 2/2 Komplexpraktikum Linux-Cluster in Theorie und Praxis 0/0/4 Hochparallele Simulationsrechnungen mit CUDA und OpenCL 2/2 Komplexpraktikum "Paralleles Rechnen" 0/0/4 Hauptseminar Rechnerarchitektur und Programmierung 0/2 Dr. Müller/ Dr. Brunst Prof. Nagel/ DI Georgi Prof. Nagel/ DI Juckeland Prof. Nagel / Dr. Trenkler Prof. Nagel Lecture: Mathematical Biology: Statistical Modelling 2/0 Prof. Deutsch/ Dr. Beyer Wolfgang E. Nagel Effiziente Parallele Algorithmen ! Die Lehrveranstaltungen sind Bestandteil folgender Studiengänge: – Bachelor-Studiengang Informatik (Module: INF-B-510, INF-B-520) – Master-Studiengang Informatik (Module: INF-BAS5, INF-VERT5, INF-MA-PR) – Diplom-Studiengang Informatik (Module: INF-BAS5, INF-VERT5 ) – Bachelor-Studiengang Medieninformatik (Module: INF-B-530, INF-B-540) – Studiengang Informationssystemtechnik (Module: INF-BAS5, INF-VERT5 ) – Master-Studiengang Mathematik und Technomathematik nur die Lehrveranstaltungen • Effiziente parallele Algorithmen • Hochleistungsrechner und ihre Programmierung I • Konzepte der parallelen Programmierung Wolfgang E. Nagel Lehrveranstaltungen der Professur Rechnerarchitektur Hochleistungsrechner und ihre Programmierung I Vorlesung (V2, Prof. Nagel) und Übung (Ü2, Dr. Trenkler) im WS 2012/2013: Mittwoch, 9:20 - 10:50 Uhr - WIL A317 Beginn der Übung: Montag, 6. DS - INF E008 Themenbereiche: – Konzepte der Parallelverarbeitung – Parallele und skalierbare Architekturen • Architekturkonzepte • Leistungsmerkmale • Aktuelle Beispiele - ab 22.10. – Software und die Programmierung • Parallele Programmiermodelle • Programmentwicklung • Betriebssystemunterstützung • Praktische Erfahrungen – anwendungsnahe, interdisziplinär orientierte Programmierung von Parallelrechnern • Algorithmische Fallbeispiele • Nutzung von Werkzeugen Wolfgang E. Nagel Lehrveranstaltungen der Professur Rechnerarchitektur Basismodul : Einführung in die Technische Informatik Vorlesung (jeweils V4/Ü2/P2), (Modul INF-BAS5) Diese Vorlesungsreihe wird als Ringvorlesung von den drei Professoren der Technischen Informatik angeboten. Zeiten im WS 2012/2013: Vorlesung: Donnerstag, 3. und 4. DS - INF E006 Übung: Freitag, 3. DS - INF E001 Praktika: Donnerstag, 6. DS - INF E010 Prof. Spallek - 11.10.2012 bis 08.11.2012 - VLSI-Entwurfssysteme Prof. Pionteck - 15.11.2012 bis 13.12.2012 - Eingebettete System Prof. Nagel - 20.12.2012 bis 01.02.2013 - Parallelverarbeitung Wolfgang E. Nagel Lehrveranstaltungen der Professur Rechnerarchitektur Leistungsanalyse für Rechnersysteme Vorlesung (V2, Dr. Müller, Dr. Brunst) und Übung (Ü2, Dr. Brunst) im WS 2012/2013: Mittwoch, 13.00 – 14.30 Uhr - INF E001 Übung: Donnerstag, 13.00 – 14.30 Uhr - INF E046 Themenbereiche: – Performance-Analyse = Analyse + Computersysteme – „Performance Analyst“ = Mathematiker und Informatiker ! Spezifikation von Performance-Anforderungen ! Evaluierung von Design-Alternativen ! Vergleich von zwei und mehreren Systemen ! Bestimmung eines optimalen Wertes für einen Parameter (System Tuning) ! Identifikation von Engpässen ! Charakterisierung der Last auf einem System ! Bestimmung der Anzahl und der Größe von Komponenten ! Vorhersage der Performance für zukünftige Lasten Wolfgang E. Nagel Lehrveranstaltungen der Professur Rechnerarchitektur Konzepte der parallelen Programmierung Vorlesung (V2, Dr. Trenkler ) im WS 2012/2013: Freitag, 11.10 – 12.40 Uhr - INF E007 Themenbereiche: ! Parallele Maschinenmodelle ! Parallele Programmiermodelle ! Entwurf von paralleler Software ! MPI (Message Passing Interface) ! OpenMP ! CAF (Co-Array Fortran) ! Gegenüberstellung von MPI, OpenMP und CAF Wolfgang E. Nagel Lehrveranstaltungen der Professur Rechnerarchitektur Linux-Cluster in Theorie und Praxis Vorlesung (P4, Prof. Nagel/ DI Georgi) im WS 2012/2013: Montag und Donnerstag, 14.50 – 16.20 Uhr - INF E040 Themenbereiche: ! Aufbau eines Linux-Clusters unter Anleitung ! Dazu gehört: • Installation von Betriebssystem und Systemsoftware • Konfiguration und Administration der einzelnen Softwarekomponenten • Leistungsuntersuchung des laufenden Systems • Hardware-Aufbau ! Die praktische Umsetzung der Projektphasen erfolgt jeweils nach einer theoretischen Einführung in die zu bearbeitende Problematik Wolfgang E. Nagel Lehrveranstaltungen der Professur Rechnerarchitektur Hochparallele Simulationsrechnungen mit CUDA und OpenCL Vorlesung (V2/Ü2, Prof. Nagel / DI Juckeland) im WS 2012/2013: Vorlesung: Montag, 11.10 – 12.40 Uhr - INF E069 Übung: Dienstag, 11.10 – 12.40 Uhr - INF E069 Themenbereiche: ! Simulationen bzw. Berechnungen mit hochparallelen Prozessoren ! Untergliederung in: • Einführung GPU Computing und CUDA • Hard-/Software-Ökosystem • Speichermanagement • Parallele Algorithmen • Performance Tuning • OpenCL • Fallstudien Wolfgang E. Nagel Lehrveranstaltungen der Professur Rechnerarchitektur Mathematical Biology: Statistical Modelling Vorlesung (Prof. Deutsch, Dr. A. Beyer) im WS 2012/2013: Dienstag, 14.50 – 16.20 Uhr - Biotec - CRTD Raum 2 (Beginn 16.10.2012) Themenbereiche: ! Statistische Methoden, t-Test, Lineare Regression ! Methoden des Maschinenlernens, Clusteranalyse, Zeitreihenanalyse ! jeweils mit Bezug auf reale biologische Daten ! Einführung in die experimentellen Techniken zur Erhebung der biologischen Datensätze Wolfgang E. Nagel Lehrveranstaltungen der Professur Rechnerarchitektur Hauptseminar Rechnerarchitektur und Programmierung ! Vorbesprechung/ Eröffnung: 16.10.2012 - 17.00 Uhr, INF 1005 ! Beginn der Veranstaltung: Später im Semester, je nach Belegung, evtl. Blockcharakter ! Themen – Speichersysteme – Prozessoren – Verbindungsstrukturen – Programmierung Wolfgang E. Nagel Lehrveranstaltungen der Professur Rechnerarchitektur Sommersemester ! Hochleistungsrechner und ihre Programmierung II 2/2 Prof. Nagel ! Rechnerarchitektur II 2/2 Prof. Nagel ! Struktur und Operationsprinzip von Prozessoren 2/0 Prof. Nagel DI Juckeland ! Verbindungseinrichtungen in parallelen Systemen 2/0 Prof. Nagel DI Georgi ! Proseminar Rechnerarchitektur 0/2 Prof. Nagel ! Hauptseminar Rechnerarchitektur und Programmierung 0/2 Prof. Nagel ! Mathematische Biologie 2/2 Prof. Deutsch Seminar: Biology and Mathematics of Brain Tumours Dr. Beyer Dr. Klink Wolfgang E. Nagel Lehrveranstaltungen der Professur Rechnerarchitektur Hochleistungsrechner und ihre Programmierung II Vorlesung (jeweils V2/Ü2) wieder im SS 2013 Themenbereiche: ! Konzepte der Parallelverarbeitung ! Software und die Programmierung ! Parallele und skalierbare Architekturen – Parallele Programmiermodelle – Architekturkonzepte – Programmentwicklung – Leistungsmerkmale – Betriebssystemunterstützung – Aktuelle Beispiele – Praktische Erfahrungen ! Anwendungsnahe, interdisziplinär orientierte Programmierung von Parallelrechnern – Algorithmische Fallbeispiele – Nutzung von Werkzeugen Wolfgang E. Nagel Lehrveranstaltungen der Professur Rechnerarchitektur Rechnerarchitektur II Vorlesung (jeweils V2/Ü2), wieder im SS 2013 Themenbereiche: ! Definition und Prinzipien ! Historischer Rückblick ! Klassifizierungen ! Beschreibungen ! Möglichkeiten zur Leistungssteigerung ! Statische und dynamische Verbindungseinrichtungen ! Architektonische Modelle der Parallelverarbeitung ! Leistungsbewertung ! Zuverlässigkeit ! Entwicklungstendenzen und Ausblick Wolfgang E. Nagel Lehrveranstaltungen der Professur Rechnerarchitektur Struktur und Operationsprinzip von Prozessoren (DI Juckeland) Vorlesung (jeweils V2) wieder im SS 2013 Themenbereiche: ! Rechnerarchitektur-Definition nach Giloi ! Unterscheidungskriterien von Prozessorarchitekturen ! Registersätze ! Datentypen und -zugriff ! Adressierungsarten ! Befehlsformate ! Befehlsgruppen ! Varianten der Mikroarchitektur ! Cache- und Speicherorganisation ! Busprotokoll ! Familienkonzepte und Entwicklungslinien ! Untersuchung praxisrelevanter Prozessorfamilien: MIPS, SPARC, IBM POWER, PowerPC, IA-32, Intel64, AMD64, IA-64, IBM Cell (CBEA) ! Trends ! Ausnahmeverarbeitung Wolfgang E. Nagel Lehrveranstaltungen der Professur Rechnerarchitektur Biology and Mathematics of Brain Tumours Seminar, Englisch Dozenten: Prof. Deutsch und Dr. Beyer, Dr. Klink wieder im SS 2013 Themenbereiche: ! Brain Tumourigenesis and Progression ! Tumour Molecular Signalling ! Brain Tumour Invasion ! Brain Tumour Prognosis and Therapies Wolfgang E. Nagel Fakultät Informatik, Institut für Technische Informatik, Professur Rechnerarchitektur Einführung Zellescher Weg 12 Nöthnitzer Straße 46 Willers-Bau A 205 Raum 1044 Tel. +49 351 - 463 - 35450 Tel. +49 351 - 463 - 38246 Wolfgang E. Nagel ([email protected]) Münzfernsprecher Wolfgang E. Nagel Rezept für Tzatziki Zutaten: Zubereitung: – 1 Salatgurke – Salz – 500 g Dickmilch – 3 Essl. Olivenöl – 1 Essl. Essig – 1 Zwiebel – 2-3 Knoblauchzehen – Pfeffer – Dill – Gurke schälen, halbieren, die Kerne mit einem Teelöffel herauskratzen. Gurke grob raffeln, leicht salzen und beiseite stellen. – Dickmilch mit Öl und Essig verrühren, Zwiebel pellen, fein reiben und dazugeben. Knoblauch pellen und durch die Presse ebenfalls dazugeben. Gurkenraspeln in einem Tuch ausdrücken und mit der Sauce verrühren. Mit Pfeffer und Salz abschmecken, mit gehacktem Dill verrühren. Wolfgang E. Nagel Understanding Computer Technology Wolfgang E. Nagel Der Algorithmusbegriff Der Begriff Algorithmus wird in der Informatik verwendet, um eine Lösungsvorschrift für ein Problem zu beschreiben, die sich für eine Implementierung als Computerprogramm eignet. Definition: Ein Algorithmus ist eine endliche Folge von Instruktionen (Verarbeitungsschritten), bei der jede Instruktion eine klare Bedeutung hat und in endlicher Zeit ausgeführt werden kann. ♦ Beispiele für Instruktionen: – x = x + z; – if a > 0 goto Label1; Wolfgang E. Nagel Beispiele für Anwendungsgebiete (1) 1. Anwendungsprogramme ! Lineare Algebra (dicht- oder dünnbesetzte Felder) Werte Indizes 1 2 1 7 5 3 ! Verarbeitung von Personen-/Kontendaten char Vorname[20]; char Nachname[20]; char Postleitzahl[5]; char Ort[20]; Wolfgang E. Nagel 8 7 8 3.1 1.5 1.7 3.0 0.3 2.0 1.7 0.5 6.3 Beispiele für Anwendungsgebiete (2) 2. Compiler ! Erstellung von Listen für alle verwendeten Symbole ! Ziel: a) schnelles Suchen b) kompakte Speicherung 3. Betriebssysteme ! Verwaltung von Warteschlangen (Queues) z. B. Zeitscheibenverfahren oder Batch ! Betriebsmittelvergabe allgemein, z.B. Verwaltung von Speicherbereiche (Stack) Wolfgang E. Nagel Eigenschaften eines Algorithmus ! Endlichkeit: Der Algorithmus stoppt nach einer endlichen Anzahl von Schritten (Operationen). ! Definitheit: Die Regeln sind eindeutig und präzise (deterministisch), die Reihenfolge der durchzuführenden Operationen ist (ggf. in Abhängigkeit von den Eingabedaten) eindeutig festgelegt. ! Anfangszustand: Der Algorithmus beginnt mit einem eindeutig definierten Anfangszustand, z.B. haben die Eingabedaten einen wohldefinierten Wert; der Startwert des Algorithmus ist damit festgelegt. Daten, die im Algorithmus verarbeitet werden, haben definierte Werte. ! Endzustand: Es ist eindeutig festgelegt, unter welchen Bedingungen der Algorithmus stoppt; die vom Algorithmus erzeugten Ausgabedaten sind wohldefiniert. Wolfgang E. Nagel Darstellung der Algorithmen 1. Verbale Umschreibung (Handlungsanweisung) 2. Evtl. höhere Programmiersprache (inkl.. Pseudocode) 3. Nassi-Shneiderman-Struktogramme o. ä. graphische Darstellungen 4. Pre-Post-Conditions Wolfgang E. Nagel Pre-Condition Bedingung, die erfüllt sein muss, damit die spezifizierte Operation korrekt ausführbar ist. Ist diese Bedingung nicht erfüllt, ist das Ergebnis der Operation undefiniert. Dies führt z.B. zu ! Fehlermeldung, ! Programmabbruch oder ! inkorrekten Werten. Aus diesem Grund muss vor der Ausführung der Operation sichergestellt werden, dass die Pre-Condition erfüllt ist. Wolfgang E. Nagel Post-Condition Beschreibt das System nach Ausführung der Operation. Dabei werden nur die Teile beschrieben, die durch die Operation u.U. manipuliert werden. Es werden keine Angaben gemacht, wie die Zustandstransformation erreicht werden muss. Es wird nur beschrieben, was erreicht werden muss. Wolfgang E. Nagel Abstrakter atomarer Datentyp color 1. Wertebereich { rot, grün, blau, gelb, orange, violett } 2. Operationen mix (c1,c2 : color) : color pre c1 und c2 sind Primärfarben post mix ist die Farbe, die aus Mischen von c1 und c2 entsteht primary (c : color) : boolean post IF c ist Primärfarbe THEN primary ist true ELSE primary ist false form (c : color; var c1, c2 : color) pre c ist keine Primärfarbe post c1 und c2 sind die beiden Primärfarben aus denen c besteht assign (var c1 : color; c2 : color) post c1 hat den Wert c2 Wolfgang E. Nagel Entwurf von Algorithmen ! Top-down generelle Strategie zuerst, Details am Schluss, d.h. schrittweise Verfeinerung vom (abstrakten) Modell zum (konkreten) Programm ! Bottom-up Grundfunktionen zuerst entwerfen, übergeordnete Funktionen hierauf aufbauen, d.h. zusammensetzen vorhandener Teile zu neuen Bausteinen Wolfgang E. Nagel Top-Down-Entwurf Problem Eingabe Ausgabe Wolfgang E. Nagel Bottom-Up-Entwurf String Vektor X11-Oberfläche Wolfgang E. Nagel Strukturierte Programmierung ! Hilfsmittel zur Problemlösung ! Top-Down-Entwurf von Algorithmen und Datenstrukturen ! Black-box als Konzept: – ´Was macht sie?´ ; – nicht: ´Wie macht sie es?´ – Hidden information zwischen Black-boxes – Black-boxes bilden unabhängige Module – Datenabstraktion als Konzept Wolfgang E. Nagel Was ist eine gute Lösung eines Problems? ! Summe aller Kosten während der Lebensdauer eines Programms minimal Einflussfaktoren: – CPU-Zeit – Mensch-Maschine Interaktion – Debugging bei der Fehlersuche – Wartung ! Das schnellere Programm ist nicht immer besser Wolfgang E. Nagel Schlüsselworte für die Programmierung ! Modularität durch Top-Down-Entwurf ! Verfeinerungsprozess ! Verteilung des Problems bei der Programmkonstruktion, kleine unabhängige Aufgaben ! Debugging (Fehlersuche) ist in kleinen Teilen wesentlich einfacher ! Lesbarkeit des Programms wird erhöht ! Modifikation einer Datenstruktur einfacher (nur lokale Änderungen) ! Elimination von redundantem Programm-Code (Unterprogramm-Aufrufe) Wolfgang E. Nagel Programmierstil I 1. Nutzung von Unterprogrammen (vernünftige Partitionierung und Namensgebung) unter Wahrung der Übersichtlichkeit vorteilhaft für – Wartung des Programms – Fehlersuche im Programm 2. Vermeidung von globalen Variablen in Unterprogrammen – nur dann ´global´, wenn ´global der Natur nach´ (z.B. globale Konstante nur einmal im Programm vereinbart) – Kompromisse in Abhängigkeit von Programmiersprache u.U. notwendig 3. Nutzung von ´call-by-reference´-Parametern bei Feldern und Strukturen – Rückgabevariablen – Overhead durch Kopieren wird verhindert – Fehlersuche u.U. etwas schwieriger Wolfgang E. Nagel Programmierstil II 4. Nutzung von Funktionen ausschließlich ohne Seiteneffekte – Keine Zuweisungen zu ´call-by-reference´-Parametern – Kein I/O – Keine Zuweisungen zu globalen Variablen 5. Keine Nutzung von GOTOs – Zumindest für diese Vorlesung bindend 6. Lesbarkeit von Programmen – gute, übersichtliche Struktur – vernünftige Wahl der Variablennamen – gute Programmaufteilung – Einrückung – Leerzeilen – Kommentierung Wolfgang E. Nagel Programmierstil III 7. Compiler Arbeit übernehmen lassen – z.B. -Wall bei manchen Compilern einschalten, um Warnungen zu aktivieren (und Meldungen ernst nehmen) – Funktionsprototypen in C verwenden (-Wall gibt ansonsten Warnung aus) – Assertions in C verwenden #include <assert.h> assert(x > 0); – globale Funktionen, Typen, Variablen, Konstanten in .h-Datei definieren und diese Datei in andere Dateien importieren (include), die diese Funktionen etc. benötigen – nicht-globale Funktionen etc. so definieren, dass sie nur in dieser Datei sichtbar sind static int foo(void) {...} Wolfgang E. Nagel Programmierstil IV 8. Dokumentation a) Header für das Hauptprogramm sollte enthalten: – Zweck des Programms – Autor (Name, Vorname), Datum der Erstellung, wo zu erreichen – Kurze Beschreibung der globalen Algorithmen und Datenstrukturen – Kurze Beschreibung für die Nutzung des Programms – Beschreibung der Schlüsselvariablen – Wichtig: Angaben über Annahmen, die das Programm z.B. über Eingaben macht a) Mini-Header für jedes Modul (Prozedur) b) Kommentare innerhalb der Module zur Erklärung wichtiger oder unübersichtlicher Teile des Programms Wolfgang E. Nagel Programmierstil V 9. Debugging – Interaktiver Debugger – Write-Anweisungen, am Anfang und Ende von Unterprogrammen – Write-Anweisungen, am Anfang und Ende von Schleifen – Write-Anweisungen in IF-Anweisungen Wolfgang E. Nagel Problem Problem Eingabe Verarbeitung Wolfgang E. Nagel Ausgabe Nassi-Shneiderman-Struktogramme ! dienen der Dokumentation und der Veranschaulichung von Programmen ! haben als Grundelemente die wesentlichen Kontrollfluss-Strukturen von Programmiersprachen Wolfgang E. Nagel Sequenz Aktion 1 Aktion 2 Aktion n Wolfgang E. Nagel Einfache Auswahl (IF-Abfrage) Bedingung false true Aktion 1 Aktion 2 Wolfgang E. Nagel Mehrfache Auswahl (Case-Abfrage) Auswahl Fall 1 2 Aktion 1 n Aktion 2 Aktion . . . Wolfgang E. Nagel n in C if(condition) in Fortran 90 [name:] IF(condition) THEN statement; statement else ELSE statement; statement END IF [name] switch(expression) { [name:] SELECT CASE(expression) CASE selector constant: statement; statement break; CASE DEFAULT default: statement; statement } END SELECT [name] Wolfgang E. Nagel Iteration (While-Schleife) WHILE Bedingung Aktion Wolfgang E. Nagel Iteration (Repeat-Schleife) Aktion UNTIL Bedingung Wolfgang E. Nagel Iteration (for-Schleife) for index:=anfang TO ende DO Aktion Wolfgang E. Nagel in C in Fortran 90 for (var = start ; var != end ;++ var) [name:] DO var = start, end[,step] statement statement ; END DO [name] [name:] DO IF(.NOT.cond) EXIT statement END DO [name] while (cond) statement ; do oder [name:] DO WHILE (cond) statement END DO [name] [name:] DO statement IF (condition) EXIT END DO [name] statement ; while (condition) ; Wolfgang E. Nagel Beispiel list _ index bin_search ( keytype key ) low = 1 high = n element = 0 (element == 0 ) && ( low <= high ) i = (low +high ) /2 middle = list [i ].key key > middle T F key < middle low = i + 1 TRUE FALSE high = i - 1 element = i return element Wolfgang E. Nagel Registermaschine (RAM) Programm 0: LOAD 0 1: ADD 4 2: STORE 1 3: STOP Akkumulator 3 Speicher 0: 3 1: 0 Programmzähler 2 ... Befehlssatz: - LOAD, STORE immediate/direct/indirect - ADD, SUB register - JUMP label - JUMP>0, JUMP=0 label Wolfgang E. Nagel 2: 0 ... Aufwand eines RAM-Programmes ! Jeder ausgeführte Befehl zählt als ein Rechenschritt ! Speicherplatzbedarf entspricht den zusätzlich zur Eingabe benutzten Speicherzellen Wolfgang E. Nagel Effizienz eines Algorithmus Mehrere Möglichkeiten für das ´´Messen´´ der Effizienz eines Algorithmus: ! ´´Programmieren´´ des Algorithmus in einer künstlichen Programmiersprache und zählen und gewichten der Operationen, die nötig sind, um eine bestimmte Problemgröße zu lösen ! Programmieren des Algorithmus auf einem realen Rechner und messen des Zeitverbrauchs ! Zählen der Operationen auf einem sehr hohen Niveau, z.B. Anzahl der ´´zu betrachtenden´´ Elemente beim Suchen in einer Liste, oder Anzahl der zu vertauschenden Elementpaare beim Sortieren einer Liste Wolfgang E. Nagel Definition: Eine Funktion f : M → N heißt berechenbar, wenn es einen Algorithmus gibt, der für jeden Eingabewert m ∈ M, für den f(m) definiert ist, nach endlich vielen Schritten anhält und das Ergebnis f(m) liefert; in allen Fällen, in denen f(m) nicht definiert ist, bricht der Algorithmus nicht ab. ♦ Komplexität einer berechenbaren Funktion: Der zu ihrer Berechnung erforderliche Aufwand an Betriebsmitteln (Speicherplatz, Rechenzeit, benötigte Geräte, usw.) Untersuchung des Rechenaufwandes soll unabhängig von speziellen Computern sein → formales Berechnungsmodell z.B. Turingmaschine Wolfgang E. Nagel ! Die Komplexität eines Algorithmus ist der erforderliche Rechenaufwand bei einer konkreten Realisierung des Algorithmus innerhalb des Berechnungsmodells. ! Die Komplexität einer Funktion ist die Komplexität des bestmöglichen Algorithmus der Menge aller Algorithmen, die die Funktion berechnen. ! Untersucht wird Zeitverhalten und Speicherplatzbedarf eines Algorithmus. ! Laufzeit eines Algorithmus: Anzahl der Rechenschritte, die bei Durchführung für eine Eingabe gemacht werden. Wolfgang E. Nagel Zusammenhang Algorithmus - Funktion Sei A ein Algorithmus, der die Funktion f berechnet. Dann ist die Komplexität von A eine obere Schranke für die Komplexität von f. Umgekehrt ist die Komplexität von f eine untere Schranke für die Komplexität von A. Fallen untere und obere Schranke zusammen, so hat man einen optimalen Algorithmus für das gestellte Problem. Wolfgang E. Nagel Abstrakte Maschinen ! Turingmaschine: – Rechenschritt: eine Anwendung der Übergangsfunktion δ – Speicherplatzbedarf: Zahl der benötigten Speicherzellen (zusätzlich zur Eingabe) ! RAM: – Rechenschritt: Ausführung eines Befehls – Speicherplatzbedarf: Zahl der benötigten Speicherzellen (zusätzlich zur Eingabe) Wolfgang E. Nagel Definition: Sei f : N→ R+ eine Funktion. Dann sind folgende Funktionenmengen definiert: O(f) = {g: N→ R+ | ∃ n0 ∈ N, ∃ c ∈ R+ mit g(n)≤ c f(n) ∀ n≥n0 } Ω(f) = {g: N→ R+ | ∃ n0 ∈ N, ∃ c ∈ R+ mit g(n)≥ c f(n) ∀ n≥n0 } ♦ Wolfgang E. Nagel Bemerkungen: ! O(f) ist eine Funktionenklasse, nämlich die Menge der Funktionen, die asymptotisch höchstens so schnell wachsen wie c f. ! Ω(f) ist eine Funktionenklasse, nämlich die Menge der Funktionen, die asymptotisch mindestens so schnell wachsen wie c f. ! g ∈ O(f) heißt nicht, dass g(n) ≤ f(n) für alle n gilt. Wolfgang E. Nagel Sei n Problemgröße, A Algorithmus. Der Zeitbedarf eines Algorithmus lässt sich darstellen als eine Zeitfunktion TA(n) mit TA: N→ R+ Definition: Ein Algorithmus hat die Komplexität O(f), wenn TA ∈ O(f) gilt. ♦ Bemerkung: Die Zeitfunktion TA hängt i.allg. von der Eingabe des Algorithmus ab, z.B. bei einem Sortieralgorithmus, ob die zu sortierenden Elemente bereits vorsortiert vorliegen. Wolfgang E. Nagel Beispiel ! TA(n) = n(n-1)/2 – TA(n) ∈ O(n2), denn: – n(n-1)/2 = 1/2 (n2-n) ≤ n2-n (für n ≥ 1) ≤ n2 – mit n0=1, c=1 erfüllt TA(n) die Definition ! TA(n) = n2+2n – TA(n) ∈ O(n2), denn: – n2+2n = n2+n+n ≤ n2+ n2+ n2 = 3n2 – mit n0=1, c=3 ist die Definition erfüllt Dennoch ist ein Algorithmus mit Aufwand n(n-1)/2 besser als ein Algorithmus mit n2+2n! Wolfgang E. Nagel Bemerkungen ! Man sagt üblicherweise f(n) ∈ O(log n) statt f ∈ O(log). ! Typische Zeitschranken: log2 n, n, n log2 n, n2, n3, 2n, nn ! Es gilt: O(log n) < O(n) < O(n log n) < O(n2) < O(2n) < O(nn) ! Falls f(n) ∈ O(s(n)), g(n) ∈ O(r(n)), dann gilt: f(n) + g(n) ∈ O(s(n)+r(n)) ! Falls f(n) ∈ O(s(n)), g(n) ∈ O(r(n)), dann gilt: f(n) • g(n) ∈ O(s(n) • r(n)) Wolfgang E. Nagel Bezeichnungen Festlegung: f ist konstant, wenn f ∈ O(1) f wächst logarithmisch, wenn f(n) ∈ O(log n) f wächst linear, wenn f(n) ∈ O(n) f wächst quadratisch, wenn f(n) ∈ O(n2) f wächst polynomiell, wenn f(n) ∈ O(nk) für ein k ∈ N f wächst exponentiell, wenn f(n) ∈ O(2cn) für ein c ∈ R+ Wolfgang E. Nagel Laufzeitverhalten von Algorithmen Algorithmus A1 Laufzeit (ms) 1000 N am schnellsten für max. Problemgröße (3600 sec) N>=149 3600 90<=N<=148 1500 A2 200 N log N A3 10 N² 10<=N<=89 600 A4 30 N³ nie 50 A5 2N 1<=N<=9 22 Wolfgang E. Nagel Laufzeitverhalten von Algorithmen (Rechner 10x schneller) Algorithmus A1 Laufzeit (ms) 1000 N max. Problemgröße (3600 sec) Faktor 36000 10 A2 200 N log N 13500 9 A3 10 N² 1900 3,166 A4 30 N³ 106 2,012 A5 2N 25 Wolfgang E. Nagel (+3) 1,136 Komplexität ! Man unterscheidet den Zeitbedarf im schlechtesten Fall (worst case), im Mittel (average case) und im besten Fall (best case) ! Bei der Komplexitätsanalyse wird i.Allg. der schlechteste Fall betrachtet (worst-case Analyse). Wolfgang E. Nagel Fakultät Informatik, Institut für Technische Informatik, Professur Rechnerarchitektur Effiziente Parallele Algorithmen Datenstrukturen Teil I Zellescher Weg 12 Nöthnitzer Straße 46 Willers-Bau A 205 Raum 1044 Tel. +49 351 - 463 - 35450 Tel. +49 351 - 463 - 38246 Wolfgang E. Nagel ([email protected]) Datentyp Definition: Ein Datentyp ist aus zwei Angaben festgelegt: 1. eine Menge von Datenobjekten (Werte); 2. eine Menge von Operationen auf diesen Datenobjekten. ♦ Wolfgang E. Nagel Datenstruktur Definition: Eine Datenstruktur ist ein Datentyp mit den folgenden Eigenschaften: 1. Sie kann in eine Menge von zusammenhängenden Datenelementen zerlegt werden, wobei jedes dieser Elemente ein atomarer Datentyp oder wieder eine eigene Datenstruktur ist. 2. Sie setzt die Elemente durch eine Menge von Regeln (eine Struktur) in eine Beziehung. ♦ Wolfgang E. Nagel Abstraktionsniveau bei Datentypen Abstrakter Datentyp – Datentyp, der nur in der Vorstellung des Programmentwicklers entsteht. – Gibt die wichtigsten Eigenschaften ohne Einzelheiten oder Nebenbedingungen der Realisierung an. – Umsetzung in Programmiersprache Virtueller Datentyp Datentyp in einer höheren Programmiersprache ↓ Durch Compiler und Betriebssystem Physikalischer Datentyp Datentyp, der auf einem physikalischen Prozessor existiert Wolfgang E. Nagel Spezifikation eines strukturierten abstrakten Datentyps Struktur Elemente Wertebereich Operationen Spezifikation Repräsentation Implementation Wolfgang E. Nagel Physikalische Speicherung von Daten 1. Arbeitsspeicher (Hauptspeicher) – Speicher mit wahlfreiem Zugriff (random access) – Zugriff auf Daten über Adressen – Wort- oder Byte-orientierte direkte Adressierung – Im Vergleich zu externen Speichern relativ kurze Zugriffszeit (Größenordnung 30-50 ns-Bereich) – Aus Kostengründen und im Vergleich zu externen Speichern relativ geringe Speicherkapazität (Wenige Gigabytes) Wolfgang E. Nagel Physikalische Speicherung von Daten 2. Externer Speicher – Magnetbänder • Daten werden sequentiell gespeichert • Im Vergleich zum Hauptspeicher große Speicherkapazität (ca. 400 GByte – 1 TByte) • Langsamer Zugriff, zum Teil manueller Eingriff notwendig – Magnetplatte • Daten sind blockweise gespeichert, Blöcke wahlfrei zugreifbar • Große Speicherkapazität (bis zu mehreren TBytes) • Im Vergleich zum Arbeitsspeicher lange Zugriffszeiten (einige ms) Wolfgang E. Nagel Elementare Strukturrelationen " Menge (Set) – Elemente stehen nur insoweit in Beziehung, als sie zu einer Menge gehören " Lineare Struktur – Elemente stehen jeweils zu einem anderen Element in Beziehung (oneto-one Relation) " Baumstruktur – Elemente stehen zu mehreren anderen Elementen in Beziehung (one-to-many Relation) " Graph- oder Netzwerkstruktur – Viele Elemente stehen mit vielen anderen Elementen in Beziehung (many-to-many Relation) Wolfgang E. Nagel Homogene/heterogene Datenstruktur Definition: 1. In einer homogenen Datenstruktur haben alle Komponenten den gleichen Datentyp. 2. In einer heterogenen Datenstruktur haben die Komponenten unterschiedliche Datentypen. ♦ Wolfgang E. Nagel Schlüsselmenge Definition: Eine Schlüsselmenge ist eine linear geordnete Menge S = {s1, ..., sn} . ♦ Bemerkung: – Schlüssel dienen der eindeutigen Identifikation von Daten und werden i.A. zusammen mit den Daten abgespeichert oder sind Bestandteil der Daten. – Schlüssel müssen nicht Zahlen sein. Auch Namen können z.B. als Schlüssel verwendet werden (lexikographische Sortierung). Beispiele: – Kontonummer – Nummer eines Versicherungsvertrages – Personalnummer, Postleitzahlen Wolfgang E. Nagel Datenstruktur Feld Definition: Ein Feld ist eine Kette von Elementen. 1. Elemente Durch Festlegung eines Komponententyps ist der Typ des Elementes festgelegt. Da alle Elemente den gleichen Typ haben, ist ein Feld eine homogene Datenstruktur. 2. Struktur Ein Indextyp, der linear sein muss, legt den Wertebereich der Indexwerte fest. Die Indexwerte stehen in einer 1-1 Relation zu den Komponentenwerten, jeder Indexwert identifiziert genau einen Komponentenwert. Wolfgang E. Nagel Datenstruktur Feld 3. Operationen retrieve ( arraytype S, elemtype C, int i1 , ... , int in ) gibt der Variablen C den Wert von S, der mit dem Index i1 , ... , in korrespondiert. Beispiel in C: C = S[i1]...[in]; update ( arraytype S, elemtype C, int i1 , ..., int in ) speichert den Wert der Variablen C auf die Komponente von S, die mit dem Index i1 , ..., in korrespondiert. Beispiel in C: S[i1]...[in] = C; Wolfgang E. Nagel Mögliche Fehlerquellen retrieve – Index ist nicht vom zulässigen Typ. – Undefinierte Werte in der Feldkomponente. – Komponententyp und Typ von C stimmen nicht überein. update – Index nicht vom zulässigen Typ. – Undefinierter Wert in der Zuweisungsvariablen. – Komponententyp und Typ von C stimmen nicht überein. – Index liegt außerhalb des zugehörigen Typs, eine Zuweisung auf – Feldelemente außerhalb der zulässigen Grenzen wird z.B. in – FORTRAN im Regelfall nicht überprüft. Wolfgang E. Nagel Speicherung von eindimensionalen Feldern " Datenelemente werden hintereinander im Speicher abgelegt " Speicheradresse von a[i] berechnet sich (in C) durch (Basisadresse + Offset): &a[0] + i*sizeof( a[0] ) Beispiel: int a[6]; wobei ein int eine 32-Bit Zahl (4 Bytes) sein soll a[0] a[1] a[2] a[3] a[4] a[5] Adresse von a[3], wenn Feld ab Adresse 40 beginnt: 40 + 3*4 = 52 Wolfgang E. Nagel Speicherung von mehrdimensionalen Feldern " Datenelemente werden hintereinander im Speicher abgelegt " in C, Java u.a. zeilenweise; in Fortran spaltenweise " Speicheradresse von a[i1]...[in] berechnet sich (in C) durch &a[0] + ( ... ( i1 * N2 + i2 ) * N3 +... ) Nk + ik ) * sizeof( a[0] ) Beispiel (zeilenweise Speicherung): int a[2][3]; wobei ein int eine 32-Bit Zahl (4 Bytes) sein soll a[0][0] a[0][1] a[0][2] a[1][0] a[1][1] a[1][2] Adresse von a[1][2], wenn Feld ab Adresse 40 beginnt:40 + (1*3+2)*4 = 60 Wolfgang E. Nagel Datenstruktur Zeiger Definition: Ein Zeiger (Pointer) ist ein Datentyp, dessen Werte die Adressen von Repräsentationen anderer Datentypen sind. 1. Elemente Die Elemente sind die Werte aller Speicheradressen einschließlich des Wertes NULL (undefiniert). 2. Struktur keine Wolfgang E. Nagel Datenstruktur Zeiger 3. Operationen (in C) Assignment = Relational == und != Dynamic malloc, calloc, realloc, free (C++: new, delete, new[], delete[]) Dereferenzierung * (sowie -> und []) Wolfgang E. Nagel Datenstruktur Record Definition: Ein record ist eine Gruppierung von unterschiedlichen Datenelementen zu einer Einheit. 1. Elemente Jedes Komponentenelement benötigt die Angabe eines eigenen Datentyps. Da die Komponenten unterschiedliche Datentypen haben können, ist ein record eine heterogene Datenstruktur. 2. Struktur Eine Liste von Namen (identifier) legt in einer direkten Abbildung zu den Komponenten die Struktur des records fest. Wolfgang E. Nagel Operationen 3. Operationen retrieve (R,V,id) Gibt der Variablen V den Wert der Komponente von record R, die mit id spezifiziert ist. Gleiche Typen von V und der entsprechenden Komponente von record R werden hier vorausgesetzt. update (R,V,id) speichert den Wert der Variablen V auf die Komponente von record R, die durch id bezeichnet ist. Bemerkung: Wenn R1 und R2 records vom gleichen Typ sind, kann in manchen Programmiersprachen durch R1 = R2 der Wert von R2 vollständig auf R1 zugewiesen werden. Wolfgang E. Nagel Padding Padding entsteht dadurch, dass gewisse atomaren Datentypen an bestimmten Speicheradressen abgelegt sein müssen (z.B. Wortgrenze, Doppelwortgrenze) ungenutzter Speicherplatz wird automatisch vom Compiler angelegt Wolfgang E. Nagel Beispiele a) Strukturbaum MITARBEITER PERSONALNUMMER VORNAME NAME NACHNAME ANSCHRIFT GEBURTSNAME STRASSE Nachname Geburtsn. HAUSNR PLZ ORT Padding b) Speicherdarstellung Pers.nr. Vorname KINDERZAHL Strasse Wolfgang E. Nagel Nr PLZ Ort Ki. Datenstruktur Menge " Bei einer Menge hat ein Element keinen Vorgänger, Nachfolger, Vater, Sohn, und es gibt auch kein erstes oder letztes Element. " Ein Element ist entweder in der Menge enthalten oder nicht enthalten. " Die Elemente sind atomar und haben einen ordinalen Datentyp. " Die Datenstruktur Set ist nur in wenigen Programmiersprachen direkt in der Sprache verfügbar (z.B. PASCAL). Wolfgang E. Nagel Datenstruktur Menge Definition: Ein Set ist eine Sammlung von Elementen, die alle den gleichen Datentyp haben. 1. Elemente Die Elemente haben einen Aufzählungstyp und sind atomar. 2. Struktur Die Elemente sind Mitglieder einer Menge. 3. Operationen assign (set s1, const set s2) post s1 hat die gleichen Elemente wie s2 bool in (const set s1, element e) post IF e Element von s1 THEN in ist true ELSE in ist false Wolfgang E. Nagel Operationen auf der Menge II set intersection (const set s1, const set s2) post intersection ist eine Menge, die den Durchschnitt von s1 und s2 enthält. set union (const set s1, const set s2) post union ist eine Menge, die die Vereinigung von s1 und s2 enthält. set difference (const set s1, const set s2) post difference ist eine Menge, die s1 ohne s2 enthält. bool equal (const set s1, const set s2) post IF THEN ELSE s1 und s2 haben die gleichen Elemente equal ist true equal ist false Wolfgang E. Nagel Operationen auf der Menge III bool subset (const set s1, const set s2) post IF THEN ELSE jedes Element von s1 auch in s2 subset ist true subset ist false mkset (set s, const element elem_vec[], int n) post s enthält elem_vec[0],...,elem_vec[n-1] als Elemente. set create (element ubound, id_type basetype_id) post create ist eine leere Menge. Diese kann Elemente aufnehmen, die nicht größer als ubound sind, und typkompatibel zu Mengen, die mit der gleichen basetype_id erzeugt wurden. void delete (set s) post s existiert nicht mehr. Wolfgang E. Nagel Beispielimplementierung für Menge typedef enum {false, true} bool; typedef struct set set; struct set { int type_id; int ub; bool *elem; /* Typenkennung */ /* größtes mögliches Element */ /* Element vorhanden */ /* (ja/nein) */ }; Wolfgang E. Nagel Datenstruktur Sequenz Definition: Eine Sequenz ist eine geordnete endliche Folge eines gegebenen Datentyps, wobei auf alle Elemente nur sequentiell zugegriffen werden kann. 1. Elemente Die Elemente sind vom Typ stdelement (standard-element). 2. Struktur Die Struktur zwischen den Elementen ist linear. Wolfgang E. Nagel Operationen auf der Sequenz I 3. Operationen find_first() pre Die Sequenz ist nicht leer. post Das erste Element ist das current_element. find_next () pre Die Sequenz ist nicht leer und current_element ist nicht das letzte Element. post c_next ist das current_element. Wolfgang E. Nagel Operationen auf der Sequenz II append (const stdelement e) post Die Sequenz enthält e als letztes Element und dieses Element ist das current_element. stdelement retrieve() pre Die Sequenz ist nicht leer. post retrieve ist eine Kopie des current_element. Wolfgang E. Nagel Operationen auf der Sequenz III bool empty() post IF Die Sequenz enthält kein Element THEN empty ist true ELSE empty ist false bool last() post IF Das letzte Element ist das current_element THEN last ist true ELSE last ist false create() post clear() post Die Sequenz existiert und ist leer. Die Sequenz ist leer. ♦ Wolfgang E. Nagel Veranschaulichung Sequenz e1,...,en e1 e2 e3 en current_element nach find_first e1 e2 e3 en nach find_next e1 e2 e3 en nach append(en+1) e1 e2 e3 en en+1 Wolfgang E. Nagel Sequentielle Dateien in C In C sind Sequenzen vom Typ FILE *. Implementierung der Operationen (FILE *stream): find_first: stream = fopen(Dateiname, "rb+"); bzw. rewind(stream); create: stream = fopen(Dateiname, "wb+"); bzw. stream = tmpfile(); append: fseek(stream, 0, SEEK_END); fwrite(&e, sizeof(e), 1, stream); fflush(stream); clear: freopen(Dateiname, "wb+", stream); Wolfgang E. Nagel Sequentielle Dateien in C e = retrieve(); find_next() (nur in Kombination möglich): fread(&e, sizeof(e), 1, stream); empty, last: Keine unmittelbare Entsprechung, aber mit feof(stream) kann man das Erreichen des Dateiendes abfragen. Diese Funktion liefert allerdings erst true, nachdem versucht wurde, auf den Nachfolger des letzten Elementes (durch retrieve) zuzugreifen Wolfgang E. Nagel Textdateien Dateien mit Komponenten vom Typ char – haben zusätzliche Zeilenstruktur (‚\n‘) – zusätzliche Konvertierungen erlaubt (z.B. Lesen von float-Zahlen) Wolfgang E. Nagel Lineare Datenstrukturen Datenstruktur mit beliebig erweiterbarer Sequenz von Daten – Einfügen am Ende der Sequenz – Durchlaufen der Sequenz – Abspeicherung der Sequenz muss nicht notwendigerweise aufeinanderfolgend sein Wolfgang E. Nagel Lineare Datenstrukturen head current_node ... ... First node last node c_prior c_pre Wolfgang E. Nagel c_next Beispiel in C struct liste { int Daten; struct liste *next; } *head, *current_node, *tmp; head = malloc(sizeof(*head)); current_node head->Daten = 4711; head->next = NULL; current_node = head; tmp = malloc(sizeof(*tmp)); current_node tmp->Daten = 31415; tmp->next = NULL; current_node->next = tmp; current_node = tmp; Wolfgang E. Nagel head! 4711 -head! 4711 31415 -- Einfach verkettete Liste Definition: Eine einfach verkettete Liste ist eine Folge aus keinem oder aus endlich vielen Elementen eines gegebenen Datentyps, bei der der Nachfolger eines Elementes durch einen Pointer erreicht werden kann. 1. Elemente Die Elemente sind Knoten. Jeder Knoten enthält eine Datenkomponente und einen Knoten-Pointer. 2. Struktur Die Struktur zwischen den Knoten ist linear. Wolfgang E. Nagel Vereinbarung bei Post-Conditions current_node: c_pre: c_prior: c_next: l_pre: zeigt auf den aktuellen Knoten current_node vor Operation Vorgänger von c_pre Nachfolger von c_pre gesamte Liste vor Operation current_node c_prior c_pre c_next Wolfgang E. Nagel Operationen auf einfach verketteter Liste I 3. Operationen insert_after (const stdelement e) pre Liste darf nicht voll sein. post Ein neuer Knoten mit dem Datenelement e ist in der Liste enthalten, dieser Knoten ist auch der neue current_node. IF L_pre war nicht leer THEN current_node ist Nachfolger von c_pre und c_next ist Nachfolger vom current_node. Wolfgang E. Nagel Vor insert_after head current_node Wolfgang E. Nagel Nach insert_after head c_pre c_next current_node e Wolfgang E. Nagel Operationen auf einfach verketteter Liste II insert_before (const stdelement e) pre Liste darf nicht voll sein. post Ein neuer Knoten mit dem Datenelement e ist in der Liste enthalten, dieser Knoten ist auch der neue current_node. IF THEN L_pre war nicht leer IF c_pre war erstes Element in der Liste THEN c_pre ist Nachfolger vom current_node ELSE current_node ist Nachfolger von c_prior und c_pre ist Nachfolger vom current_node. Wolfgang E. Nagel Operationen auf einfach verketteter Liste III delete() pre Die Liste ist nicht leer. post c_pre ist nicht mehr in der Liste. IF THEN ELSE c_pre gleich erster Knoten c_next ist erster Knoten, falls er existiert Nachfolger von c_prior ist c_next IF THEN ELSE Liste nicht leer current_node gleich erster Knoten current_node gleich leerer Knoten stdelement retrieve () pre Die Liste ist nicht leer. post retrieve ist eine Kopie des Datenelementes von c_pre. Wolfgang E. Nagel Operationen auf einfach verketteter Liste IV update (const stdelement e) pre Die Liste ist nicht leer. post c_pre enthält e als sein Datenelement. find_first pre Die Liste ist nicht leer. post current_node zeigt auf den ersten Knoten bool find_next () pre Die Liste ist nicht leer. post IF THEN ELSE c_pre zeigt nicht auf den letzten Knoten der Liste current_node gleich c_next; find_next ist true find_next ist false Wolfgang E. Nagel Operationen auf einfach verketteter Liste V bool find_prior () pre Die Liste ist nicht leer. post IF THEN ELSE c_pre zeigt nicht auf den ersten Knoten der Liste current_node gleich c_prior; find_prior ist true find_prior ist false bool locate (const stdelement e) pre Die Liste ist nicht leer. post IF THEN ELSE e ist in der Liste locate ist true und current_node zeigt auf den Knoten, der e als Datenelement enthält locate ist false und current_node zeigt auf den letzten Knoten der Liste Wolfgang E. Nagel Operationen auf einfach verketteter Liste VI bool empty() post IF THEN ELSE Liste nicht leer empty ist false empty ist true bool full() post IF THEN ELSE Liste enthält maximal zulässige Anzahl von Elementen full ist true full ist false create() post Eine leere verkettete Liste existiert. Wolfgang E. Nagel Realisierung über Pointer typedef struct node * listpointer; struct node { stdelement element; listpointer next; }; /* oder struct node *next; */ /* globale Variablen: */ static listpointer head, current_node; /* lokale Variablen: */ listpointer p; bool found, not_failed; Wolfgang E. Nagel create/retrieve/update Komplexität: void create ( void ) O(1) head = NULL current _ node = NULL stdelement retrieve ( void ) O(1) return (current _ node ->element ) void update ( stdelement e ) O(1) current _ node ->element = e Wolfgang E. Nagel insert_after Komplexität: void insert_ after ( stdelement e ) p = (listpointer ) malloc (sizeof(struct node )) assert (p ! = NULL ) O(1) p ->element = e head != NULL T F p ->next = current_node ->next p ->next = NULL current_ node ->next = p head = p current_ node = p Wolfgang E. Nagel insert_before Komplexität: void insert_ before( stdelement e ) current_ node == head TRUE p = (listpointer) malloc (sizeof(struct node )) assert(p != NULL) FALSE p = head p->next != current_ node p->element = e p = p->next p->next = head head = p current_ node = p current_ node = p insert_ after(e) Wolfgang E. Nagel O(n) find_first/find_next Komplexität: void find_first ( void ) O(1) current_node = head bool find _ next ( void ) found = (current_ node ->next != NULL) O(1) found T F current_ node = current_ node ->next return found Wolfgang E. Nagel find_prior Komplexität: bool find _ prior( void ) found = (current_ node != head ) found T F p = head p->next != current_ node p = p->next current_ node = p return found Wolfgang E. Nagel O(n) locate/empty Komplexität: bool locate ( stdelement e ) not _ failed = true O(n) find _ first() not _ failed && ( retrieve () != e ) not _ failed = find _ next ( ) return not _ failed bool empty ( void ) O(1) return (head == NULL) Wolfgang E. Nagel delete Komplexität: void delete ( void ) current _ node != head TRUE FALSE p = head O(n) p ->next != current _ node p = p ->next head = head ->next p ->next = current _ node ->next free (current _ node ) current _ node = head Wolfgang E. Nagel Realisierung mit 3 Feldern typedef int listpointer; typedef listpointer listpointer_array[MAXLIST]; /* globale Variablen: */ static stdelement static listpointer_array static listpointer static listpointer list[MAXLIST]; next, free_list; head, current_node; first_free; /* lokale Variablen: */ listpointer i, p; bool found, not_failed; Wolfgang E. Nagel first_free 4 0 5 current_node head 3 2 a4 a1 a2 a3 1 -1 list next n-1 free_list -1 -1 3 n-2 Wolfgang E. Nagel create Komplexität: void create( void ) head = -1 current_ node = -1 O(n) for (i = 0 ; i < MAXLIST ; i ++) free_ list[i ] = i next[i ] = -1 first_ free = 0 Wolfgang E. Nagel insert_after Komplexität: void insert_ after( stdelement e ) p = free_ list[first_ free++] list[p] = e O(1) current_ node >= 0 T F next[p] = next[current_ node ] next[p] = -1 next[current_ node ] = p head = p current_ node = p Wolfgang E. Nagel insert_before Komplexität: void insert_ before ( stdelement e ) current_ node != head TRUE FALSE p = head p = free_ list[first_ free++] next[p] != current_ node list[p] = e p = next[p] next[p] = head current_ node = p head = p insert_ after (e) current_ node = p Wolfgang E. Nagel O(n) delete Komplexität: void delete ( void ) current_ node != head T F p = head O(n) next [p ] != current_ node head = next [head ] p = next[p ] next [p ] = next[current_ node ] free_ list[--first_ free] = current_ node current_ node = head Wolfgang E. Nagel Realisierung durch ein Feld Idee: keine Verkettung, sondern Element a[i] steht auf Position i im Feld. Konsequenz: Einfügen bedeutet verschieben von Elementen ab Einfügeposition. typedef int list_index; /* globale Variablen: */ static stdelement list[MAXLIST]; static list_index last_node; /* Anzahl Knoten in Liste - 1 */ static list_index current_node; /* lokale Variablen: */ list_index k; bool found, not_failed; Wolfgang E. Nagel create Komplexität: void create ( void ) O(1) last_ node = -1 current_ node = -1 Wolfgang E. Nagel insert_after Komplexität: void insert_after ( stdelement e ) last_node >= 0 T F for (k = last_node ; k > current_node ; k--) list[k+1] = list[k] list[++current_node] = e last_node++ Wolfgang E. Nagel O(n) insert_before Komplexität: void insert_ before ( stdelement e ) current_ node >= 0 T F current_ node -insert_ after (e ) Wolfgang E. Nagel O(n) delete Komplexität: void delete ( void ) for (k = current_ node +1 ; k <= last_ node ; k++) O(n) list[k-1 ] = list[k] last_ node -current_ node = (last_ node >= 0 ) ? 0 : -1 Wolfgang E. Nagel N-fach verkettete Liste Definition: Eine n-fach verkettete Liste ist eine Folge aus keinem oder aus endlich vielen Elementen eines gegebenen Datentyps, bei denen die Nachfolger eines Elementes durch Pointer erreicht werden. 1. Elemente Die Elemente heißen Knoten. Jeder Knoten enthält eine Datenkomponente und n Knotenpointer. 2. Struktur Die Struktur dieser Knoten ist linear. 3. Operationen wie bei einfach verketteter Liste. Wolfgang E. Nagel Begründung Ziel: Listenelement soll nur einmal gespeichert werden Unter Umständen können jedoch mehrere logische Sortierungen sinnvoll sein: – Name – Geburtsdatum – Kinderanzahl – Postleitzahl Spezialfall: Doppelt verkettete Liste, in der jedes Listenelement einen Zeiger auf Vorgänger und Nachfolger hat. Dadurch können einige Operationen schneller ausgeführt werden als bei einfach verketteten Listen. Wolfgang E. Nagel Verpointerung head current_node ... ... first node last node Wolfgang E. Nagel Doppelt verkettete Liste typedef struct node *listpointer; struct node { stdelement element; listpointer next; listpointer prior; }; /* globale Variablen: */ static listpointer head, current_node; Wolfgang E. Nagel find_prior Komplexität: bool find _ prior ( void ) found = ( current _ node ->prior != NULL ) O(1) found T F current _ node = current _ node - >prior return found Wolfgang E. Nagel insert_after Komplexität: void insert_ after ( stdelement e ) p = (listpointer ) malloc (sizeof (struct node )) assert(p != NULL ) p ->element = e O(1) p ->prior = current _ node head != NULL T F p ->next = current _ node ->next p ->next = NULL c_ pre = current _ node find _ next () T F current _ node ->prior = p head = p c_ pre ->next = p current _ node = p Wolfgang E. Nagel insert_before Komplexität: void insert_ before ( stdelement e ) current_ node != head T F p = (listpointer ) malloc (sizeof (struct node )) assert(p != NULL) find _ prior () O(1) p ->element = e p ->prior = NULL p ->next = head head != NULL T F head ->prior = p insert_ after (e ) head = p current_ node = p Wolfgang E. Nagel delete Komplexität: void delete ( void ) current_ node != head TRUE FALSE c_ pre = current_ node head = head ->next O(1) find _ prior () free(current_ node ) current_ node ->next = c_ pre->next !empty() free(c_ pre) T c_ pre = current_ node find _ first() find _ next() T current_ node ->prior = c_ pre F current_ node ->prior = NULL current_ node = head Wolfgang E. Nagel F Zeitkomplexität der einzelnen Basisoperationen in verschiedenen Listenrealisierungen Einfach verkettete Liste Operationen Dynamische Pointer Realisierung mit 3 Feldern Realisierung mit einem Feld Doppelt verkettete Liste insert_after O(1) O(1) O(n) O(1) insert_before O(n) O(n) O(n) O(1) delete O(n) O(n) O(n) O(1) retrieve O(1) O(1) O(1) O(1) update O(1) O(1) O(1) O(1) find_first O(1) O(1) O(1) O(1) find_next O(1) O(1) O(1) O(1) find_prior O(n) O(n) O(1) O(1) Wolfgang E. Nagel Zeitkomplexität der einzelnen Basisoperationen in verschiedenen Listenrealisierungen Einfach verkettete Liste Operationen Dynamische Pointer Realisierung mit 3 Feldern Realisierung mit einem Feld Doppelt verkettete Liste locate O(n) O(n) O(n) O(n) create O(1) O(n) O(1) O(1) clear O(n) O(n) O(1) O(n) empty O(1) O(1) O(1) O(1) full O(1) O(1) O(1) O(1) Wolfgang E. Nagel Zusammenfassung " Verkettete Listen erlauben eine flexible Handhabung beim Einfügen und Löschen von Elementen an vorgegebenen Positionen (kein Shiften von Elementen). " Dem steht ein erhöhter Speicherplatz für die Pointer gegenüber. " Verkettete Listen sollten nur dort verwendet werden, wo häufiges Einfügen oder Löschen wichtig ist. " Elemente werden i.A. nicht zusammenhängend abgespeichert. " Für Anwendungen, bei denen es auf einen schnellen Zugriff (oder schnelles Suchen) ankommt, sind ARRAY-Realisierungen für Listen wesentlich vorteilhafter. Wolfgang E. Nagel Stapel " andere Namen: Stack, Kellerspeicher, LIFO-Liste " Anwendungen: z.B. Implementierung von Programmiersprachen, Auswertung von arithmetischen Ausdrücken, Übersetzung von Programmiersprachen, u.w. " Anwendung außerhalb des EDV-Bereich: Tablettspender im Kasino Wolfgang E. Nagel Stapel Definition: Bei einem Stack handelt es sich um eine lineare Datenstruktur mit einem LIFO Verhalten. 1. Elemente Die Elemente sind von einem beliebigen Datentyp (stdelement). 2. Struktur Die Struktur setzt die Elemente so in Beziehung, dass die Ankunftsreihenfolge zu jeder Zeit erhalten bleibt (z.B. Zeitmarke). 3. Operationen create muss vor jeder anderen Operation ausgeführt werden, d.h. zusätzliche Pre-Condition für die anderen Operationen: der Stack existiert. Wolfgang E. Nagel Operationen auf Stapeln I push (const stdelement e) pre Der Stack ist nicht voll. post Der Stack enthält e als das zuletzt angekommene Element. stdelement pop () pre Der Stack ist nicht leer. post pop ist das zuletzt angekommene Element. Der Stack enthält dieses Element nicht mehr. Wolfgang E. Nagel Operationen auf Stapeln II bool empty() post IF THEN ELSE Der Stack enthält kein Element empty ist true empty ist false bool full () post IF THEN ELSE Der Stack hat seine maximale Länge erreicht full ist true full ist false create() post Der Stack existiert und ist leer. Wolfgang E. Nagel Realisierung mit Hilfe einer verketteten Struktur typedef struct stack_element *stack_pointer; struct stack_element { stdelement element; stack_pointer next; }; /* globale Variablen: */ static stack_pointer top; Wolfgang E. Nagel Element TOP Element Element . . . Element Element Element Wolfgang E. Nagel create/empty Komplexität: void create ( void ) O(1) top = NULL bool empty ( void ) O(1) return (top == NULL ) Wolfgang E. Nagel push Komplexität: void push( stdelement e ) p = (stack_ pointer ) malloc (sizeof (struct stack_ element )) assert (p != NULL) O(1) p ->element = e p ->next = top top = p Wolfgang E. Nagel P neues Element Element Element TOP Element Element Wolfgang E. Nagel pop Komplexität: stdelement pop ( void ) e = top->element p = top O(1) top = top-> next free(p ) return e Wolfgang E. Nagel Realisierung mit Hilfe eines Feldes typedef int stackbereich; typedef stdelement stack_type[MAX_STACK]; /* globale Variablen: */ static stackbereich top; static stack_type stack; belegt 0 frei top Wolfgang E. Nagel MAX_STACK-1 create/empty/full Komplexität: void create ( void ) O(1) top = -1 bool empty( void ) O(1) return (top == -1 ) bool full ( void ) O(1) return (top == (MAX _ STACK -1 )) Wolfgang E. Nagel push/pop Komplexität: void push( stdelement e ) O(1) stack [++top ] = e stdelement pop ( void ) O(1) return (stack[ top - -] ) Wolfgang E. Nagel Zeitkomplexitäten für die Stack-Implementierungen Operation verkettete Liste Feldimplementation push O(1) O(1) pop O(1) O(1) empty O(1) O(1) full O(1) O(1) create O(1) O(1) clear O(n) O(1) Wolfgang E. Nagel Warteschlangen (Queues) " Wichtige Datenstruktur z.B. in Betriebssystemkernen " FIFO-Prinzip (First-In-First-Out) " Einfügen am Ende (tail), Entfernen am Anfang der Schlange (head) " Einfügeoperation: put oder enqueue " Entferneoperation: get oder dequeue Wolfgang E. Nagel Element head (get hier) Element Element ... Element Element tail (put hier) Wolfgang E. Nagel Queues Definition: Eine Queue ist ein Datentyp mit einem FIFO-Verhalten (First-In-First-Out). 1. Elemente Die Elemente sind von einem beliebigen Datentyp (stdelement). 2. Struktur Die Struktur setzt die Elemente so in Beziehung, dass ihre Ankunftsreihenfolge erhalten bleibt. 3. Operationen create muss vor jeder anderen Operation ausgeführt werden, d.h. zusätzliche Pre-Condition für die anderen Operationen: die Queue existiert. Q_pre bezeichnet die Queue vor Ausführung der Operation. Wolfgang E. Nagel Operationen auf Queues I put (e : stdelement) pre Die Queue ist nicht voll. post Die Queue enthält e als das zuletzt angekommene Element. get (var e : stdelement) pre Die Queue ist nicht leer. post e ist das Element, das am längsten in Q_pre ist. Das Element e ist nicht mehr in der Queue. Wolfgang E. Nagel Operationen auf Queues II bool empty() post IF THEN ELSE bool full () post IF THEN ELSE clear() post Anzahl der Elemente in der Queue ist Null. empty ist true empty ist false Queue hat maximal zulässige Anzahl von Elementen erreicht full ist true full ist false Die Queue ist leer. create() post Die Queue existiert und ist leer. Wolfgang E. Nagel Realisierung mit Hilfe eines Feldes #define MAX_QUEUE ...! ! /* globale Variablen: */! static stdelement queue[MAX_QUEUE];! static int head, tail;! Wolfgang E. Nagel create/clear Komplexität: void create ( void ) O(1) tail = -1 head = MAX_QUEUE -1 void clear ( void ) O(1) create() Wolfgang E. Nagel empty/full Komplexität: bool empty ( void ) O(1) return (tail == -1) bool full ( void ) O(1) return (tail == head) Wolfgang E. Nagel put/get Komplexität: void put ( stdelement e ) tail = (tail+1)%MAX_QUEUE O(1) queue[tail] = e stdelement get ( void ) int h h = (head+1)%MAX_QUEUE h == tail TRUE clear() FALSE head = h return (queue[h]) Wolfgang E. Nagel O(1) length Komplexität: int get ( void ) tail > head TRUE len = tail - head FALSE len = MAX_QUEUE + tail - head return (len) Wolfgang E. Nagel O(1) Fakultät Informatik, Institut für Technische Informatik, Professur Rechnerarchitektur Effiziente Parallele Algorithmen Datenstrukturen Teil II Zellescher Weg 12 Nöthnitzer Straße 46 Willers-Bau A 205 Raum 1044 Tel. +49 351 - 463 - 35450 Tel. +49 351 - 463 - 38246 Wolfgang E. Nagel ([email protected]) Begriffe aus der Graphentheorie ! Ein Graph G‘ = (V‘, E‘) heißt Teilgraph von G = (V, E), wenn V‘ ⊆ V und E‘ ⊆ E gilt. ! Ein Weg heißt einfacher Weg, wenn jeder Knoten auf dem Weg genau einmal vorkommt. ! Ein Zykel (Kreis) ist ein einfacher Weg mit der Ausnahme α(W) = ω(W). ! Zwei Knoten heißen adjazent, wenn es eine Kante gibt, die sie verbindet. ! Ein Graph heißt gewichtet (bewertet), wenn jeder Kante ein Wert als Gewicht zugeordnet ist. (z.B. Transportkosten, Entfernung, etc.) Wolfgang E. Nagel Speicherung von Graphen 1. Adjazenzmatrix Sei G = (V, E) ein ungerichteter oder gerichteter Graph mit V = {v1, ... , vn} . Die Adjazenzmatrix eines Graphen ist eine n x n Matrix A = (aij), i,j = 1...n mit aij = 1 falls (vi,vj) ∈ E 0 sonst 2. Adjazenzlisten (Nachbarschaftslisten) Es wird eine Liste aller Knoten verwendet, deren Elemente auf den Anfang einer verketteten Liste zeigen, in der alle Knoten stehen, die mit dem entsprechenden Knoten adjazent sind. Wolfgang E. Nagel Speicherkomplexität ! Adjazenzmatrix: |V|2 ! Adjazenzliste: |V| + |E| ! Der Platzbedarf für Adjazenzmatrizen ist i.A. wesentlich höher als der für Adjazenzlisten, jedoch ist die Verwaltung der Adjazenzmatrix effizienter möglich (Zugriff auf a[i][j]) als die der Adjazenzlisten. Wolfgang E. Nagel Datenstruktur Baum Definition: Ein Baum ist ein gerichteter Graph G = (V, E) mit folgenden Eigenschaften: Es existiert genau ein Knoten v ∈ V (Wurzel des Baumes, root) derart, dass für alle Knoten vi genau ein Weg von v nach vi ∈ V existiert. ♦ Wolfgang E. Nagel Unterscheidung Baum/Graph Baum (und Graph) Graphen, aber keine Bäume Wolfgang E. Nagel Bemerkungen ! Hierarchische (rekursive) Datenstruktur ! Partielle Ordnung auf den Knoten eines Baumes, da alle Wege von der Wurzel ausgehen. – A heißt Vorgänger von B, wenn A auf einem Weg von der Wurzel zu B liegt. B heißt entsprechend Nachfolger von A. – A heißt Vater von B, wenn (A, B) ∈ E, B heißt Sohn von A. – Haben B und C denselben Vater, so heißen sie Brüder. – Knoten, die keinen Nachfolger haben, heißen Blätter. – Knoten, die einen oder mehrere Nachfolger haben, heißen innere Knoten. ! Ein Knoten S mit allen Nachfolgern wird Teilbaum eines Baumes T genannt, falls S ein Knoten ungleich der Wurzel von T ist. Wolfgang E. Nagel Knotenbezeichnungen Wurzel (root) Blatt (leaf) innerer Knoten Blatt Blatt Wolfgang E. Nagel Definitionen Definition: Jeder Knoten in einem Baum liegt auf einem bestimmten Level. 1. Das Level der Wurzel ist 0. 2. Das Level eines Knotens ist gleich dem um 1 inkrementierten Level seines Vaters. ♦ Definition: Die Höhe eines Baumes ist gleich dem maximalen Level, welches mit einem beliebigen Knoten des Baumes assoziiert ist. ♦ Wolfgang E. Nagel Datenstruktur Binärbaum Strukturgedanke: Bäume, bei denen jeder Knoten höchstens zwei Söhne hat, heißen Binärbäume. Hat jeder Knoten entweder keinen oder zwei Söhne und die Blätter liegen auf dem gleichen Level, so heißt der Baum voller Binärbaum. Ist die Reihenfolge der Söhne durch die Indizes eindeutig festgelegt (T1 = linker Sohn, linker Teilbaum; T2 = rechter Sohn, rechter Teilbaum), so handelt es sich um einen geordneten Binärbaum. Wolfgang E. Nagel Besuchen aller Knoten ! Es gibt n! Möglichkeiten, eine Struktur mit n Elementen zu durchlaufen. ! Für einen Binärbaum gibt es 3!=6 Möglichkeiten, einen solchen Baum geordnet zu durchlaufen: – W L R = Preorder – W R L – R W L – R L W – L W R = Inorder W – L R W = Postorder L Wolfgang E. Nagel R 1 Preorder 2 4 3 5 6 3 Inorder 1 5 2 4 6 6 Postorder 2 5 1 3 Wolfgang E. Nagel 4 Arithmetische Ausdrücke in Prefix-Notation +! ! (a + b) * c + 7 Prefix-Notation: 7! *! +*+abc7 +! Postfix-Notation: a b + c * 7 + a! c! b! ! (a + b) * (c + 7) Prefix-Notation: *! *+ab+c7 +! Postfix-Notation: a b + c 7 + * a! +! b! c! Bemerkung: Bei der Infix-Notation sind i.A. Klammern notwendig, um die Baumstruktur eindeutig wiedergeben zu können. Wolfgang E. Nagel 7! Größe, Höhe und durchschnittliche Weglänge Baum 1 Baum 2 Baum 3 Größe 1 2 7 Höhe 0 1 2 0 +1 1 = 2 2 0 + 1 + 1 + 2 + 2 + 2 + 2 10 = 7 7 durchschn. Weglänge € € 0 € € € Wolfgang E. Nagel € € Datenstruktur Binärbaum Definition: Binärbaum 1. Elemente Die Elemente eines Binärbaumes heißen Knoten, und jeder Knoten enthält eine Datenkomponente e. 2. Struktur Ein Binärbaum ist entweder leer oder besteht aus einem Knoten (Wurzel) zusammen mit zwei Binärbäumen. Diese zwei Binärbäume sind disjunkt voneinander sowie von der Wurzel und werden linker bzw. rechter Teilbaum der Wurzel genannt. Ein Binärbaum hat eine hierarchische Struktur. Immer dann, wenn der Baum nicht leer ist, gibt es eine eindeutige Wurzel. Jeder Knoten (außer der Wurzel) hat genau einen Vater und keinen, einen oder zwei Söhne. Ein Sohn ist entweder ein rechter oder ein linker Sohn eines Knotens. Wolfgang E. Nagel Operationen auf dem Binärbaum I 3. Operationen create muss vor jeder anderen Operation ausgeführt werden, d.h. zusätzliche Pre-Condition für die anderen Operationen: der Binärbaum existiert. Vereinbarungen: • current_node bezeichnet den aktuell bearbeiteten Knoten im Baum. • c_pre ist der current_node vor der Ausführung der Operation. • t_pre ist der Baum vor Ausführung der Operation. • Folgende Datentypen werden vereinbart: - typedef enum {preorder, inorder, postorder} order; - typedef enum {rootv, leftchild, richtchild, parent} relative; - struct status { int size; int height; double average_path_length; }; • rootv wird verwendet, um den Knoten Wurzel von dem Pointer root auf die Wurzel zu unterscheiden. Wolfgang E. Nagel Operationen auf dem Binärbaum II traverse (order traverse_order, void process (stdelement *e_ptr, int level, void *data_ptr), void *data_ptr) pre Der Baum ist nicht leer. post Jeder Knoten des Baumes wurde genau einmal bearbeitet. Die Reihenfolge, in der die Knoten bearbeitet werden, ist durch den Parameter traverse_order festgelegt. switch (traverse_order) preorder: Jeder Knoten wird vor der Verarbeitung seiner Teilbäume bearbeitet. inorder: Jeder Knoten wird nach der Verarbeitung seines linken Teilbaumes und vor der Verarbeitung seines rechten Teilbaum bearbeitet. postorder: Jeder Knoten wird nach der Verarbeitung seiner Teilbäume bearbeitet. Jeder Knoten wird über die Funktion process bearbeitet, die einen Zeiger auf das Datenelement übergeben bekommt. data_ptr kann auf einen Datenbereich zeigen, der z.B. in process verwendet wird. Wolfgang E. Nagel Operationen auf dem Binärbaum III bool insert (const stdelement e, relative rel) pre Entweder ist rel gleich rootv und der Baum ist leer, oder rel ist ungleich rootv und der Baum ist nicht leer. post Ein neuer Knoten mit dem Datenelement e kann in Abhängigkeit vom Wert rel im Baum enthalten sein. Falls der Knoten im Baum enthalten ist, ist er der current_node. ... Wolfgang E. Nagel Operationen auf dem Binärbaum IV bool insert (...) cont. switch (rel) rootv: leftchild: e ist Element der Wurzel des Baumes und insert ist true. IF c_pre hat keinen linken Sohn THEN c_pre hat einen linken Sohn, dieser Knoten enthält e als Datenelement, und insert ist true ELSE insert ist false rightchild: IF c_pre hat keinen rechten Sohn THEN c_pre hat einen rechten Sohn, dieser Knoten enthält e als Datenelement, und insert ist true ELSE insert ist false parent: insert ist false Wolfgang E. Nagel Operationen auf dem Binärbaum V void delete_sub() pre Der Baum ist nicht leer. post Der Teilbaum von t_pre, dessen Wurzel c_pre ist, ist aus dem Baum entfernt. Der neue current_node ist die Wurzel des Baumes. stdelement retrieve() pre Der Baum ist nicht leer post retrieve ist eine Kopie des Datenelements von c_pre. void update (const stdelement e) pre Der Baum ist nicht leer. post c_pre enthält e als sein Datenelement. Wolfgang E. Nagel Operationen auf dem Binärbaum VI bool find (relative rel ) pre Der Baum ist nicht leer. post Der current_node ist in Abhängigkeit von rel wie folgt definiert switch (rel) rootv: der current_node ist die Wurzel und find ist true. leftchild: IF c_pre hat linken Sohn THEN current_node ist der linke Sohn, find ist true ELSE find ist false rightchild: IF c_pre hat rechten Sohn THEN current_node ist der rechte Sohn, find ist true ELSE find ist false parent: IF c_pre hat Vater THEN current_node ist der Vater, find ist true ELSE find ist false Wolfgang E. Nagel Operationen auf dem Binärbaum VII struct status characteristics() post characteristics enthält die Anzahl der Knoten, die Höhe des Baumes und die durchschnittliche Pfadlänge von der Wurzel zu einem Blatt bool empty() post IF THEN ELSE Baum nicht leer empty ist false empty ist true create() post Ein leerer Binärbaum existiert. clear() post Der Binärbaum ist leer. Wolfgang E. Nagel Darstellung von Binärbäumen im Rechner 1. Inhalt des Knotens 2. Pointer auf den linken Sohn 3. Pointer auf den rechten Sohn 4. Ein Pointer root zeigt auf die Wurzel des Baumes e! Wolfgang E. Nagel Binärbaumdarstellung mit Pointern Wurzel root V Y X Z Wolfgang E. Nagel Binärbaumdarstellung mit Feldern Element Y 1 left right -1 -1 2 root Wurzel 3 6 7 Z 4 -1 -1 5 V 6 1 4 X 7 -1 -1 . . . . . . . . . Wolfgang E. Nagel Realisierung eines Binärbaumes typedef enum {preorder, inorder, postorder} order; typedef enum {rootv, leftchild, rightchild, parent} relative; typedef struct node * node_pointer; struct node { stdelement element; node_pointer left, right; }; struct status { int size, height double average_path_length; }; struct characteristics { struct status st; int total_path_length; }; static node_pointer root, current_node; Wolfgang E. Nagel newnode Komplexität: static node _ pointer newnode ( void ) p = (node _ pointer ) malloc (sizeof (struct node )) assert ( p != NULL ) O(1) p ->left = NULL p ->right = NULL return p Wolfgang E. Nagel insert - Teil 1 bool insert ( stdelement e, relative rel ) success = true p = newnode ( ) p->element= e switch(rel) rootv root = p break leftchild current_node -> left == NULL TRUE current_node ->left = p FALSE success = false break Wolfgang E. Nagel insert - Teil 2 Komplexität: rightchild current _ node->right == NULL TRUE current_node ->right = p FALSE success = false break parent success = false O(1) break success T current_ node = p F free( p ) return success Wolfgang E. Nagel find_parent static node _ pointer find _ parent ( void ) Komplexität: Stack erzeugen push(NULL) found = false p = root (p != NULL) && !found (p ->left == current_ node ) | | (p ->right == current_ node ) TRUE FALSE p ->right != NULL O(n) T found = F push(p ->right ) p ->left != NULL true TRUE p = p ->left FALSE p = pop () Stack löschen return p Wolfgang E. Nagel find_parent (rekursiv) static node_ pointer find _ parent _ rec ( node_ pointer p) Komplexität: node_ pointer found p = = NULL T F p - > left == current_ node | | p - > right = = current_ node T found = F found = find _ parent _ rec (p - > left ) found NULL = p found == NULL T found = find _ parent _ rec (p - > right) O(n) return found node_ pointer find _ parent ( void ) node_ pointer found T found = NULL F root = = current_ node F found = find _ parent _ rec ( root ) return found Wolfgang E. Nagel find - Teil 1 bool find( relative rel ) success = true switch(rel) rootv current _ node = root break leftchild current _ node->left != NULL T current_ node = current_ node->left F success = false break Wolfgang E. Nagel find - Teil 2 Komplexität: rightchild current _ node->right != NULL T F current_ node = current_ node->right success = false break parent (p = find_ parent()) != NULL TRUE current_ node = p FALSE success = false break return success Wolfgang E. Nagel O(n) treedispose Komplexität: static void treedispose( node _ pointer p ) p != NULL T F O(n) treedispose(p->left ) treedispose(p->right) free(p) Wolfgang E. Nagel delete_sub Komplexität: void delete _ sub( void ) (p = find _ parent ()) != NULL T F p ->left == current _ node TRUE p ->left = NULL FALSE O(n) p ->right = NULL clear () treedispose (current _ node ) current _ node = root Wolfgang E. Nagel retrieve/update Komplexität: stdelement retrieve ( void ) O(1) return (current _ node ->element ) void update ( stdelement e ) O(1) current _ node - >element = e Wolfgang E. Nagel traverse Komplexität: void traverse (order ord, void (*process)( stdelement *e_ ptr, int level , void *data _ ptr ), void *data _ ptr ) ord preorder preord(root, 0, inorder postorder process, inord (root, 0, process, data _ ptr) data _ ptr) postord(root, 0, process, data _ ptr) break break break Wolfgang E. Nagel default O(n) preord Komplexität: static void preord ( node _ pointer p , int level , void ( *process)( stdelement *e _ ptr, int level , void *data _ ptr ), void *data _ ptr ) p != NULL T F O(n) process( &(p ->element ), level , data _ ptr) preord ( p ->left , level +1 , process, data _ ptr) preord ( p ->right , level +1 , process, data _ ptr) Wolfgang E. Nagel inord Komplexität: static void inord ( node _ pointer p, int level , void (*process)( stdelement *e_ ptr, int level , void *data _ ptr ), void *data _ ptr ) p != NULL T F O(n) inord (p->left , level +1, process, data _ ptr) process (&(p->element ), level , data _ ptr) inord (p->right, level +1, process, data _ ptr) Wolfgang E. Nagel postord Komplexität: static void postord ( node _ pointer p , int level , void (*process)( stdelement *e _ ptr, int level , void *data _ ptr ), void *data _ ptr ) p != NULL T F postord (p ->left , level +1 , process, data _ ptr) postord (p ->right , level +1 , process, data _ ptr) process(&(p ->element ), level , data _ ptr) Wolfgang E. Nagel O(n) characteristics Komplexität: struct status characteristics ( void ) d.st.size = 0 d.st.height = 0 d.total _ path _ length = 0 !empty() T F traverse(inorder , process_ characteristics, &d) d.st.average _ d.st.average _ path _ length = (double ) d .total _ path _ length / (double ) d .st.size return (d .st) Wolfgang E. Nagel path _ length = 0.0 O(n) create/clear/empty Komplexität: void create ( void ) O(1) root = NULL current_ node = NULL void clear ( void ) O(n) treedispose (root) create () bool empty( void ) O(1) return (root == NULL) Wolfgang E. Nagel Bäume und Rekursion Ein voller Binärbaum der Höhe H hat K = 2H+1 - 1 Knoten, von denen B = 2H Blätter und I = 2H - 1 innere Knoten sind. Aufrufgraphen (speziell bei Rekursion) Bei n Eingabeelementen H = k * n (verschiedene Wege stellen verschiedene Alternativen dar) oder B = n bzw. K = n Wolfgang E. Nagel Strategien zur Wegsuche Volle Traversierung → Rekursion (Größe bestimmt Komplexität) oder Wegsuche einfach; es wird immer maximal ein Sohn besucht (Liste) → Iteration (Höhe bestimmt Komplexität) komplex; es müssen im worst case alle Wege untersucht werden → Rekursion (Größe bestimmt Komplexität) Wolfgang E. Nagel Komplexität H=k*n K = n oder B = n Volle Traversierung O(2n) O(n) Einfache Wegsuche O(n) O(log n) Wolfgang E. Nagel Rekursion bei Binärbäumen typedef … returntype; typedef … statustype; bool goon_left( statustype s , node_pointer p ); bool goon_right( statustype s , node_pointer p ); statustype begin( node_pointer p ); statustype middle( statustype s, node_pointer p ); returntype end( statustype s , node_pointer p ); statustype from_left( statustype s, returntype r ); statustype from_right( statustype s, returntype r ); // Alle Funktionen haben die Komplexität O(1) returntype rec( node_pointer p ) { status = begin( p ); if ( goon_left( status, p ) ) { result = rec( p->left ); status = from_left( status, result ); } status = middle( status, p ); if ( goon_right( status, p ) ) { result = rec( p->right ); status = from_right( status, result ); } result = end( status, p ); return result; } Wolfgang E. Nagel Datenstruktur binärer Suchbaum Strukturgedanke: Ein binärer Suchbaum ist ein Binärbaum mit der Eigenschaft, dass für jeden Knoten N die folgenden Aussagen wahr sind: 1. Falls sich der Knoten L im linken Teilbaum vom Knoten N befindet, so ist der Wert vom Knoten L kleiner als der Wert vom Knoten N. 2. Falls sich der Knoten R im rechten Teilbaum vom Knoten N befindet, so ist der Wert vom Knoten R größer als der Wert vom Knoten N. Vorteil: Man findet schneller ein Element, weil man weiß, wo man suchen muss. Wolfgang E. Nagel Datenstruktur binärer Suchbaum Definition: binärer Suchbaum 1. Elemente Die Elemente eines binären Suchbaumes heißen Knoten; sie sind durch einen Schlüssel eindeutig identifiziert. 2. Struktur Ein binärer Suchbaum ist ein Binärbaum mit den zusätzlichen Eigenschaften: – Für alle Knoten im linken Teilbaum eines Knotens N gilt, dass sie bezüglich des Schlüssels kleiner sind als der Knoten N. – Für alle Knoten im rechten Teilbaum eines Knotens N gilt, dass sie bezüglich des Schlüssels größer sind als der Knoten N. – Diese Eigenschaften gelten rekursiv, d.h. jeder Teilbaum ist wiederum ein binärer Suchbaum. Wolfgang E. Nagel Operationen auf dem binären Suchbaum I 3. Operationen Spezifikation von vier wichtigen Operationen, die restlichen Operationen wie in der Definition des Binärbaumes. bool insert (const stdelement e) post IF THEN ELSE t_pre enthält e nicht e ist Element des Baumes, und insert ist true insert ist false bool delete_key (const stdelement e) post IF THEN ELSE t_pre enthält das Element e dieses Element ist nicht mehr im Baum enthalten, und delete_key ist true delete_key ist false Wolfgang E. Nagel Operationen auf dem binären Suchbaum II update (const stdelement e) pre Der Baum ist nicht leer. post current_node enthält das Element e, und der Baum ist weiterhin ein binärer Suchbaum. bool findkey (const stdelement e) pre Der Baum ist nicht leer. post IF der Baum enthält einen Knoten mit dem Inhalt e THEN dieser Knoten ist der current_node und findkey ist true ELSE der current_node ist der Knoten, an dem ein Knoten mit dem Inhalt e als Sohn angehängt wäre, wenn e im Baum enthalten wäre und findkey ist false Wolfgang E. Nagel Realisierung eines binären Suchbaumes Wie beim binären Baum, d.h.: typedef enum {preorder, inorder, postorder} order; typedef enum {rootv, leftchild, rightchild, parent} relative; typedef struct node *node_pointer; struct node { stdelement element; node_pointer left, right; }; /* globale Variablen: */ static node_pointer root, current_node; Wolfgang E. Nagel findkey bool findkey( stdelement e ) success= false p = root current_ node = NULL !success&& (p != NULL) current_ node = p p->element .key == e.key T F e.key < p->element .key success= true TRUE p = p->left FALSE p = p->right return success Wolfgang E. Nagel insert bool insert( stdelement e ) findkey(e) T F p = newnode() p->element = e success= true root == NULL T F e.key < current_ node ->element.key success= false TRUE root = p FALSE current_ node ->left = p current_ node ->right = p current_ node = p return success Wolfgang E. Nagel delete_key (Skizze) bool delete _ key_ s ( stdelement e ) suche nach dem Knoten , der entfernt werden soll found T einer der Teilbäume F dieses Knotens ist leer TRUE FALSE entferne den Knoten durch Umhängen des Zeigers des Vaters auf den nichtleeren Teilbaum (falls beide leer , ist der Teilbaum leer ) finde den am weitesten rechts stehenden Knoten des linken Teilbaumes (=> righty ) bringe den Inhalt von righty zu dem Knoten , der gelöscht werden soll entferne den Zeiger von rightys Vater und setze ihn auf den linken Teilbaum von righty gib den Speicherplatz für den Knoten frei gib den Speicherplatz für righty frei return found Wolfgang E. Nagel delete_key bool delete _ key( stdelement e ) !empty() TRUE success= del (e, &root) FALSE success= false current_ node = root return success Wolfgang E. Nagel del static bool del ( stdelement e , node _ pointer *addr _ of _ nodeptr ) p = *addr _ of _ nodeptr p == NULL T F e . key < p - >element . key T F e . key > p - >element . key T F remove = p success = success = true success = success = p - >right == NULL T del ( e , F p - >left == NULL &( p - >left ) del ( e , *addr _ of _ TRUE nodeptr = false ) &( p - >right ) ) p - >left *addr _ of _ nodeptr = p - >right free ( remove ) return success Wolfgang E. Nagel FALSE subdel ( &( p - >left ) , &remove ) subdel static void subdel ( node _ pointer *addr _ of_ nodeptr , node _ pointer *addr _ of_ remove ) q = *addr _ of_ nodeptr q ->right != NULL TRUE FALSE (*addr _ of_ remove )->element = q ->element subdel (&(q->right), addr _ of_ remove ) *addr _ of_ remove = q *addr _ of_ nodeptr = q->left Wolfgang E. Nagel delete_key (iterativ) bool delete_ key (stdelement e) success := true root == NULL T F p = root addr_ p = & root p != NULL & & e. key != p- >element.key success T e.key < p->element.key addr_ p = & ( p- >left) addr_ p = & ( p- > right) p = p- > left p = p- >right = false T success := false p == NULL del ( addr_ p ) current_ node = root return success Wolfgang E. Nagel F F del (iterativ) static void del ( node_ pointer *addr _ p ) p = *addr_ p remove = p p- >right == NULL T T F p- > left == NULL addr_ p = & ( p- >left) p = p- >left *addr_ p *addr _ p- >right != NULL addr _ p = & ( p- > right ) p = p- > p = p- > right remove- > element = p- > element = p- >left right remove = p *addr_ p = p- > left free( remove ) Wolfgang E. Nagel F update void update ( stdelement e ) delete_key ( current _ node - >element ) insert ( e ) Wolfgang E. Nagel Einfügen in einen binären Suchbaum ! keine balancierte Verteilung der Knoten wird garantiert ! bei ungünstiger Reihenfolge der Einfügeoperationen kann es zu linearen Listen statt balancierten Suchbäumen kommen Wolfgang E. Nagel Definitionen Definition: Ein Binärbaum heißt minimal genau dann, wenn kein Binärbaum existiert, der die gleiche Anzahl von Knoten aber eine niedrigere Höhe hat. ♦ Definition: Ein vollständiger Binärbaum ist ein minimaler Binärbaum, in dem die Knoten auf dem untersten Level so weit wie möglich links stehen. ♦ Wolfgang E. Nagel Suchen in vollen binären Suchbäumen Suchlänge zu einem Knoten: Weglänge von der Wurzel bis zum Knoten Suche in einem vollen binären Suchbaum: – worst case: log(n+1) entspricht O(log n) – average case: log(n+1) - 1 entspricht O(log n) Degenerierter Baum, d.h. auf jedem Level nur ein Knoten: – worst case: n entspricht O(n) – average case: (n+1)/2 entspricht O(n) è höhenbalancierte Bäume erzeugen Bekannte Varianten von höhenbalancierten Bäumen sind: – AVL-Bäume – 2-3-Bäume – B-Bäume Wolfgang E. Nagel Einfügen in einem höhenbalancierten Binärbaum A B B A T1 T3 T1 T2 T2 neuer Knoten Wolfgang E. Nagel T3 Datenstruktur B-Baum Definition: Jeder Knoten in einem B-Baum der Ordnung d enthält d bis 2d Elemente. Die Wurzel bildet die einzige Ausnahme, sie kann 1 bis 2d Elemente enthalten. Die Anzahl der Söhne in einem B-Baum ist entweder 0 oder um eins größer als die Anzahl der Elemente, die der Knoten enthält. Die Elemente in einem Knoten sind aufsteigend sortiert. ♦ Wolfgang E. Nagel B-Bäume der Ordnung d Jeder Knoten des Baumes kann als (p0, k1, p1, k2, p2,..., kn, pn) aufgefasst werden, wobei pi ein Zeiger auf den i-ten Sohn des Knotens ist (0≤i ≤2d) und ki ein Schlüssel (1≤i ≤2d). Für die Schlüssel innerhalb der Knoten gilt k1<k2<...<kn. Alle Schlüssel in dem Teilbaum, auf den p0 zeigt, sind kleiner als k1. Für 1≤i<2d liegen alle Schlüssel k des Teilbaumes, auf den pi zeigt, im Bereich ki<k<ki+1. Alle Schlüssel in dem Teilbaum, in den pn zeigt, sind größer als kn. Der längste Weg in einem B-Baum der Ordnung d ist logd+1 n. Wolfgang E. Nagel B-Baum der Ordnung 2 30 38 42 10 20 25 32 34 40 41 Wolfgang E. Nagel 44 50 56 Datenstruktur B-Baum Definition: B-Baum 1. Elemente Die Elemente sind von einem beliebigen Datentyp (stdelement) und sind durch eine Schlüssel eindeutig identifiziert. 2. Struktur Ein B-Baum ist ein geordneter Baum. Jeder Knoten, außer der Wurzel, hat einen eindeutigen Vater. Jeder Knoten, außer den Blättern, hat einen Sohn mehr als er Elemente enthält. Die Elemente in einem Knoten sind nach den Schlüsseln aufsteigend sortiert. Wolfgang E. Nagel Operationen auf dem B-Baum 3. Operationen Die Operationen sind denen eines Binärbaumes bzw. eines binären Suchbaumes sehr ähnlich. – empty und characteristics bleiben gleich – Für create und clear wird Binärbaum durch B-Baum ersetzt. – retrieve: Das Element, das geholt werden soll, muss aus 2d Möglichkeiten spezifiziert werden. – delete_sub: ist nicht erlaubt. – find: Es wird nicht nur der linke oder rechte Sohn angegeben, sondern es gibt 2d+1 mögliche Söhne. – traverse: inorder ist nicht erlaubt. – Die Operationen für einen binären Suchbaum findkey, update, insert und delete_key bleiben erhalten. ♦ Wolfgang E. Nagel Realisierung eines B-Baumes #define ORDER ... /* Ordnung des B-Baumes */ #define MAXNODE (2*ORDER) /* max. Anzahl Elemente */ typedef struct node * node_pointer; struct node { node_pointer parent; short number_of_elements; stdelement elements[MAXNODE]; node_pointer children[MAXNODE+1]; }; /* globale Variablen: */ static node_pointer root, current_node; Wolfgang E. Nagel /*0..MAXNODE */ Einfügen in einen B-Baum ! insert1: – Der Knoten enthält weniger als 2d Elemente und es kann ein zusätzliches Element eingefügt werden. – Das Element wird so eingefügt, dass die Ordnung erhalten bleibt. – Der Einfüge-Prozess ist beendet. ! insert2: – Der Knoten ist voll und muss aufgeteilt werden. – Ein neuer Knoten wird gebildet und erhält die d größten Elemente. – Die d kleinsten Elemente bleiben in dem Knoten. – Das mittlere Element wird an den Vater gegeben. – Der Einfüge-Prozess wird fortgesetzt. ! insert3: – Die Wurzel wird aufgeteilt. – Ein neuer Wurzelknoten wird gebildet. – Das Element mit dem mittleren Schlüssel wird das einzige Element der Wurzel. – Der Einfüge-Prozess ist beendet. Wolfgang E. Nagel insert bool insert( stdelement e ) success = !findkey (e ) success T F !empty() TRUE insert_ into _ node (current_ node , e ) FALSE bilde Wurzel mit dem Element e return success Wolfgang E. Nagel insert_into_node static void insert_ into _ node ( Knoten , Element ) füge neues Element der Ordnung nach ein Knoten enthält 2d +1 Elemente T F entferne mittleres Element aus dem Knoten bilde neuen Knoten aus den d größten Elementen Knoten == Wurzel TRUE bilde neue Wurzel mit dem mittleren Element FALSE insert_ into _ node (Vater , mittleres Element ) Wolfgang E. Nagel Löschen in einem B-Baum ! target_node bezeichnet den Knoten, in dem das Element target_element gelöscht werden soll. ! delete1: target_node enthält mehr als d Elemente. target_element wird gelöscht. Der Prozess ist beendet. Wolfgang E. Nagel Löschen in einem B-Baum ! delete2: target_node enthält genau d Elemente. Wenn der rechte oder linke Bruder mehr als d Elemente enthält, kann ein Element an target_node abgegeben werden. Dadurch muss der Vater geändert werden. Wenn der rechte und linke Bruder genau d Elemente enthält, wird target_node mit einem Bruder zu einem Knoten mit 2d Elementen verschmolzen. Das Element des Vaters, das diese beiden Knoten trennte, ist auch in dem Knoten enthalten. Wenn der Vater mehr als d Elemente enthält, wird der Prozess beendet; sonst wird der Prozess mit dem Vater als neuem target_node fortgeführt. Wolfgang E. Nagel Löschen in einem B-Baum ! delete3: target_node ist die Wurzel. Solange mindestens ein Element übrigbleibt, ändert sich die Höhe des Baumes nicht. Wenn das letzte Element entfernt wird, so wird der einzige Sohn zur neuen Wurzel. Wolfgang E. Nagel delete void delete ( target _ node , target _ element ) target _ node == Blatt TRUE FALSE suche Element s mit nächstgrößerem delete _ leaf ( target _ node Blatt x ersetze target _ element durch s , target _ element ) delete _ leaf ( x, s) Wolfgang E. Nagel Schlüssel in delete_leaf static void delete _ leaf ( target _ node , target _ element ) lösche target _ element Anzahl Elemente < d T linker oder rechter Bruder hat > d Elemente TRUE F FALSE verschmelze target _ node mit einem Bruder ein Element wird über Vater an target _ node abgegeben den übernimm das trennende Element s des Vaters v in diesen Knoten delete _ leaf ( v, s) Wolfgang E. Nagel Fakultät Informatik, Institut für Technische Informatik, Professur Rechnerarchitektur Effiziente Parallele Algorithmen Parallele Maschinenmodelle Zellescher Weg 12 Nöthnitzer Straße 46 Willers-Bau A 205 Raum 1044 Tel. +49 351 - 463 - 35450 Tel. +49 351 - 463 - 38246 Wolfgang E. Nagel ([email protected]) Laufzeit von Algorithmen Ermittlung der Laufzeit von Algorithmen ! Implementierung des Algorithmus in einer konkreten Programmiersprache auf einen konkreten Rechner – konkrete Ergebnisse für die Laufzeit als Funktion der Eingabedaten – Übertragbarkeit der Ergebnisse problematisch ! Nutzung eines idealisierten Modellrechners als Referenzmaschine – einfache Handhabung, breite Nutzung – höhere Allgemeingültigkeit der Ergebnisse – Übertragbarkeit auf reale Maschinen problematisch Wolfgang E. Nagel RAM (Random Access Machine) Sequentielle Registermaschine ! Modell für konventionelle Einprozessorrechner ! Wichtiges Hilfsmittel für – Berechnung der Laufzeit von Algorithmen – Ermittlung der Komplexität von Problemen ! RAM-Modell hat sich für die theoretische Analyse von sequentiellen Algorithmen weitgehend durchgesetzt ! Besteht aus: – Prozessor, der ein ausgezeichnetes Register, den Akkumulator, und einen Befehlszähler besitzt – Lokalen Speicher mit abzählbar unendlich vielen Zellen (oder auch Registern) L[0], L[1], L[2], ... ! Bildet Basis der PRAM Wolfgang E. Nagel PRAM (Parallel Random Access Machine) Parallele Registermaschine ! Erfinder sind Fortune und Wyllie 1978 ! Globaler Speicher (abzählbar unendlich viele Speicherzellen G[0],G[1], ...) ! Abzählbar unendlich viele modifizierte RAMs (Random Access Machines) – klassische RAM wurde durch ein Flag RUNNING erweitert, das anzeigt, ob der Prozessor gerade aktiv ist Wolfgang E. Nagel PRAM (Parallel Random Access Machine) ! synchrone Verarbeitung im Sinne einer synchronisierten SPMD-Verarbeitung – Steuerung aller Prozessoren durch gemeinsamen Takt – Prozessoren führen zu einem Zeitpunkt die selbe oder auch verschiedene Rechenoperationen aus – Maß für die Kosten ist der PRAM-Schritt, besteht aus: • Lesen von Daten aus dem gemeinsamen Speicher • Ein Berechnungsschritt • Schreiben in den gemeinsamen Speicher ! Zusätzliche Synchronisations- und Speicherzugriffskosten werden nicht berücksichtigt Wolfgang E. Nagel PRAM (Parallel Random Access Machine) ! ! G[0]! G[1]! G[2]! G[3]! ACCU! PC! RUNNING! P0! …! L[0]! L[1]! L[2]! …! …! ! ! ! ACCU! PC! RUNNING! ! ! ! L[1]! L[2]! P1! …! RAM 1! Wolfgang E. Nagel …! L[0]! …! RAM 0! ! …! Modifikationen der PRAM ! EREW (Exclusive Read Exclusive Write) – PRAM ! CREW (Concurrent Read Exclusive Write) – PRAM ! CRCW (Concurrent Read Concurrent Write) – PRAM ! [ ERCW ( Exclusive Read Concurrent Write) - PRAM ] Wolfgang E. Nagel PRAM (Parallel Random Access Machine) praktische Handhabung ! Bestimmung der Zeitkomplexität von parallelen Algorithmen • Zeitaufwand als Funktion der Problemgröße ! Bestimmung der asymptotischen Zeitkomplexität parallelen Algorithmen • Grenzverhalten: Problemgröße gegen unendlich • Keine Beschränkung des Parallelismus, da Prozessoranzahl der PRAM unbeschränkt • Zumeist ist man nur an der „Größenordnung“ einer Komplexitätsfunktion interessiert - O(1), O(log n), O(n), O(n2), O(nk), O(2cn) Wolfgang E. Nagel PRAM (Parallel Random Access Machine) Diskussion ! Welche asymptotische Zeitkomplexität hat der Algorithmus „Rekursives Doppeln“? ! Welche asymptotische Zeitkomplexität hat der Algorithmus „Multiplikation zweier n * n Matrizen“? ! Was wäre eine ideale asymptotische Zeitkomplexität? ! Macht die Entwicklung immer schnellerer Computer die Entwicklung asymptotisch schnellerer Algorithmen überflüssig? ! Wie schätzen Sie das Butget-Verhältnis innerhalb der Grand Challenge Initiative der USA ein bzgl.: – Entwicklung neuer Rechentechnik – Entwicklung neuer Algorithmen? Wolfgang E. Nagel P-PRAM (PRAM mit begrenzter Prozessoranzahl) ! Abschätzung des Sp (Speedup mit p Prozessoren) hat in der Parallelverarbeitung eine zentrale Bedeutung T1 Sp = Tp p T1 Tp Anzahl der Prozessoren Zeit(-schritte) mit p = 1 Prozessoren Zeit(-schritte) mit p > 1 Prozessoren à Erfordert ein paralleles Maschinenmodell mit begrenzter Prozessoranzahl (P-PRAM) ! Basis für Skalierbarkeitsanalysen ! Entwicklung und Darstellung von Algorithmen auf der Grundlage der P-PRAM Wolfgang E. Nagel PRAM Bewertung der PRAM ! sehr einfach, breite Anwendung bei der Bewertung von Algorithmen ! gute Vergleichbarkeit von theoretischen Ergebnissen ! liefert Aussagen zur asymptotischen Zeitkomplexität und zur logischen Struktur von Algorithmen ! oft ungeeignet für die Vorhersage von Laufzeiten realer Rechner – Speicher der PRAM ist uniform à Speicherhierarchie bei realen Maschinen ! synchrone Berechnung à zumeist asynchrone Berechnung in der Praxis Wolfgang E. Nagel PRAM Bewertung der PRAM ! Anzahl der Prozessoren skaliert mit dem Problem à begrenzte Prozessoranzahl bei realen Maschinen à P-PRAM liefert als eine Sonderform der PRAM Aussagen zur Laufzeit von Algorithmen mit begrenzter Prozessoranzahl ! jede Berechnung, incl. Kommunikation, dauert einen Zeitschritt à unterschiedliche Kommunikationszeiten (Nachrichtenlänge, Entfernung der Prozessoren, Zugriffskonflikte) à Laufzeit-Unterschiede bei verschiedenen arithmetischen Operationen Fazit: Verwendung von parallelen Maschinenmodellen in Betracht ziehen, die Kommunikation berücksichtigen Wolfgang E. Nagel MP-RAM (Message Passing Random Access Machine) Kennzeichen einer MP-RAM ! autonome Verarbeitungseinheiten, die ausschließlich über Nachrichtenaustausch kommunizieren ! N Verarbeitungseinheiten (VE), jede besitzt einen lokalen Speicher ! Verbindungsnetzwerk mit fester Topologie (z.B. 2-d-Gitter) ! Ergänzung des Instruktionsvorrates jeder VE durch eine (synchrone Send- und Receive-Operation à erlaubt nur Datenaustausch zwischen benachbarten Knoten à erzwingt eine Abschätzung der Kommunikationsschritte à Berücksichtigung der Topologie der Kommunikationsnetzwerkes Wolfgang E. Nagel LogP-Modell Parameter des Modells heißen L, o, g, P und geben dem Maschinenmodell seinen Namen ! Kommunikationsverzögerung (communication latency) L - obere Schranke für die Übertragungszeit einer Nachricht (mit geringer Anzahl von übertragenen Werten) ! zusätzliche Kommunikationskosten (communication overhead) o - Rechenzeit, die ein Prozessor zum Absetzen oder Empfangen einer Nachricht benötigt ! Kommunikationstotzeit (gap) g - Zeit, die zwischen zwei aufeinander folgenden Sende- oder Empfangsoperationen liegen muss ! Anzahl der Prozessoren (number of processors) p Wolfgang E. Nagel LogP-Modell weitere Kennzeichen des LogP - Modell ! alle Prozessoren über Kommunikationsnetzwerk direkt miteinander verbunden ! Kanalbandbreite begrenzt ! Parameter L, o, g, P sind wählbar à Modell anpassbar an verschiedene Parallelrechner à L, o, g werden als Vielfaches des Maschinenzyklus der zu modellierenden Maschine angegeben ! jede Sende bzw. Empfangsoperation fordert vom Prozessor o Maschinenzyklen à alle Sende bzw. Empfangsoperation erfolgen sequentiell ! alle arithmetischen Operationen einen Maschinenzyklus Wolfgang E. Nagel Parallele Maschinenmodelle Zusammenfassung ! PRAM und P-PRAM sehr weit verbreitet – Bilden auch Grundlage für Abschätzung von Zeitkomplexitäten innerhalb der Lehrveranstaltung „Effiziente parallele Algorithmen“ ! LogP-Modell ist am besten geeignet, wenn theoretisch ermittelte Zeitkomplexitäten auf die Laufzeit realer Maschinen übertragen werden sollen – Vertiefung innerhalb der Lehrveranstaltung „Konzepte der parallelen Programmierung“ (Dr. Trenkler) Wolfgang E. Nagel Fakultät Informatik, Institut für Technische Informatik, Professur Rechnerarchitektur Effiziente parallele Algorithmen Suchen Zellescher Weg 12 Nöthnitzer Straße 46 Willers-Bau A 205 Raum 1044 Tel. +49 351 - 463 - 35450 Tel. +49 351 - 463 - 38246 Wolfgang E. Nagel ([email protected]) Algorithmen ! Suchen von Elementen ! Sortieren von Elementen ! Effiziente Speicher- und Zugriffsverfahren ! Graphenalgorithmen Ziel: Effiziente Verfahren Wolfgang E. Nagel Suchen Definition: Eine Schlüsselmenge ist eine linear geordnete endliche Menge S = {s1, ..., sn} . ♦ Bemerkung: ! Schlüssel dienen der eindeutigen Identifikation von Daten und werden i. A. zusammen mit den Daten abgespeichert oder sind Bestandteil der Daten. ! Schlüssel müssen nicht Zahlen sein. Auch Namen können als Schlüssel verwendet werden (lexikographische Sortierung). Wolfgang E. Nagel Bemerkungen ! Such- und Sortieralgorithmen werden hier nur für Felder betrachtet ! Indizes haben Wertebereich 1..n ! Feldelemente haben eine Komponte key, die den Schlüssel enthält, also z.B. struct { int key; double daten; } liste[N+1]; ! Vergleich zweier Elemente: if( liste[i].key < liste[j].key) ... Wolfgang E. Nagel Lineares Suchen (Sequential Search) list_ index seq_ search( keytype key ) i =1 element = 0 (element == 0) && (i <= n) list[i ].key == key T F element = i i ++ return element Wolfgang E. Nagel Lineares Suchen (Sequential Search) Vorteile des Algorithmus: ! Einfach zu programmieren. ! Schnell für kurze Listen. ! Liste braucht nicht sortiert zu sein. Nachteile des Algorithmus: ! Hoher Zeitbedarf für lange Listen. ! Im Mittel müssen (n+1)/2 Elemente betrachtet werden. Wolfgang E. Nagel Sortierte Liste Definition: Eine Liste mit n Elementen heißt (nach Schlüsseln) sortierte Liste, falls gilt: ∀ 1 ≤ i ≤ n-1 : Feld[i].Schlüssel ≤ Feld[i+1].Schlüssel. ♦ Wolfgang E. Nagel Binäres Suchen (Binary Search) Voraussetzung: sortierte Listenelemente Prinzip: ! Greife zuerst auf das mittlere Element zu ! Prüfe, ob das gesuchte Element a. kleiner b. gleich oder c. größer ist als das betrachtete Element. Im Fall b. ist man fertig. Im Fall a. bzw. c. wird mit der linken bzw. rechten Teilliste fortgefahren. Wolfgang E. Nagel Binäres Suchen (Binary Search) list _ index bin _ search ( keytype key ) low = 1 high = n element = 0 (element == 0 ) && (low <= high ) i = ( low +high ) /2 middle = list [i ].key key > middle T F key < middle low = i + 1 TRUE FALSE high = i - 1 element = i return element Wolfgang E. Nagel Komplexität des binären Suchens Bei jedem Suchschritt halbiert sich die Anzahl der für die Suche relevanten Elemente. ⇒ im worst-case ⎡log2 n+1⎤ Suchschritte. Veranschaulichung durch binären Suchbaum. Mit k Suchschritten sind 2k -1 Elemente erreichbar. Nach ⎡ log2 n+1 ⎤ -1 Schritten ist ungefähr die Hälfte aller Elemente erreichbar, d.h. der Suchaufwand im Mittel ist nicht wesentlich geringer. Wolfgang E. Nagel Vor-/Nachteile des binären Suchens Vorteil des Algorithmus: ! Komplexität O(log2 n). Nachteil des Algorithmus: ! Liste muss sortiert sein, d.h. zum Suchaufwand kommt der Sortieraufwand hinzu. Der Algorithmus ist günstig, falls oft gesucht, aber selten eingefügt oder gelöscht werden muss. Beim Einfügen sind u.U. spezielle Einfüge-Operationen notwendig, die die Eigenschaft ‚sortiert‘ erhalten. Wolfgang E. Nagel Hash-Verfahren Binärer Suchbaum in höhenbalancierter Version (z.B. B-Baum) erlaubt effiziente Zugriffe zu Elementen großer Mengen. Bei n = 10.000 Elementen wird jedes beliebige Element in log2 n ≈ 14 Schritten gefunden. Ziel: Zugriffsverhalten im average-case günstiger als log n zu machen. à Hash - Verfahren Annahme: Feld a[N], in das die Elemente gespeichert werden sollen. address calculator, der für einen Schlüssel X einen Feldindex i angibt, wo das Element gespeichert werden soll. Wolfgang E. Nagel Feld Index 1 2 i X adress calculator 3 R 4 T n-1 ... X n Wolfgang E. Nagel Definitionen Definition: Sei S eine Schlüsselmenge und I eine Adressmenge im weitesten Sinn. Dann heißt h : S → I Hash - Funktion. Die Bildmenge h(S) ⊆ I bezeichnet die Menge der Hash-Indizes. ♦ Bemerkung: Die Schlüsselmenge ist im allgemeinen sehr viel größer als die Adressmenge. Deshalb wird eine Hash-Funktion surjektiv [∀b∈I:∃a∈S:h(a)=b], aber nicht injektiv [h(a)=h(b)àa=b] sein. Wolfgang E. Nagel Beispiele Schlüsselmenge: Strings Adressmenge: 0..25 Schlüssel ! h1(s) = s[0] - ‚A‘ I J ia ib ! h2(s): index = 0; for(i=0; i < strlen(s); ++i) index += s[i] - ‘A‘; index = index % 26; Wolfgang E. Nagel I J ia ib Adressen Beispiel h3(s) = (ord(s[0]) + ord(s[1]) + ord(s[2])) % 17 mit ord(x) = toupper(x) - ‚A‘ 0: 8: Juni 1: 9: August, Oktober 2: 10: Februar 3: Mai, September 11: 4: 12: 5: Januar 13: 6: Juli 14: November 7: 15: April, Dezember 16: März Wolfgang E. Nagel Definition Definition: Sei S Schlüsselmenge, h Hash-Funktion. Ist für s1 ≠ s2 (mit si ∈ S) h(s1) = h(s2), so spricht man von einer Kollision. ♦ Bemerkung: Kollisionen sind abhängig von der Hash-Funktion. Deshalb sind solche HashFunktionen gesucht, die gut streuen! Weiterhin sollte die Hash-Funktion effizient berechenbar sein. Wolfgang E. Nagel Wahrscheinlichkeit von Kollisionen ! Annahme: ideale Hash-Funktion, d.h. gleichmäßige Verteilung über die HashTabelle ! Geburtstagproblem: Wie groß ist die Wahrscheinlichkeit, dass mindestens 2 von n Leuten am gleichen Tag Geburtstag haben? ! Analogie zum Geburtstagsproblem: – m = 365 Tage = Größe Hash-Tabelle – n Personen = Zahl Elemente Wolfgang E. Nagel Wahrscheinlichkeit von Kollisionen p(i;m) := Wahrscheinlichkeit, dass der i-te Schlüssel auf einen freien Platz abgebildet wird (i=1,...,n) wie alle Schlüssel vorher n n −1 n −1 i P( NoKo ln, m ) = ∏ p(i; m) = ∏ (1 − ) m i =0 i =0 Wolfgang E. Nagel P(Kol n,m) 10 0,11695 20 0,41144 22 23 24 0,4757 0,5073 0,53835 30 40 50 0,70632 0,89123 0,97037 Änderung der Größe der Hash-Tabelle Situation: Doppelt so viele Elemente wie bisher sollen in einer Hash-Tabelle gespeichert werden. Wie soll m mit n wachsen, um P(NoKoln,m) konstant zu halten? D.h. um wieviel muss die Hash-Tabelle vergrößert werden, um die Kollisionswahrscheinlichkeit konstant zu halten? Ohne Beweis: P(NoKoln,m) bleibt in etwa konstant, wenn m (Größe der Hash-Tabelle) quadratisch mit n (Anzahl Elemente) wächst. Wolfgang E. Nagel Kollisionsbehandlung ! Situation: zwei Einträge werden durch die Hash-Funktion auf die gleiche Feldadresse abgebildet, d.h. h(s1) = h(s2) ! verschiedene Strategien bei Kollisionsbehandlung möglich: – Hash in Teillisten – offenes Hash-Verfahren Wolfgang E. Nagel Hash in Teillisten (Variante 1) Prinzip: Die Hash-Tabelle besteht aus n linearen Listen. h(s) = 0 1 h(s) = 1 . . . . . . . 0 ... n-1 Wolfgang E. Nagel Hash in Teillisten (Variante 2) Prinzip: Die Hash-Tabelle besteht aus n Teillisten der fixen Länge k. 0 1 k-1 h(s) = 0 1 h(s) = 1 . . . . . . . 0 ... ... n-1 Wolfgang E. Nagel Speicherung: 1. Zunächst wird mit Hilfe der Hash-Funktion aus dem Schlüssel der HashIndex (1 ≤ h(s) ≤ n) berechnet. Dieser gibt an, in welcher Teilliste der Datensatz gespeichert wird. 2. Innerhalb der Teillisten wird sequentiell gespeichert. Bemerkung: Das Suchen in der Teilliste bleibt sequentiell. Die Speicherung in der (sequentiellen) Teilliste kann effizient vorgenommen werden, wenn zu jeder Teilliste ein Pointer existiert, der jeweils auf den ersten freien Listenplatz zeigt. Wolfgang E. Nagel Realisierung einer Hash-Tabelle in Teillisten #define N 1000 #define K 10 typedef struct { int key; char name[20]; char ort[20]; } daten; static daten tabelle[N][K]; static short pointer[N]; Wolfgang E. Nagel Bemerkung: ! Das Feld Pointer enthält die Indizes des jeweils ersten freien Platzes in jeder Teilliste und dient gleichzeitig dazu, festzustellen, ob eine Teilliste voll ist. ! Die Teillisten können als verkettete Liste implementiert werden. Beim Speichern eines Datensatzes ist dann Einfügen am Anfang sinnvoll. Wolfgang E. Nagel Schrittzahl beim Suchen: Sei n die Anzahl der Teillisten und m die Anzahl der gespeicherten Records. Bei idealer Verteilung der Schlüssel entfallen α = m/n Elemente auf jede Teilliste. α wird Füllfaktor der Hash-Tabelle genannt. Für den Aufwand (Zugriffe auf Tabelle) gilt: erfolgreiches Suchen: 1 + α /2 erfolgloses Suchen: 1 + α Damit dauert das Suchen O( m/n ) Schritte (m ≥ n ). Ist m < n ⇒ mindestens ein Suchschritt. Folgerung: Um schnell suchen zu können, müssen größenordnungsmäßig so viele Teillisten wie Records vorhanden sein. Wolfgang E. Nagel Offenes Hash - Verfahren Prinzip: 1. Berechnung einer Speicheradresse aus dem Schlüssel eines Datensatzes mit Hilfe einer geeigneten Hash-Funktion. Ist der so berechnete Speicherplatz frei, so wird der Datensatz dort gespeichert. 2. Ist der errechnete Speicherplatz durch einen anderen Datensatz belegt, so liegt eine Kollision vor. Berechnung einer Ersatzadresse und Wiederholung des Speicherversuches. Ist der berechnete Speicherplatz wiederum belegt, so wird erneut eine Ersatzadresse berechnet, bis ein freier Platz gefunden wird oder der verfügbare Speicher ganz durchlaufen wurde. 3. Das Lesen (Suchen) von Elementen erfolgt analog, bis das gesuchte Element gefunden ist. 4. Löschoperationen sind jetzt sehr aufwendig! Wolfgang E. Nagel Beispiel offenes Hash-Verfahren Eine Firma hat zur eindeutigen Identifizierung ihrer Mitarbeiter Personalnummern im Bereich von 1...9999 vergeben. Sie hat zur Zeit 60 Mitarbeiter und hat zur Speicherung der Personaldaten deshalb nur 100 Speicherplätze für Datensätze bereitgestellt. Schlüsselmenge: S = {1...9999} Hash-Indizes: I = {1...100} Hash-Funktion: h : S → I mit h(s) = s mod 100 + 1 Bei einer Kollision wird der berechnete Hash-Index solange um 1 (modulo 100) erhöht, bis ein freier bzw. der gesuchte Eintrag gefunden wird. Wolfgang E. Nagel Divisions-Hash Definition: Divisions-Hash Ein Hash-Verfahren, das die Hash-Indizes aus dem Schlüssel mit Hilfe der Modulo-Funktion berechnet, heißt Divisions-Hash. Wird die Ersatzadresse bei jeder Kollision durch Erhöhen der alten Adresse um 1 berechnet, so spricht man von linearem Sondieren (linearem Probing). Die i-te Ersatzadresse für einen Schlüssel s mit Hash-Index h(s) wird also wie folgt berechnet: ei(s) = (h(s) + i) mod n (e0(s) = h(s) ist der Hash-Index der Hash-Funktion selbst) ♦ Wolfgang E. Nagel Bemerkung: Nachteil des Divisions-Hash mit linearem Sondieren ist, dass sich an Stellen mit häufigen Kollisionen leicht Ketten bilden, so dass sich die Wahrscheinlichkeit für weitere Kollisionen in diesem Bereich noch erhöht. Das Verfahren streut also nicht wirklich. Diesen Effekt [h0(s) ≠ h0(t) ^ hi(s) = hi(t) à hi+1(s) = hi+1(t)] nennt man primäre Häufung (primary clustering). Ein weiterer Effekt ist die sekundäre Häufung (secondary clustering). Wenn zwei Schlüssel denselben Hashwert haben h0(s) = h0(t), so ist auch hi(s) = hi(t). Wolfgang E. Nagel Quadratisches Sondieren Definition: Die Strategie, bei der die Funktion ei(s) := (h(s) + i2 ) mod n zur Berechnung der Ersatzadresse gewählt wird, heißt quadratisches Sondieren. ♦ Wolfgang E. Nagel Quadratisches Sondieren (Sei n = 11, d.h. 11 Speicheradressen) i 0 1 2 3 4 i2 0 1 4 9 16 25 36 49 64 81 100 i2 mod n 0 1 4 9 5 5 3 6 3 7 5 8 9 9 4 10 1 Bemerkungen: – Eine primäre Häufung findet nicht mehr statt. – Ein Nachteil ist aber offensichtlich, dass ei(s) = en-i(s) gilt, d.h. nicht alle zur Verfügung stehenden Adressen werden erreicht. Wolfgang E. Nagel Satz: (ohne Beweis) Ist n eine Primzahl, so sind die Zahlen i2 mod n für 0 ≤ i ≤ n/2 paarweise verschieden. Hiermit lässt sich also bei geeigneter Wahl der Tabellengröße immerhin der halbe Speicherplatz überdecken. ♦ Satz: (ohne Beweis) Ist n ≡ 3 mod 4 (n kongruent 3 modulo 4, d.h. n lässt bei Division durch 4 den Rest 3) und n Primzahl, so ist die Menge der Reste modulo n von i2 für i ungerade - i2 für i gerade (inkl. i = 0) ein volles Restsystem modulo n, d.h. alle möglichen Reste kommen genau einmal vor. ♦ Wolfgang E. Nagel Daher: Die Funktion ei(s) := (h(s) + (-1)i+1 i2 ) mod n mit n ≡ 3 mod 4 und n Primzahl stellt n verschiedene Ersatzadressen bereit. Wolfgang E. Nagel Vollständiger Hash Sei n = 11, d.h. 11 Speicheradresse. n=11 erfüllt die oben genannte Voraussetzung, wegen (11 div 4) = 2 mit einem Rest von 3. i (-1)i+1 i2 mod n 0 1 2 3 4 5 6 7 8 9 10 0 1 7 9 6 3 8 5 2 4 10 Bemerkungen: – Ist (-1)i+1 i2 mod n negativ, so muss der Wert um n erhöht werden. – Laufen die Adressen nicht von 0 bis n-1, sondern von 1 bis n, so ist die Ersatzadresse entsprechend um 1 zu erhöhen. Andere Indexbereiche sollten vorher auf den Adressbereich 0 bis n-1 transformiert werden. Wolfgang E. Nagel Definitionen Definition: Unter der Schrittzahl S(s) versteht man die Anzahl der zu berechnenden Hash-Indizes und Ersatzadressen, die nötig sind, um den Datensatz mit Schlüsseln s zu speichern bzw. wiederzufinden. ♦ Definition: Der Füllungsgrad einer Hash-Tabelle ist der Quotient α = k/n, wobei n die Anzahl der Tabellenplätze (Adressen) und k die Anzahl der belegten Tabellenplätze ist. ♦ Wolfgang E. Nagel Bemerkung: Für ein ideales Hash-Verfahren gilt: 1. Zu Beginn eines Speicherversuches sind alle möglichen SpeicherBelegungen gleich wahrscheinlich. 2. Die Anzahl der Speicherversuche ist unabhängig davon, welche Adressen bereits belegt sind. Bei einem idealen Hash-Verfahren ist der Erwartungswert für die mittlere Schrittzahl S(n,k) bei der Speicherung eines Datensatzes in einer Tabelle mit n Plätzen, von denen k bereits belegt sind, gegeben durch: S(n,k) = n+1 n-k+1 Wolfgang E. Nagel Eigenschaften des offenen Hash-Verfahrens ! Zugrundeliegende Datenstruktur: Speicher mit wahlfreiem Zugriff (z.B. Array, Array of Records, etc.). ! Schlüssel werden zusammen mit den Daten gespeichert. Schlüssel sind eindeutig. ! Es muss erkennbar sein, ob ein Speicherplatz belegt ist oder nicht (z. B.Schlüsselfeld leer, extra Bit, etc.). ! Beim Füllen der Tabelle sollten die auftretenden Schlüssel möglichst gleichmäßig auf die verfügbaren Adressen verteilt werden (keine Häufung). Wolfgang E. Nagel Eigenschaften des offenen Hash-Verfahrens ! Beim Berechnen der Ersatzadressen sollten alle möglichen Adressen (genau einmal) durchlaufen werden. ! Bei festem Füllungsgrad ist die mittlere Schrittzahl beim Suchen niedriger als die beim Speichern. ! Die Tabellengröße kann nicht dynamisch verändert werden. Beim vollständigen Hash muss sie zusätzlich eine Primzahl n ≡ 3 mod 4 sein. Wolfgang E. Nagel Vergleich Offene Hash-Verfahren - Verfahren mit Teillisten ! Der Speicherplatzbedarf ist bei Hash in Teillisten im allgemeinen höher, denn die Hash-Tabelle ist schon voll, wenn eine Teiltabelle voll ist (unter Umständen ist nur 1/n der Tabelle belegt). ! Die Zugriffszeit beim Hash in Teillisten ist nur dann günstig, wenn die Teillisten kurz sind. Diese entspricht einer Situation mit wenigen Kollisionen beim offenen Hash. ! Löschen von Elementen ist beim Hash in Teillisten ohne Probleme möglich! Wolfgang E. Nagel Bemerkungen ! Eine injektive Hash-Funktion bedeutet indizierter Array-Zugriff mit O(1) (Indexmenge muss klein sein) ! Aufwand der Hash-Funktion muss gering sein ! Schlechte Hash-Funktion [h(s)=k] bedeutet Suchen in linearer Liste ! Gute Kenntnis der Daten ist wichtig ! Hash-Verfahren nur, wenn: 1. Hash-Funktion nicht injektiv sein kann oder Indexmenge zu gross wird 2. Sehr oft auf die Daten zugegriffen wird. 3. Kaum Daten gelöscht werden. 4. Daten bekannt sind. Wolfgang E. Nagel Fakultät Informatik, Institut für Technische Informatik, Professur Rechnerarchitektur Effiziente parallele Algorithmen Sortieren Zellescher Weg 12 Nöthnitzer Straße 46 Willers-Bau A 205 Raum 1044 Tel. +49 351 - 463 - 35450 Tel. +49 351 - 463 - 38246 Wolfgang E. Nagel ([email protected]) Sortierverfahren Sortierverfahren internes Sortieren externes Sortieren die zu sortierenden die zu sortierenden Elemente liegen Elemente brauchen nicht vollständig im vollständig im Hauptspeicher Hauptspeicher zu liegen Wolfgang E. Nagel Sortierverfahren Sortierverfahren Allgemeine Voraussetzungen: ! Daten seien mit Schlüsseln in einem Feld gespeichert. ! Für den Zugriff zu den Daten sei wahlfreie Adressierung gegeben (eindimensionales Feld von Records). Bemerkung: Das Sortieren von Daten kann auf zwei Arten geschehen: 1. Daten (inkl. Schlüssel) werden im Speicher auf die richtige Position gebracht (vertauscht). 2. Die Daten bleiben im Speicher an ihrer ursprünglichen Position. Der Zugriff zu den Daten erfolgt über eine "Sortierpermutation" der Indizes. Wolfgang E. Nagel Definitionen Definition: Eine Permutation σ über (1..n) heißt eine Sortierpermutation der Liste A, falls gilt: ∀ i, j 1≤ i < j ≤ n: A[σ(i)].key ≤ A[σ(j)].key ♦ Definition: Eine Sortiermethode heißt stabil, wenn die relative Ordnung der Elemente mit gleichen Schlüsseln durch den Sortierprozess unverändert bleibt ♦ Wolfgang E. Nagel Beispiel Sortierpermutation 1 2 3 4 5 6 7 8 9 72 38 12 21 73 11 13 23 35 6 3 7 4 8 9 2 1 5 Index Daten (key) Sortierpermutation Das Sortieren mit Hilfe einer Sortierpermutation ist u.U. dann sinnvoll, wenn das Umspeichern der Datenrecords sehr aufwendig ist oder wenn eine Liste nach mehreren Schlüsseln sortiert werden soll. Implementierung z.B. über einen Indexvektor int ix[N] mit ix[i]=i (i=1,...,N) initialisiert. Nach dem Sortieren steht im allgemeinen in ix[i] ein Wert ungleich i. Der Zugriff auf die Datenelemente geschieht durch Daten[ix [i]]. Wolfgang E. Nagel Beispiel stabiles Sortieren char Daten [NDATEN] [MAXSTRLEN]; Schlüssel: Daten[i][0], d.h. erster Buchstabe Datensatz: "Heinz", "Anton", "Ulli", "Alfred", "Wolfgang" Stabiles Sortieren: "Anton", "Alfred", "Heinz", "Ulli", "Wolfgang" Instabiles Sortieren: "Alfred", "Anton", "Heinz", "Ulli", "Wolfgang" Stabilität wichtig, wenn man z.B. vorher/nachher nach einem anderen Kriterium sortiert. Bei den Verfahren, die wir betrachten, sind nur das Sortieren durch Einfügen und der Bubblesort stabile Verfahren. Wolfgang E. Nagel Sortieren durch Auswahl (Selection Sort) Grundidee: for(i=1; i<n; ++i) { 1) Suche kleinstes Element in a[i]..a[n] und weise Index der Variablen min zu 2) Vertausche a[i] und a[min] } Wolfgang E. Nagel Selection Sort void sel_ sort( void ) for (i = 1 ; i <= (n -1 ); i ++) min = i for (j = (i +1 ); j <= n ; j ++) a [j ].key < a [min ].key T F min = j help = a [min ] a [min ] = a [i ] a [i ] = help Wolfgang E. Nagel Zeitkomplexität Im i-ten Schritt ist das Minimum aus (n-i+1) Elementen zu suchen, wobei i von 1 bis (n-1) läuft. Im ersten Schritt sind es also (n-1) Vergleiche. n!1 T av sel _ sort (n) = (n !1) + (n ! 2) +…+1 = " i = i=1 n(n !1) # O(n 2 ) 2 Vertauscht werden maximal n-1 Elemente, da das Vertauschen in der äußeren Schleife stattfindet. Wolfgang E. Nagel Sortieren durch Auswahl (Selection Sort) Vorteile des Algorithmus: ! Einfach zu programmieren. ! Schnell für sehr kleine Werte von n (z.B. n<20). Nachteile des Algorithmus: ! Hohe Zeitkomplexität von O(n2). ! Der Algorithmus erkennt nicht, ob eine Liste vorsortiert ist. Bei total sortierter Eingabe ist der gleiche Rechenaufwand nötig. Wolfgang E. Nagel Selection Sort (Permutationsvektor) void sel _ sort_ permutation ( void ) for (i = 1 ; i < = n ; i + + ) s[ i ] = i for (i = 1 ; i < = (n -1 ); i + + ) min = i for (j = (i + 1 ); j < = n ; j + + ) a [ s[ j ] ] . key < a [ s[ min ] ] . key T F min = j help = s[ min ] s[ min ] = s[ i ] s[ i ] = help Wolfgang E. Nagel Sortieren durch Einfügen (Insertion Sort) Grundidee: for(i=2; i<=n; ++i) { 1) x = a[i] 2) füge a[i] am entsprechenden Platz in a[1]...a[i] ein } Wolfgang E. Nagel Insertion Sort void ins _ sort( void ) for (i = 2 ; i <= n ; i ++) x = a [i ] a [0 ] = x for (j = (i -1 ); a [j ].key > x.key; j --) a [ j +1 ] = a [ j ] a [ j +1 ] = x Wolfgang E. Nagel Erläuterung Zur Bestimmung des entsprechenden Einfügeplatzes ist es angebracht, zwischen Vergleichen und Bewegungen abzuwechseln, d.h. x "nach links wandern zu lassen", mit dem nächsten Element a[j] zu vergleichen und entweder x einzufügen oder a[j] nach rechts zu schieben und nach links fortzufahren. Es gibt zwei Möglichkeiten, durch die der Prozess des "nach links Wanderns" beendet werden kann: – Ein Element a[j] mit kleinerem Schlüssel als x wird gefunden. – Das linke Ende der Zielsequenz ist erreicht. Für diese Wiederholung mit zwei Abbruchbedingungen wird eine Marke a[0]=x verwendet. Konsequenz: Erweiterung des Indexbereiches auf 0..n. Wolfgang E. Nagel Zeitkomplexität Die Zahl der Vergleiche von Schlüsseln beim i-ten Durchlauf ist höchstens i und mindestens 1 und somit, unter der Annahme, dass alle n! Permutationen gleich wahrscheinlich sind, im Mittel (i+1)/2. n 2 " % i +1 1 n(n + 2) n(n + 2) ! 2 n + 2n ! 2 av 2 T ins (n) = = !1 = = ) O(n ) $ ' ( _ sort & 2# 2 4 4 i=2 2 Der beste Fall ist, wenn die Elemente von Anfang an geordnet sind. Der schlimmste Fall tritt ein, wenn die Elemente zu Beginn in umgekehrter Reihenfolge angeordnet sind. Der Algorithmus lässt sich verbessern, indem man eine binäre Suche zum Auffinden der Einfügeposition verwendet (Binäres Einfügen). Wolfgang E. Nagel Sortieren durch Einfügen (Insertion Sort) Vorteile des Algorithmus: ! Einfach zu programmieren. ! Schnell für sehr kleine Werte von n. ! Vorsortierung wird ausgenutzt. Nachteile des Algorithmus: ! Hohe Zeitkomplexität von O(n2). Wolfgang E. Nagel Sortieren durch Vertauschen (Bubble Sort, Sinking Sort) Prinzip: ! Jeweils zwei aufeinanderfolgende a[i] und a[i+1] ( i = 1, .., (n-1)) werden miteinander verglichen und vertauscht, wenn a[i] > a[i+1] gilt. Auf diese Weise wandert im ersten Durchgang zumindest das größte Element an das Ende der Liste. ! Es werden weitere Durchgänge für i = 1, ..., k gemacht, wobei k die Position ist, von der ab im vorigen Durchgang keine Vertauschungen mehr aufgetreten sind. Wolfgang E. Nagel Bubble Sort void bubble_sort (void ) bound = n bound > 1 k= 1 j =1 a[j ].key > a[j +1].key T F help = a[j ] a[j ] = a[j +1] a[j +1] = help k= j j ++ while ( bound > j ) bound = k Wolfgang E. Nagel Zeitkomplexität Im ersten Durchgang werden (n-1) Elementpaare miteinander verglichen und ggf. miteinander vertauscht. Allgemein werden im i-ten Durchgang im ungünstigsten Fall (n-1) Elementpaare verglichen und vertauscht (i=1,...,n-1), d.h. der Aufwand beträgt: n!1 w T bubble _ sort (n) = (n !1) + (n ! 2) +…1 = " i = i=1 n(n !1) # O(n 2 ) 2 Im günstigsten Fall, d.h. wenn die Liste schon vorsortiert ist, sind (n-1) Vergleiche erforderlich. Der Aufwand beträgt dann: T bbubble _ sort (n) ! O(n) Wolfgang E. Nagel Sortieren durch Vertauschen (Bubble Sort, Sinking Sort) Vorteile des Algorithmus: ! Schnell für sehr kleines n. ! Vorsortierung wird ausgenutzt. Nachteile des Algorithmus: ! Viele Vertauschungen in der innersten Schleife. ! Hohe Komplexitätsordnung. Wolfgang E. Nagel Heapsort Prinzip: Der Algorithmus besteht aus 2 Schritten. 1. Die Elemente werden in einen Heap gebracht. 2. Die Elemente werden sortiert aus dem Heap entnommen. Beachte: Das kleinste Element ist das erste Element. Heap a[1] a[2] a[3] ... sortiert! a[k] a[k+1] Wolfgang E. Nagel a[n]! Grundidee Im ersten Schritt werden a[1] und a[n] ausgetauscht. Für a[1],...,a[n-1] wird der Heap wieder aufgebaut. Ergebnis des ersten Schrittes ist, dass a[1],...,a[n-1] einen Heap bilden und a[n] eine sortierte Liste. Im zweiten Schritt werden a[1] und a[n-1] vertauscht, und für a[1],...,a[n-2] wird der Heap wieder aufgebaut, d.h. a[1],...,a[n-2] erfüllen die Heapbedingung und a[n-1], a[n] bilden eine sortierte Liste. usw. Wolfgang E. Nagel Realisierung eines Heap typedef struct { keytype key; datatype data; } stdelement; /* Vektor mit n+1 Elementen a[0]...a[n], von denen a[0] nicht benutzt wird. */ extern stdelement a[]; Wolfgang E. Nagel heapcreate void heapcreate ( int k ) insert = k/2 + 1 insert > 1 siftdown (--insert , k) Wolfgang E. Nagel siftdown void siftdown(int pos, int r) i = pos j = 2*pos x = a [i ] finished = false (j <= r) && !finished j<r T F a[j ].key > a[j +1].key T F j ++ x.key <= a[j ].key TRUE FALSE a [i ] = a [j ] finished = true i=j j = 2*i a [i ] = x Wolfgang E. Nagel Heapsort void heapsort ( void ) heapcreate (n ) k= n k> 1 temp = a [1 ] a [ 1 ] = a [ k] a [ k] = temp siftdown (1 , --k) Wolfgang E. Nagel Zeitkomplexität Für das Bilden des Heap (heapcreate) wird für n/2 Elemente die Funktion siftdown ausgeführt. Maximal werden log(n+1) Elemente verglichen. Für Schritt 1 gilt: n w T heap (n) = log(n +1) _ create 2 Für das Sortieren der Elemente wird siftdown für n-1 Elemente ausgeführt, d.h. für Schritt 2 gilt: T (n) = (n !1)log(n +1) Für Heapsort insgesamt gilt dann: T w heap _ sort 3 (n) = n log(n +1) ! O(n log n) 2 Wolfgang E. Nagel Quicksort: divide-and-conquer-Prinzip (‘teile‘ und ‘herrsche‘) Vorgehensweise: 1. Die zu sortierende Liste wird in zwei Teile aufgeteilt (untere Teilliste, obere Teilliste). 2. Die Aufteilung geschieht so, dass in der unteren Teilliste nur Elemente ≤ einem Referenz-Element, in der oberen Teilliste nur Elemente ≥ diesem Referenz-Element vorkommen (ggf. müssen dabei Elemente vertauscht werden). 3. Die so entstandenen Teillisten werden nach dem gleichen Prinzip „sortiert" (rekursive Vorgehensweise). Das Verfahren endet, wenn die Teillisten weniger als zwei Elemente enthalten. ≤X untere Teilliste X Referenz-Element Wolfgang E. Nagel ≥X obere Teilliste Quicksort Teil 1 void quicksort ( int low , int high ) i = low j = high x = a [ (low + high )/2 ] a [i ].key < x.key i ++ a [j ].key > x.key j -- Wolfgang E. Nagel Quicksort Teil 2 i <= j T F help = a [i ] a [ i + +] = a [ j ] a [j --] = help while ( i <= j ) j > low T F quicksort (low , j ) i < high T F quicksort (i , high ) Wolfgang E. Nagel Korrektheit des Algorithmus Nach Ende der Partitionierung in zwei Teillisten (Ende der while-do-Schleife) gilt: – i zeigt auf ein Element >= x.key – j zeigt auf ein Element <= x.key – i > j – Alle Elemente links von j sind <= x.key, weil i > j – Alle Elemente rechts von i sind >= x.key, weil i > j Also ist (low,j) eine Teilliste mit Elementen <= x.key und (i,high) eine Teilliste mit Elementen >= x.key. Zwei Fälle sind möglich: – j+1=i: ok, da vollständige Zerlegung in zwei Teillisten. – j+2=i: In diesem Fall gilt unmittelbar vor Ende der while-do-Schleife i==j und a[i]==a[j]==x. Dann folgt i++ und j--, und damit endet die Schleife. Also: (low,j) x (i,high), d.h. x steht schon an der korrekten Position, (low,j) und (i,high) sind noch zu sortieren. Wolfgang E. Nagel Zeitkomplexität Aufwand im günstigsten Fall (Teillisten werden stets halbiert): Stufe 1 X 1 n In jeder Teilliste der Stufe 1 werden n/2 Elemente betrachtet und ggf. vertauscht. ⇒ Insgesamt in Stufe 1: n/2 + n/2 = n Stufe 2 X 1 X n/2 n In jeder Teilliste werden n/4 Elemente betrachtet, also insgesamt 4*(n/4)=n. Allg. gilt: In jeder Stufe werden n Elemente betrachtet. Abbruch bei Teillisten der Länge n<2 ⇒ 'log n( Stufen. Also: T bquicksort (n) ! O(n log n) Wolfgang E. Nagel Zeitkomplexität Aufwand im worst-case: ! In jeder Stufe wird als Referenz-Element X das größte oder kleinste Element der Teilliste ausgewählt. ! Dann gilt: Die Länge der längsten Teilliste ist (n-1) bei Stufe 1, (n-2) bei Stufe 2, etc. Allg.: (n-i) bei Stufe i. ! In diesem Fall existieren (n-1) Stufen. In jeder Stufe werden n Elemente betrachtet. ! Also: w T quicksort (n) ! O(n 2 ) Aufwand im Mittel: ! Genaue Analyse ist sehr aufwendig. ! Resultat: av T quicksort (n) ! O(n log n) Wolfgang E. Nagel Verbesserungsmöglichkeiten: 1. End-Rekursion vermeiden. Die End-Abfrage erfolgt nicht nach dem letzten (rekursiven) Aufruf, sondern vor dem Aufruf. 2. Kleine Teillisten (mit n < 20) sollten mit Insertion-Sort oder Bubble_Sort sortiert werden. Vorteil: Dies verhindert die sehr häufigen Rekursionen am Ende. 3. Medium-of-three-Methode zum Auswählen des Referenz-Elementes: Prinzip: Es werden drei Elemente als Referenz-Elemente, z.B. vom Listenanfang, vom Listenende und aus der Mitte gewählt. Das Element mit dem mittleren Schlüssel wird als Referenz-Element gewählt. Wolfgang E. Nagel Quicksort Teil 1 void quicksort( int low , int high ) Stack erzeugen stack_ empty = false low < high TRUE FALSE i = low j = high Stack leer x = a [ (low +high )/2 ] a [i ].key < x.key i ++ a [j ].key > x.key T Wolfgang E. Nagel F Quicksort Teil 2 j -i <= j T F help = a [i ] a [ i + +] = a [ j ] pop (& low , &high a [j --] = help stack_ empty = true while ( i <= j ) (j -low ) > (high -i ) T ) F push (low , j ) push (i , high ) low = i high = j while ( !stack_ empty ) Stack löschen Wolfgang E. Nagel Quicksort Vorteile des Algorithmus: ! Niedrige Komplexitätsordnung im average-case. ! Kleine Konstanten in der Zeitfunktion. Nachteile des Algorithmus: ! Zusätzlicher Zeit- und Platzbedarf für den Stack. ! Vorsortierung wird nicht ausgenutzt. ! Schlechte Zeitkomplexität im worst-case-Fall. Wolfgang E. Nagel Mergesort (Sortieren durch Mischen) ! Bisherige Sortierverfahren hatten als Voraussetzung, dass alle Daten in einem Speicher mit direktem Zugriff (d.h. Zugriff über a[i]) vorhanden waren. Diese Verfahren sind deshalb nicht anwendbar bei sehr großen Datenbeständen z.B. auf Hintergrundspeichern mit sequentiellem Zugriff (wie Bändern). ! Sortieren durch Mischen basiert auf der Grundidee, dass man zwei Sequenzen von sortierten Daten zu einer Sequenz zusammenführt. ! Hier für den Fall n=2k Wolfgang E. Nagel Mergesort (Sortieren durch Mischen) Grundidee: 1. Zerlege die Sequenz c in zwei Hälften a und b. 2. Mische a und b durch Kombination einzelner Elemente zu geordneten Paaren. 3. Bezeichne die gemischte Sequenz mit c, und wiederhole Schritt 1. und 2., wobei dieses Mal die geordneten Paare zu geordneten Quadrupeln zusammenzufassen sind. 4. Wiederhole die voranstehenden Schritte durch Mischen der Quadrupel zu Octrupel, und fahre damit so lange fort, indem jedes mal die Längen der gemischten Sequenzen verdoppelt werden, bis die ganze Sequenz geordnet ist. Wolfgang E. Nagel A a1 a2 . . . an C a1 a2 b1 b2 1 2 3 4 B b1 b2 . . . bn Wolfgang E. Nagel . . . mj Mergesort mit Feldern void mergesort _ array( void ) n1 = n /2 k = (int ) log2 (n ) for (i = 1 ; i <= k; i ++) distribute _ array(n1 ) length = ipow2 (i -1 ) /* Länge der Teillisten */ mmax = n /ipow2 (i ) /* Anzahl der Mischvorgänge */ for (m = 1 ; m <= mmax ; m ++) anfx = (m -1 )*length + 1 /* Anfangsindex */ endx = m *length /* Endindex */ anfxc = (m -1 )*ipow2 (i ) + 1 /* Anfangsindex für merge _ array(anfx , endx , anfx , endx , anfxc ) Wolfgang E. Nagel Array c */ Mergesort Teil 1 void mergesort _ file ( void ) n1 = n /2 k = (int ) log2 (n ) for (i = 1 ; i <= k; i ++ ) distribute _ file (n1 ) assert( (c = fopen (DATEI _ C, " w" )) != NULL ) assert( (a = fopen (DATEI _ A , " r" )) != NULL ) assert( (b = fopen (DATEI _ B , " r" )) != NULL ) Wolfgang E. Nagel Mergesort Teil 2 a _ element = getstdelem (a ) b _ element = getstdelem (b ) length = ipow2 (i -1 ) /* Länge der Teillisten */ mmax = n /ipow2 (i ) /* Anzahl der Mischvorgänge */ for (m = 1 ; m <= mmax ; m ++ ) anfx = (m -1 )*length + 1 /* Anfangsindex */ endx = m *length /* Endindex */ anfxc = (m -1 )*ipow2 (i ) + 1 /* Anfangsindex für Array c */ merge _ file (anfx , endx , anfx , endx , & a _ element , & b _ element ) fclose (a ), fclose (b ), fclose (c) Wolfgang E. Nagel Verteilen des Feldes C auf A und B static void distribute _ array( int n1 ) i=1 for (j = 1 ; j <= n1 ; j ++) a[j ] = c[i ++] for (j = 1 ; j <= n1 ; j ++) b[j ] = c[i ++] Wolfgang E. Nagel Verteilen der Datei c auf a und b static void distribute _ file ( int n1 ) assert( (c = fopen (DATEI _ C, " r" )) != NULL ) assert( (a = fopen (DATEI _ A , " w" )) != NULL ) assert( (b = fopen (DATEI _ B , " w" )) != NULL ) c_ element = getstdelem (c) for (j = 1 ; j <= n1 ; j ++ ) copy (a , c, & c_ element ) for (j = 1 ; j <= n1 ; j ++ ) copy (b , c, & c_ element ) fclose (a ), fclose (b ), fclose (c) Wolfgang E. Nagel Kopieren eines Elementes von f1 nach f2 static void copy( FILE *out , FILE *in , stdelement *elem _ptr ) putstdelem (*elem _ ptr, out ) *elem _ptr = getstdelem (in ) Wolfgang E. Nagel Mischen von zwei sortierten Feldern void merge _ array ( int i , int m1 , int j , int m2 , int k ) (i < = m1 ) & & (j <= m2 ) a [i ].key <= b [ j ].key TRUE c[k++ ] = a [i + +] FALSE c[k++ ] = b [j + +] i < = m1 c[k++ ] = a [i + +] j < = m2 c[k++ ] = b [j + +] Wolfgang E. Nagel Mischen von zwei sortierten Dateien void merge _ file _ to _ eof ( void ) assert( (f = fopen (DATEI _ A , " r" )) ! = NULL ) assert( (g = fopen (DATEI _ B , " r" )) ! = NULL ) assert( (h = fopen (DATEI _ C, " w" )) ! = NULL ) f_ element = getstdelem (f ) g _ element = getstdelem (g ) endfg = feof (f) | | feof (g ) !endfg f_ element . key <= g _ element . key TRUE FALSE copy (h , f, & f _ element ) copy (h , g , & g _ element ) endfg = feof (f ) endfg = feof (g ) !feof (g ) copy (h , g , & g _ element ) !feof (f) copy (h , f, & f _ element ) Wolfgang E. Nagel Mischen einer Anzahl von Elementen void merge _ file ( int i , int m1 , int j , int m2 , stdelement *a _ ptr, stdelement *b _ ptr ) (i <= m1 ) & & (j <= m2 ) a _ ptr->key <= b _ ptr->key TRUE FALSE copy (c, a , a _ ptr) copy (c, b , b _ ptr) i ++ j ++ i <= m1 copy (c, a , a _ ptr) i ++ j <= m2 copy (c, b , b _ ptr) j ++ Wolfgang E. Nagel Zeitkomplexität ! Die Dateien A, B und C werden k = (log n)-mal verteilt. Das Verteilen und Mischen fordert O(n) Operationen. Daraus folgt: w T av (n) = T mergesort mergesort (n) ! O(n log n) Wolfgang E. Nagel Vorteile des Algorithmus: ! Niedrige Komplexitätsordnung (auch im worst-case). ! Der Mergesort-Algorithmus sortiert (auch) extern. Nachteile des Algorithmus: ! Wird das Verfahren als internes Sortierverfahren verwendet, so ist je nach Implementierung zusätzlicher Speicherplatz erforderlich. ! Programmierung für beliebiges n ist aufwendig. ! Die Konstanten bezüglich der O-Notation sind u.U. sehr groß. Wolfgang E. Nagel Bitonic Sort Definition: Eine Folge a1,a2,..,an heisst bitonisch, wenn der erste Teil der Folge aufsteigend und der zweite absteigend sortiert ist, oder wenn man die Folgenglieder so verschieben kann, dass diese Bedingung gilt. ♦ Beispiele bitonischer Folgen: 11, 20, 43, 72, 85, 43, 29, 27, 16 81, 96, 98, 15, 14, 11, 10, 44, 56 Wolfgang E. Nagel Bitonic Sort – Idee des Algorithmus ! Fundamentale Ideen: Rekursion und Divide-and-Conquer ! Ist a1,..,an bereits bitonisch, so werden die beiden Teilfolgen zu einer sortierten Folge gemischt. ! Andernfalls geht man rekursiv vor und versucht den gewünschten Zustand herzustellen, indem die linke Hälfte aufsteigend und die rechte Hälfte absteigend sortiert wird. Wolfgang E. Nagel Bitonic Sort – Idee des Algorithmus Mischen einer bitonischen Folge zu einer sortierten Folge: ! Gegeben: a1,a2,..,a2n ! 1. Schritt: Jedes Element aj,1≤j≤n, der ersten Hälfte wird mit korrespondierendem Element aj+n der zweiten Hälfte verglichen; beide werden vertauscht, wenn sie in der falschen Reihenfolge stehen, falls also aj>aj+1 ist. Alle Elemente der ersten Hälfte der Folge sind nun kleiner als die Elemente der zweiten Hälfte, und beide Hälften sind bitonisch. ! 2. Schritt: Anwendung des Verfahrens rekursiv getrennt auf die beiden Hälften der Folge Offenbar ist die Folge sortiert, wenn die Rekursion abbricht. Wolfgang E. Nagel Bitonic Sort – Parallele Realisierung I ! Netzwerk von Prozessoren mit log n Spalten ! Funktionsverhalten der Prozessoren: X Y ≤ X‘ Y‘ X‘:=min(X,Y) Y‘:=max(X,Y), X,Y,X‘,Y‘ Z Wolfgang E. Nagel Bitonic Sort – Zeitkomplexität ! Aufwand sequentiell (Bitonische Folge ! Sortierte Folge): av w T bitonic (n) = T bitonic (n) ! O(n log n) Wolfgang E. Nagel Bitonic Sort – Parallele Realisierung I ! Netzwerk von Prozessoren mit log n Spalten ! Funktionsverhalten der Prozessoren: X Y ≤ X‘ X Y‘ Y X‘:=min(X,Y) Y‘:=max(X,Y), X,Y,X‘,Y‘ ≥ X‘ Y‘ X‘:=max(X,Y) Z Y‘:=min(X,Y), X,Y,X‘,Y‘ Wolfgang E. Nagel Z Bitonic Sort – Zeitkomplexität ! Aufwand sequentiell (Beliebige Folge ! Sortierte Folge): av w T bitonic (n) = T bitonic (n) ! O(n log n) Wolfgang E. Nagel Zeitkomplexitäten der verschiedenen Sortierverfahren Sortierverfahren worst-case average-case Selection Sort N2 N2 Insertion Sort N2 N2 Bubble Sort N2 N2 Quicksort N2 N log2 N Heapsort N log2 N N log2 N Mergesort N log2 N N log2 N Bitonic Sort N log22 N N log22 N Algorithmus von Cole N log2 N N log2 N Wolfgang E. Nagel Center for Information Services and High Performance Computing (ZIH) Effiziente parallele Algorithmen Paralleler Mergesort Algorithmus Zellescher Weg 12 Willers-Bau A205 Tel. +49 351 - 463 35450 Nöthnitzer Str. 46 Raum 1044 Tel. +49 351 - 463 38246 Wolfgang E. Nagel([email protected]) Agenda 1 Einführung 2 Prinzip 3 Ausführliche Beschreibung Wolfgang E. Nagel Agenda 1 Einführung 2 Prinzip 3 Ausführliche Beschreibung Wolfgang E. Nagel Paralleles Sortieren Bitonisches Sortieren: O(log2 n) mit n Komparatoren Erster paralleler Sortieralgorithmus in O(log n) mit O(n) Prozessoren: AKS-Sortiernetzwerk (Ajtai,Komlós, Szemerédi 1983) Proportionalitätskonstante so hoch, dass nur schneller für n > 10100 1986 präsentierte Cole parallelen Sortieralgorithmus in O(log n)-Klasse und mit kleinerer Konstante Wolfgang E. Nagel Agenda 1 Einführung 2 Prinzip 3 Ausführliche Beschreibung Wolfgang E. Nagel Prinzip Vollständiger Binärbaum, n = 2k zu sortierende Elemente befinden sich an den Blättern An jedem Knoten werden die Elemente des linken und rechten Teilbaums gemischt Mischen beginnt an Blättern und setzt sich bis zur Wurzel fort Bevor das Mischen in einem Knoten abgeschlossen ist, werden Samples der Elemente, die bisher empfangen wurden, den Baum hoch geschickt Dies ermöglicht das gleichzeitige Mischen an verschiedenen Stufen des Baumes Wolfgang E. Nagel Beobachtungen von Cole Cole’s log n merging procedure: “The Problem is to merge two sorted arrays of n items. We proceed in log n stages. In the ith stage, for each array, we take a sorted sample of 2i 1 items, comprising every n/2i 1 th items in the array. We compute the merge of these samples.” Wolfgang E. Nagel Beobachtungen von Cole Beobachtungen von Cole: Merging in constant time: Given the results of the merge from the (i ith stage can be done in O(1). 1)st stage, the merge in the Level by level approach ! O(log2 n) Die zweite Beobachtung von Cole erklärt, wie eine langsamere Merging-Prozedur zu einem schnelleren Sortieralgorithmus führen kann: The merges at the di↵erent levels can be pipelined: This is possible since merged samples made at level l of the tree may be used to provide samples of the appropriate size for merging at the next level above l without losing the O(1) time merging property. Wolfgang E. Nagel Beobachtungen von Cole x u v level l 1 level l w Level by level: Sobald Knoten v (und w ) eine sortierte Sequenz hat, die alle Elemente des Unterbaums von v (w ) enthält, beginnt das Mergen am Level l. Der Merge-Knoten u braucht log k Stufen, wobei k die Anzahl der Elemente der sortierten Sequenzen in Knoten v und w bezeichnet. Jede dieser Stufen kann in konstanter Zeit ausgeführt werden, aufgrund des systematischen Samplings der sortierten Folgen in v und w . Ist Knoten u fertig mit dem Mergen, beginnt Knoten x damit. Wolfgang E. Nagel Beobachtungen von Cole x u v level l 1 level l w Pipelined: Hier beginnen die Knoten früher mit dem Mergen. Sobald Knoten u vier Elemente enthält, sendet er Samples zu seinem Vorfahr-Knoten x. Knoten x tut das gleiche, sobald er vier Elemente enthält, beginnt er mit der Versendung der Samples an seinen Vorfahr. Cole hat gezeigt, dass diese pipelineartige Vorgehensweise beim Mischen so implementiert werden kann, dass jede Merge-Stufe in O(1) ausgeführt werden kann. Wolfgang E. Nagel Agenda 1 Einführung 2 Prinzip 3 Ausführliche Beschreibung Wolfgang E. Nagel Aufgabe jedes Knotens u Jeder Knoten u produziert einen sortierte Liste L(u) und speichert ein Feld Up(u), welches eine sortierte Untermenge von L(u) ist Zu Beginn gilt für alle Blätter y : Up(y ) = L(y ) Am Ende des Algorithmus gilt für die Wurzel t des Baumes: Up(t) = L(t) Während der Ausführung des Algorithmus wird Up(u) üblicherweise aus einem Sample der Elemente aus L(u) bestehen Definition Ein Knoten u wird fertig genannt, wenn |Up(u)| = |L(u)|, andernfalls ist u ein aktiver Knoten. Zu Beginn des Algorithmus sind nur die Blätter fertig Wolfgang E. Nagel Aufgabe jedes Knotens u Eine Stufe im parallelen Mergesort-Algorithmus von Cole: Phase 1: Generierung der Samples Phase 2: Mergen in O(1) Schritt 1: Mergen Berechnung der Kreuzränge Teilschritt 1 Teilschritt 2 Generierung von NewUp(u) Schritt 2: Bereitstellung der Ränge fromsendernode fromothernode Wolfgang E. Nagel Aufgabe jedes Knotens u In jeder Stufe des Algorithmus wird ein neues Feld NewUp(u) in jedem aktiven Knoten auf folgende Weise in zwei Phasen erzeugt: (P1) Samples von den Feldern Up(v ) und Up(w ) werden generiert und in SampleUp(v ) bzw. SampleUp(w ) gespeichert. (P2) NewUp(u) entsteht aus Mischen von SampleUp(v ) und SampleUp(w ) mit Hilfe von Up(u). Für jeden Knoten gilt, dass das NewUp(u)-Feld generiert in Stufe i das Up(u)-Feld zu Beginn von Stufe i + 1 ist. An fertigen Knoten wird Phase 2 nicht ausgeführt, da diese bereits die sortierte Liste L(u) erzeugt haben. Wolfgang E. Nagel Aufgabe jedes Knotens u Die zwei Phasen der Berechnung von NewUp(u): NewUp(u) MergeWithHelp Up(u) SampleUp(v ) SampleUp(w ) MakeSamples Up(v ) Up(w ) Wolfgang E. Nagel Phase 1: Generierung der Samples Berechnung der Samples SampleUp(u) wird parallel von allen Knoten auf folgende Weise durchgeführt: 1 2 Wenn u ein aktiver Knoten ist, dann ist SampleUp(u) das sortierte Feld, welches aus jedem 4. Element von Up(u) besteht (beginnend beim 1. Element). Ist |Up(u)| < 4, ist SampleUp(u) leer. Wenn u das 1. Mal fertig ist, dann wird SampleUp(u) wie für aktive Knoten gebildet. Ist u das 2. Mal fertig, besteht SampleUp(u) aus jedem 2. Element von Up(u). Und ist u das 3. Mal fertig, gilt SampleUp(u) = Up(u). SampleUp(u) ist immer sortiert! Wolfgang E. Nagel Phase 1: Generierung der Samples Der Algorithmus hat 3 log n Stufen Zu Beginn sind nur die Blätter fertig In der 3. Stufe als fertiger Knoten hat ein Knoten v alle Elemente aus L(v ) benutzt, um SampleUp(v ) in Phase 1 zu generieren Der Vater-Knoten u hat SampleUp(v ) und SampleUp(w ) zu Up(u) in Phase 2 gemischt Knoten u hat alle Elemente in L(u) empfangen und die Kind-Knoten v und w sind fertig In der nächsten Stufe ist u das erste mal fertig Das Level mit fertigen Knoten bewegt sich jede 3. Stufe ein Level nach oben Nach 3 log n Stufen wird die Wurzel t fertig und der Algorithmus terminiert Wolfgang E. Nagel Phase 2: Mergen in O(1) Der komplizierteste Teil im Algorithmus von Cole ist das Mergen von SampleUp(v ) und SampleUp(w ) zu NewUp(u) in O(1). Das Mergen basiert auf der Bereitstellung von Rängen. Definition Der Rang von e in einer sortierten Folge S ist rng (e, S) := |{x 2 S|x e}|. Als Rang zwischen zwei sortierten Folgen A und B wird die Funktion RngA,B : A ! N bezeichnet mit RngA,B (e) = rng (e, B) 8e 2 A. Beispiel: A = (2, 4, 6, 8) B = (1, 3, 5, 7) rng (1, A) = 0 rng (5, A) = 2 RngA,B = (1, 2, 3, 4) Wolfgang E. Nagel Phase 2: Mergen in O(1) Der Rang gibt an, wo ein Element e in eine sortierte Folge S eingefügt werden muss, wenn die Sortierung erhalten bleiben soll. Dabei ist es wichtig zu wissen, ob e in S ist, oder nicht. Beispiel: S = (1, 2, 5, 8, 12) e = 2 ) Rng (e, S) = 2 e = 9 ) Rng (e, S) = 4 insert an Position 2 oder 3 = Rng (e, S) + 1 insert an Position 5 = Rng (e, S) + 1 Rng (e, S) + 1 ist die korrekte Position, wo e in S eingefügt werden müsste. Wolfgang E. Nagel Phase 2: Mergen in O(1) Annahme A1 Zu Beginn jeder Stufe sind für einen aktiven Knoten u mit Kind-Knoten v und w die Ränge Rng (Up(u), SampleUp(v )) und Rng (Up(u), SampleUp(w )) bekannt. Die Generierung von NewUp(u) kann in zwei Schritte geteilt werden. Zum einen in das Mergen der Samples der Kind-Knoten und zum anderen Sicherstellung von obiger Annahme A1 . Wolfgang E. Nagel Phase 2, Schritt 1: Mergen NewUp(u) soll aus Mergen von SampleUp(v ) und SampleUp(w ) Gesucht: Die Position von jedem Element aus SampleUp(v ) und SampleUp(w ) in NewUp(u) Mergen dann einfach durch Einfügen jedes Elementes an die richtige Position Betrachtung eines beliebigen Elementes e in SampleUp(v ). Gesucht ist die Position von e in NewUp(u), also rng (e, NewUp(u)), der Rang von e in NewUp(u) NewUp(u) wurde aus Mergen von SampleUp(v ) und SampleUp(w ) generiert, es gilt also: rng (e, NewUp(u)) = rng (e, SampleUp(v )) + rng (e, SampleUp(w )) Wolfgang E. Nagel Phase 2, Schritt 1: Mergen e r1 + r2 d NewUp(u) f Up(u) r r2 = 2 t e r1 2 SampleUp(v ) SampleUp(w ) Die Benutzung der Ränge für die Merging-Prozedur in O(1). Kennt man die Position von e in SampleUp(v ), rng (e, SampleUp(v )) = r1 , und seine Position in SampleUp(w ), rng (e, SampleUp(w ) = r2 , dann ist die Position von e in NewUp(u), rng (e, NewUp(u)), einfach r1 + r2 . Wolfgang E. Nagel Berechnung der Kreuzränge Betrachtung der Situation, wenn e aus SampleUp(v ) ist Zunächst (parallele) Berechnung des Ranges in Up(u) für jedes Element e 2 Sample(Up(v )) (Beschreibung auf nächster Folie) Der Rang von e in Up(u) liefert die Elemente d und f in Up(u), welche e in Up(u) einschließen, also d e f Annahme A1 liefert die Ränge r := rng (d, SampleUp(w )) und t := rng (f , SampleUp(w )), welche die richtige Position beschreiben, wenn d und f in SampleUp(w ) eingefügt werden sollen Die richtige Position von e in SampleUp(w ) hängt von r und t ab (da e 2 [d, f ]) Es kann gezeigt werden, dass r und t immer maximal einen Bereich von 4 Positionen beschreibt, und die richtige Position, rng (e, SampleUp(w )), kann in konstanter Zeit bestimmt werden (ausführlicher siehe Teilschritt 2) Wolfgang E. Nagel Teilschritt 1: Berechnung von Rng (SampleUp(v ), Up(u)) Die Berechnung von Rng (SampleUp(v ), Up(u)) erfolgt mittels der Berechnung von Rng (Up(u), SampleUp(v )). 1 2 ... b c i1 i2 r r +1 ... Up(u) 1 2 ... s ... 1 s ... SampleUp(v ) Betrachtung eines beliebigen Elementes i1 2 Up(u) Der Bereich [i1 , i2 i ist das Intervall beginnend bei i1 und endend bei i2 , wobei i2 nicht mehr enthalten ist Gesucht: Anzahl der Elemente in SampleUp(v ), welche sich in I (i1 ) := [i1 , i2 i befinden Wolfgang E. Nagel Teilschritt 1: Berechnung von Rng (SampleUp(v ), Up(u)) 1 2 ... b c i1 i2 r r +1 ... Up(u) 1 2 ... s ... 1 s ... SampleUp(v ) Die Elemente in I (i1 ) haben den Rang b in Up(u), wobei b die Position von i1 in Up(u) ist (Zähler für Feldpositionen beginnt bei 1) Ist I (i1 ) gefunden, kann ein Prozessor assoziiert mit dem Element i1 , den Rang b der Elemente in der Menge bestimmen Gleichzeitig kann ein Prozessor assoziiert mit i2 den Rang c von I (i2 ) ermitteln. Wolfgang E. Nagel Teilschritt 1: Berechnung von Rng (SampleUp(v ), Up(u)) Berechnung von I (i1 ) Gesucht sind alle Elemente ↵ 2 SampleUp(v ) mit rng (↵, Up(u)) = b Diese Elemente müssen die Bedingung (↵ i1 ) ^ (↵ < i2 ) erfüllen. Sei item(j) des Element, welches an Position j in SampleUp(v ) steht Annahme A1 liefert rng (i1 , SampleUp(v )), also r , und rng (i2 , SampleUp(v )), also s, was zu (item(r ) i1 ) ^ (item(s) i2 ) führt Demzufolge: Wenn Elemente in SampleUp(v ) zwischen Position r und s (r < < s) exisitieren, muss rng ( , Up(u)) = b gelten Wolfgang E. Nagel Teilschritt 1: Berechnung von Rng (SampleUp(v ), Up(u)) Die Elemente r und s benötigen eine gesonderte Behandlung: item(r ) = i1 ) rng (item(r ), Up(u)) = b item(r ) < i1 ) rng (item(r ), Up(u)) < b item(s) = i2 ) rng (item(s), Up(u)) = c item(s) < i2 ) rng (item(s), Up(u)) < c ) rng (item(s), Up(u)) = b Zusammenfassend: I (i1 ) wird durch Vergleichen von item(r ) mit i1 und item(s) mit i2 gefunden. Ein mit i1 assoziierter Prozessor kann diese einfachen Berechnungen durchführen und den Rang b von allen Elementen in I (i1 ) in konstanter Zeit bestimmen. Das ist möglich, da gezeigt werden kann, dass I (y ) für eine beliebiges y 2 Up(u) maximal drei Elemente enthält. Wolfgang E. Nagel Teilschritt 2 Berechnung von Rng (SampleUp(v ), SampleUp(w )) mittels Rng (SampleUp(v ), Up(u)): ... d f ... Up(u) rng (e, Up(u)) r e t t+1 ... SampleUp(w ) ... ] < e [ > e rng (e, Up(u)), berechnet in Teilschritt 1, liefert die umschließenden Elemente d und f in Up(u) Annahme A1 liefert die Ränge r und t Gesucht: Exakte Position von e beim Einfügen in SampleUp(w ) Frage: Welche Elemente aus SampleUp(w ) müssen mit e verglichen werden. Wolfgang E. Nagel Teilschritt 2 Das liefern die folgenden Beobachtungen: Alle Elemente in SampleUp(w ) links von Position r (r eingeschlossen) sind kleiner als e. Alle Elemente in SampleUp(w ) rechts von Position t sind größer als e. ) Man kann zeigen, dass e mit maximal drei Elementen aus SampleUp(w ) verglichen werden muss. Ist Teilschritt 2 für jedes Element e aus SampleUp(v ) und SampleUp(w ) erledigt (parallel), kennt jedes Element seine Position in NewUp(u) und kann an die richtige Position geschrieben werden. Wolfgang E. Nagel Phase 2, Schritt 2: Bereitstellung der Ränge Annahme A1 besagt, dass die Ränge Rng (Up(u), SampleUp(v )) und Rng (Up(u), SampleUp(w )) zu Beginn jeder Stufe bekannt sind Gültigkeit der Annahme durch Berechnung von Rng (NewUp(u), NewSampleUp(v )) und Rng (NewUp(u), NewSampleUp(w )) am Ende jeder Stufe Berechnung von Rng (NewUp(u), NewSampleUp(v )) (Rng (NewUp(u), NewSampleUp(w )) analog) Betrachtung eines Elementes e in NewUp(u) Berechnung von rng (e, NewSampleUp(v )) kann in zwei Fälle unterteilt werden: e aus SampleUp(v ) (fromsendernode) e nicht aus SampleUp(v ) (fromothernode) Wolfgang E. Nagel Berechnung des Rangs in NewSampleUp (fromsendernode) Bekannt: Ränge Rng (Up(u), SampleUp(v )) und Rng (Up(u), SampleUp(w )) NewUp(u) beinhaltet alle Elemente aus SampleUp(v ) und SampleUp(w ) Folglich kann der Rang rng (↵, NewUp(u)) für alle Elemente ↵ aus NewUp(u) einfach aus der Summe der Ränge rng (↵, SampleUp(v )) und rng (↵, SampleUp(w )) berechnet werden Dies erfolgt parallel für jeden Knoten. Wolfgang E. Nagel Berechnung des Rangs in NewSampleUp (fromsendernode) SampleUp(v ) entsteht aus jedem vierten Element von Up(v ) Finden der Position eines Elementes in Up(v ) einfach, wenn die Position in SampleUp(v ) bekannt Hat man Rng (Up(v ), NewUp(v )) berechnet, kann man Rng (SampleUp(v ), NewUp(v )) einfach berechnen mittels durchgehen durch Up(v ) Für jedes Element in SampleUp(v ) hat man rng ( , NewUp(v )). Gleichermaßen, wie für SampleUp(v ), entsteht NewSampleUp(v ) aus jedem vierten Element von NewUp(v ) Informell ist daher rng ( , NewSampleUp(v )) rng ( , NewUp(v )) geteilt durch vier. Wolfgang E. Nagel Berechnung des Rangs in NewSampleUp (fromsendernode) An diesem Punkt kennt man Rng (SampleUp(v ), NewSampleUp(v )) und das Element e aus NewUp(u) kommt aus SampleUp(v ). Um rng (e, NewSampleUp(v )) zu finden, verbleibt das Element e in SampleUp(v ) zu lokalisieren. Dies kann einfach realisiert werden, wenn man für jedes Element in NewUp(u) die Sender-Adresse (Position) von e in SampleUp(v ) speichert. Wolfgang E. Nagel Berechnung des Rangs in NewSampleUp (fromothernode) Abbildung stellt Fall dar, wenn e aus SampleUp(w ) ist. Gesucht ist die korrekte Position von e, wenn es in NewSampleUp(v ) eingefügt werden soll. ... d f e ... NewUp(u) r von SampleUp(w ) von SampleUp(v ) t ... NewSampleUp(v ) ... ] <e [ >e Wolfgang E. Nagel Berechnung des Rangs in NewSampleUp (fromothernode) Die folgende Annahme wird helfen: Annahme A2 In jeder Stufe, zu Beginn von Schritt 2 (Bereitstellung der Ränge), sind für jedes Element e in NewUp(u), welches aus SampleUp(w ) stammt, die umschließenden Elemente d und f aus SampleUp(w ) bekannt. (und natürlich andersrum für Elemente aus SampleUp(v)). Das Element d ist das erste Element in NewUp(u) “vom anderen Knoten” (SampleUp(v )) links von e Identisch ist f das erste Element auf der rechten Seite Die Ränge r und t von d und f in NewSampleUp(v ) sind bekannt (im Fall fromsendernode berechnet) Wolfgang E. Nagel Berechnung des Rangs in NewSampleUp (fromothernode) Gleiche Situation, wie Teilschritt 2 Der Rang rng (e, NewSampleUp(v )) wird durch Vergleichen von e mit maximal drei Elementen aus NewSampleUp(v ) ermittelt. (d und f haben verschiedene Bedeutungen in Abbildung und Annahme A2 ) Bekannt: e nicht in SampleUp(w ) Der Rang rng (e, SampleUp(w )) liefert exakt die zwei Elemente von der anderen Menge, die e einschließen Für ein Element e aus SampleUp(w ) liefert die komplett symmetrische Berechnung rng (e, SampleUp(v )), und daher implizit die zwei Elemente d und f von SampleUp(v ), welche e einschließen. Dies sind die gesuchten Elemente aus der Abbildung. Daher ist die Gültigkeit von Annahme A2 gesichert, wenn am Ende von Schritt 1, der Generierung von NewUp(u), die Positionen (Ränge) von d und f in NewUp(u) gespeichert werden. Wolfgang E. Nagel Zusammenfassung Algorithmus von Cole: O(log n) mit O(n) Prozessoren Bitonisches Sortieren: O(log2 n) mit n Prozessoren Algorithmus von Cole nur schneller für n 268 ⇡ 3 · 1020 Folglich wird in der Praxis das einfachere und schnellere bitonische Sortieren Einsatz finden. ABER: Sortieren in O(log n) mit O(n) Prozessoren ist möglich! Wolfgang E. Nagel Fakultät Informatik, Institut für Technische Informatik, Professur Rechnerarchitektur Effiziente parallele Algorithmen Graphenalgorithmen Zellescher Weg 12 Nöthnitzer Straße 46 Willers-Bau A 205 Raum 1044 Tel. +49 351 - 463 - 35450 Tel. +49 351 - 463 - 38246 Wolfgang E. Nagel ([email protected]) Graphenalgorithmen ! Besuchen aller Knoten ! Kürzeste Wege zwischen zwei Knoten ! Minimale Spannbäume Wolfgang E. Nagel Besuchen aller Knoten ! Wichtiges Anwendungsskelett für viele Graphalgorithmen: Wende einer Funktion auf jeden Knoten des Graphen an ! Tiefensuche und Breitensuche hier für gerichtete und ungerichtete Graphen A B C E D Wolfgang E. Nagel Depth-First-Search (DFS) 1. Markiere alle Knoten als nicht besucht. 2. Wähle beliebigen Knoten v als Startknoten. 3. Dieser Knoten wird als besucht markiert. 4. Jeder Knoten, der adjazent zu v und noch nicht als besucht markiert ist, wird als nächster Knoten durch rekursiven Aufruf von ‘Depth-first-search‘ besucht. 5. Wenn alle Knoten, die von v erreichbar sind, besucht worden sind, ist die Suche für v beendet. 6. Wenn noch Knoten existieren, die als nicht besucht markiert sind, wird einer dieser Knoten ausgewählt und man fährt mit Schritt 3 des Algorithmus fort. Wolfgang E. Nagel Bemerkungen zu DFS ! Die Rekursion kann mit einem Stack simuliert werden. ! Preorder-Durchlaufstrategie ! DFS kann als Basis für viele andere Graphenalgorithmen dienen ! für gerichtete oder ungerichtete Graphen Wolfgang E. Nagel Breadth-First-Search (BFS) 1. Markiere alle Knoten als nicht besucht. 2. Wähle beliebigen Knoten v als Startknoten. 3. Dieser Knoten wird als besucht markiert. 4. Jeder Knoten, der adjazent zu v und noch nicht als besucht markiert ist, wird markiert und in einer Schlange gespeichert. 5. Entferne Knoten v aus der Schlange und fahre bei 4. fort, bis die Schlange leer ist. 6. Wenn noch Knoten existieren, die als nicht besucht markiert sind, wird einer dieser Knoten ausgewählt und man fährt mit Schritt 3 des Algorithmus fort. Wolfgang E. Nagel Datenstrukturen L[v] : Adjazenzliste für Knoten v mark[v] : kann ‚visited‘ oder ‚unvisited‘ sein; zu Beginn ‚unvisited‘ Initialisierung für den DFS- und BFS-Algorithmus: for(v = 1; v <= |V|; ++v) mark[v] = unvisited; for(v = 1; v <= |V|; ++v) if(mark[v] == unvisited) dfs(v); bzw. bfs(v); Wolfgang E. Nagel DFS void depth _first_search( vertex v ) mark[v] = visited for each vertex w in L[v] mark[w] == unvisited T F depth _first_search(w) Wolfgang E. Nagel BFS void breadth _first_search( vertex v ) mark[v] = visited put (v) !empty () v = get () for each vertex w in L [v] mark[w] == unvisited T F mark[w] = visited put (w) Wolfgang E. Nagel Beispiel 1 3 2 4 8 5 6 7 9 Besuchsreihenfolge DFS: 1 2 4 8 9 5 3 6 7 Besuchsreihenfolge BFS: 1 2 3 4 5 6 7 8 9 Wolfgang E. Nagel Zeitkomplexität ! Speicherung des Graphen mit Adjazenzlisten: – DFS, BFS: O( |V| + |E| ) ! Speicherung des Graphen mit Adjazenzmatrix: – DFS, BFS: O( |V|2 ) Wolfgang E. Nagel Kürzeste-Wege-Probleme ! Praktische Bedeutung für Transport- und Kommunikationsnetzwerke 1. Bestimmung des kürzesten Weges von einem festen Knoten (Quelle) zu allen anderen Knoten in einem Graphen (single-source shortest-path problem) 2. Bestimmung des kürzesten Weges zwischen allen Knotenpaaren in einem Graphen (all-pairs shortest-path problem) 3. Bestimme den kürzesten Weg zwischen zwei gegebenen Knoten. Wolfgang E. Nagel Anwendungsbeispiel Eine Spedition fährt jeden Tag im Dreieck Dresden-Leipzig-Chemnitz und möchte die Kosten minimieren. Die Kantenkosten im Graphen sind z.B. Kilometer oder benötigte Zeit. Leipzig 90 60 100 25 xxxxx Chemnitz 60 75 Dresden 1) Kostenminimaler Weg von Chemnitz zu allen anderen Orten? 2) Kostenminimaler Weg zwischen allen Orten? 3) Kostenminimaler Weg zwischen Chemnitz und Leipzig? Wolfgang E. Nagel Kürzeste Wege von einer Quelle Gegeben: Gerichteter oder ungerichteter Graph G = (V,E). Jeder Kante von vi nach vj ist eine positive Bewertung C(i,j) zugeordnet (Kantenkosten). Bemerkung: Ist die Bewertung negativ, so addiere den Betrag der betragsmäßig größten negativen Bewertung zu allen Bewertungen ≠ 0 und ∞ . Es ergibt sich ein Folgeproblem, bei dem alle Kantenbewertungen positiv sind. D.h. wir gehen ab jetzt von einer positiven Kantenbewertung aus. Keine Kante zwischen vi und vj : C(i,j) = ∞ , Diagonalelemente: C(i,j) = 0 Wolfgang E. Nagel Algorithmus von Dijkstra Eine Menge S enthält die Knoten, deren kürzeste Entfernungen von der vorgegebenen Quelle bereits bekannt sind. Vor.: C[u,v] ≥ 0 ∀ u, v ∈V . 1. Zu Beginn: S = {Quelle} 2. Der Algorithmus beginnt an der Quelle und berechnet die direkten Wege zu allen erreichbaren Nachbarknoten (analog BFS). Der Knoten mit der kürzesten Entfernung wird zu S hinzugenommen. 3. Die Berechnung wird an diesem Knoten fortgesetzt und die Wege von diesem Knoten zu allen Knoten, die nicht in S sind, berechnet. Dieser Schritt wird so lange wiederholt, bis alle Knoten in S enthalten sind. Wolfgang E. Nagel Bemerkungen ! Das Feldelement d[v] enthält die jeweils aktuelle kürzeste Entfernung des Knotens v von der Quelle. ! Wenn man zusätzlich zu der kürzesten Entfernung auch den kürzesten Weg zu jedem Knoten haben möchte (d.h. welche Zwischenknoten), kann man ein Feld p verwenden, so daß p[v] den direkten Vorgänger von v auf dem kürzesten Weg enthält. P[v] wird mit der Quelle initialisiert. Am Ende des Algorithmus kann der kürzeste Weg zu jedem Knoten mit Hilfe des Feldes p bestimmt werden. Wolfgang E. Nagel Algorithmus von Dijkstra void dijkstra1 ( void ) S [ 1 ] = true / * Knoten 1 ist in der Menge S enthalten */ for (i = 2 ; i < = N ; i + + ) S [ i ] = false / * keine weiteren Elemente in S * / d [i ] = c[1 ][i ] p [i ] = 1 for (i = 1 ; i < = N -1 ; i + + ) wähle einen Knoten w in V -S , so daß S [ w ] = true / * füge d [ w ] ein Minimum ist w zu S hinzu * / for (jeden Knoten v in V -S ) d [w]+ c[w][v] < d [v] T F d [v] = d [w]+ c[w][v] p [v] = w Wolfgang E. Nagel Beispiel zum Algorithmus von Dijkstra (z.B. Flugplan) 1 10 100 30 5 2 50 10 60 3 Iteration Initial. 1 2 3 4 4 20 S {1} w - d[2] 10 d[3] ∞ d[4] 30 {1,2} {1,2,4} {1,2,4,3} {1,2,4,3,5} 2 4 3 5 10 10 10 10 60 50 50 50 30 30 30 30 Wolfgang E. Nagel d[5] 100 100 90 60 60 Bemerkungen ! Wenn man den kürzesten Weg von der Quelle zu jedem Knoten rekonstruieren möchte, so gibt nach Ablauf des Algorithmus p[v] den direkten Vorgänger von v auf dem kürzesten Weg an. ! Beispiel: Iteration p[2] p[3] p[4] p[5] Initial. 1 1 1 1 1 1 2 1 1 2 1 4 1 4 3 1 4 1 3 4 1 4 1 3 ! Zur Bestimmung des kürzesten Weges von 1 nach 5 bestimmt den Vorgänger von 5 = 3, den Vorgänger von 3 = 4, den Vorgänger von 4 = 1, d.h. der kürzeste Weg ist 1-4-3-5 mit Länge 60. Wolfgang E. Nagel Zeitkomplexität des Dijkstra-Algorithmus ! Adjazenzmatrix: innere for-Schleife braucht O(|V|) Zeit Also insgesamt: Tavdijkstra (|V|) = Twdijkstra (|V|) = Tbdijkstra (|V|) ∈ O(|V|2) ! Adjazenzliste: Falls |E| << |V|2 ist, so kann man die Zeitkomplexität auf O(max{|V|, |E|}) reduzieren. Wolfgang E. Nagel Kürzeste Wege zwischen allen Knotenpaaren Gegeben: G = (V,E) und für jede Kante (v,w) eine nicht-negative Bewertung C(v,w). Aufgabenstellung: Bestimme für alle geordneten Paare (v,w) den kürzesten Weg von v nach w. Lösungsmöglichkeit: 1. Wende den Dijkstra-Algorithmus für alle Knoten v als Quelle an ⇒ Zeitkomplexität O(n3). 2. Verwende den Floyd-Algorithmus. Wolfgang E. Nagel Grundidee zum Algorithmus von Floyd Im ersten Schritt vergleicht man alle Wege, die von einem beliebigen Knoten i zu einem anderen Knoten j führen, mit dem Umweg über Knoten 1. Ist der Umweg kürzer, so wird der alte Weg durch den Umweg ersetzt. Im zweiten Schritt werden alle Umwege über den Knoten 2 gebildet. Im k-ten Schritt werden alle Umwege über den Knoten k gebildet, usw. Zum Schluß enthält A[i][j] den kürzesten Weg von i nach j. k Ak-1 [i][k] i Ak-1 [k][j] Ak-1 [i][j] Wolfgang E. Nagel j Algorithmus von Floyd 1. Der Floyd-Algorithmus nutzt eine n x n Matrix, um die Längen der kürzesten Wege zu berechnen. 2. Setze zu Beginn A0[i,j] = C[i,j]. Diagonalelemente = 0 ∀i≠j 3. Es werden n Iterationen über die Matrix A gemacht. In der k-ten Iteration wird die folgende Formel zur Berechnung der neuen A [i,j] benutzt: Ak[i,j] = min (Ak-1[i,j], Ak-1 [i,k] + Ak-1[k,j]) 4. Zum Schluß enthält An[i][j] den kürzesten Weg von i nach j. Wolfgang E. Nagel Floyd-Algorithmus void floyd ( double A [N+ 1 ][ N+ 1 ] , double C[ N+ 1 ] [N+ 1 ] ) int i , j , k for (i = 1 ; i < = N; i + + ) for (j = 1 ; j < = N; j + + ) A [ i ] [ j ] = C[ i ] [ j ] P [i ][ j ] = -1 for (i = 1 ; i < = N; i + + ) A [ i ] [i ] = 0.0 for (k = 1 ; k < = N; k+ + ) for (i = 1 ; i < = N; i + + ) for (j = 1 ; j < = N; j + + ) A [ i ][ k] + A [ k][ j ] < A [i ][ j ] T F A [ i ][ j ] = A [ i ][ k] + A [k][ j ] P [ i ][ j ] = k Wolfgang E. Nagel Zeitkomplexität des Floyd-Algorithmus Tavfloyd (|V|) = Twfloyd ( |V| ) = Tbfloyd ( |V| ) ∈ O(n3) Bemerkung: Der Algorithmus hat eine sehr einfache Struktur. Deshalb wird ein Compiler hier effizienten Code erzeugen. Wolfgang E. Nagel Transitiver Abschluss ! In speziellen Fällen ist man lediglich daran interessiert, ob ein Weg (einer beliebigen Länge) von Knoten i zu Knoten j existiert. ! Der Floyd-Algorithmus kann so modifiziert werden, dass er dieses Problem löst: Warshall-Algorithmus. ! Angenommen, die Kostenmatrix C sei gerade die Adjazenzmatrix des Graphen G, d.h. C[i][j] = 1, falls eine Kante von Knoten i nach Knoten j existiert und 0 sonst. ! Wir wollen die Matrix A so berechnen, dass A[i][j] = 1 genau dann gilt, wenn ein nicht-trivialer Weg von i nach j existiert, d.h. der Weg hat eine Länge ≥ 1. ! A wird häufig der transitive Abschluss der Adjazenzmatrix genannt. ! Berechnungsformel: Ak[i][j] = Ak-1[i][j] OR (Ak-1[i][k] AND Ak-1[k][j]) Wolfgang E. Nagel Warshall-Algorithmus void warshall ( bool A [ N+ 1 ] [ N+ 1 ] , bool C[ N+ 1 ] [ N+ 1 ] ) int i , j , k for (i = 1 ; i < = N; i + + ) for (j = 1 ; j < = N; j + + ) A [ i ][ j ] = C[ i ] [ j ] for (k = 1 ; k < = N; k+ + ) for (i = 1 ; i < = N; i + + ) for (j = 1 ; j < = N; j + + ) A [ i ] [ j ] = = false T F A [ i ] [ j ] = A [ i ] [ k] & & A [ k] [ j ] Wolfgang E. Nagel Zeitkomplexität des Warshall-Algorithmus Tavwarshall ( |V| ) = Twwarshall ( |V| ) = Tbwarshall ( |V| ) ∈ O( |V|3 ) Wolfgang E. Nagel Minimale Spannbäume Definition: Ein Spannbaum ist ein Teilgraph eines ungerichteten Graphen G, welcher ein Baum ist und alle Knoten von G enthält. Ein minimaler Spannbaum eines gewichteten ungerichteten Graphen G ist ein Spannbaum mit minimalen Gewicht. ♦ ! Das Gewicht in einem Subgraphen eines gewichteten Graphen ist die Summe der Gewichte der Kanten des Subgraphen. Wolfgang E. Nagel Minimale Spannbäume Algorithmus von Prim ! Algorithmus von Prim zum Finden eines minimalen Spannbaums ist ein Greedy-Algorithmus ! Algorithmus beginnt mit der Wahl eines beliebigen Startknotens ! Dann wird der minimale Spannbaum aufgebaut, durch Auswahl eines neuen Knotens und Kante, die garantiert im minimalen Spannbaum sind ! Der Algorithmus bricht ab, wenn alle Knoten ausgewählt wurden Wolfgang E. Nagel Algorithmus von Prim void prim (V,E,w,r) VT={r}; d[r]=0; for all v (V-VT) edge (r,v) exists T F 1d[v]=w(r,v); d[v]=∞ while find a vertex u such that d[u]=min{d(v)|v (V-VT)}; 1VT=VT {u}; for all v (V-VT) 1d[v]=min{d[v],w(u,v)}; Wolfgang E. Nagel Zeitkomplexität ! Die while-Schleife wird |V|-1 Mal ausgeführt ! Die Berechnungen von min{d[v]|v (V-VT)} und der for-Schleife führen O(|V|) Schritte aus ! Damit ergibt sich die Zeitkomplexität Tprim(|V|) ∈ O(|V|2) Wolfgang E. Nagel Parallele Formulierung vom Algorithmus von Prim ! Algorithmus von Prim ist iterativ ! In jeder Iteration wird ein neuer Knoten zum minimalen Spannbaum hinzugefügt ! Da sich der Wert d[v] für einen Knoten v immer, wenn ein neuer Knoten u zu VT hinzugefügt, ändern kann, ist es ausgeschlossen mehr als einen Knoten für das Hinzufügen in den minimalen Spannbaum auszuwählen ! Daher können die Iterationen in der while-Schleife nicht parallel ausgeführt werden Wolfgang E. Nagel Parallele Formulierung vom Algorithmus von Prim Jede Iteration kann auf folgende Weise parallelisiert werden: ! Sei p Anzahl Prozesse, n Anzahl Knoten ! Partitionierung des Feldes d und der Menge V, so dass jede Untermenge n/p aufeinanderfolgende Knoten beinhaltet und die Arbeit assoziiert mit jeder Untermenge wird von unterschiedlichen Prozessen ausgeführt n p d[1…n] Prozesse … … 0 1 i p-1 ! Sei Vi die Untermenge der Knoten assoziiert mit Prozess pi für i=1,..,p-1 ! Jeder Prozess pi speichert den Teil des Feldes d, der zu Vi gehört ! Jeder Prozess pi berechnet di[u]=min{di[v]|v∈(V-VT)∩Vi} während jeder Iteration der while-Schleife ! Das globale Minimum über alle di[u] wird dann in Prozess p0 gespeichert Wolfgang E. Nagel Parallele Formulierung vom Algorithmus von Prim ! Prozess p0 verteilt Knoten u, welcher in VT eingefügt wurde, an alle Prozesse ! Der Prozess pi, der für Knoten u verantwortlich ist, markiert u zugehörig zur Menge VT ! Schließlich kann jeder Prozess die Werte in d[v] für seine lokalen Knoten aktualisieren ! Wird ein neuer Knoten u zu VT hinzugefügt, müssen die Werte d[v] für v∈(VVT) aktualisiert werden ! Der Prozess, der für v verantwortlich ist, muss das Gewicht der Kante (u,v) kennen, daher muss jeder Prozess die Spalten der gewichteten Adjazenzmatrix speichern, welche zur Menge Vi, assoziiert mit ihm, gehören A Prozesse … 0 1 … i Wolfgang E. Nagel n p-1 Zeitkomplexität des parallelisierten Algorithmus von Prim ! Der Aufwand eines jeden Prozesses, um die Werte von d[v] zu minimieren und aktualisieren während jeder Iteration ist O(n/p) ! Damit ist der parallele Algorithmus von Prim in O(n) mit n Prozessen Wolfgang E. Nagel Zusammenfassung der Algorithmen zur Graphen-Theorie ! Die Einschränkung auf gerichtete Graphen ist nicht substantiell. Entsprechende Lösungen können auch für ungerichtete Graphen angegeben werden. ! Teile dieser Algorithmen finden sich als ‘Skelett‘ in vielen interessanten Lösungen zu Problemen aus dem wirklichen Leben: – Netzplantechnik – Compiler (Vektorisierung) – Problem des Handlungsreisenden – Produktionssteuerung, etc. ! Darüber hinaus gibt es eine große Anzahl weiterer Probleme und interessanter Algorithmen. Wolfgang E. Nagel Weitere Graph-Algorithmen ! Zusammenhangskomponenten (Erreichbarkeit) ! Flüsse auf Netzwerken (maximale, minimale) ! Transportprobleme – Euler-Kreis: Kreis, in dem jede Kante einmal durchlaufen wird (einfaches Problem) – Hamilton-Kreis: Kreis, in dem jeder Knoten einmal durchlaufen wird (sehr schwieriges Problem) – Traveling Salesman: Gibt es eine Rundreise, die jeden Knoten einmal besucht und deren Länge < L ist? (sehr schwieriges Problem) ! Färbungsprobleme Wolfgang E. Nagel