Prioritätswarteschlangen In vielen Anwendungen müssen Datensätze mit Schlüsseln der Reihe nach verarbeitet werden, doch nicht unbedingt in einer vollständig sortierten Reihenfolge und nicht unbedingt auf einmal. Eine geeignete Datenstruktur muss unter solchen Bedingungen die Eigenschaft haben, dass sie die Operationen des Einfügens eines neuen Elementes und des Löschen des größten Elementes unterstützt. Eine derartige Datenstruktur, die Schlangen (Löschen des ältesten Elements) und Stapeln(Löschen des neuesten Elements) gegenüber gestellt werden kann, wird Prioritätswarteschlange (priority queue) genannt. Diese kann man sich als Verallgemeinerung des Stapels und der Schlange vorstellen. G.Heyer 1 Algorithmen und Datenstrukturen II Anwendungsbeispiele für Prioritätswarteschlangen: - Simulationssysteme, wo die Schlüssel den „Ereigniszeiten“ entsprechen könnten, die der Reihe nach verarbeitet werden müssen - das Job-Scheduling in Computersystemen, wo die Schlüssel den „Prioritäten“ entsprechen könnten, die angeben, welche Benutzer zuerst bedient werden und - numerische Berechnungen, wo die Schlüssel Berechnungsfehler sein können, so dass der größte zuerst bearbeitet werden kann. Prioritätswarteschlangen können als Basis für verschiedene fundamentale Algorithmen zum Durchsuchen von Graphen dienen. Der Hauptgrund der Nützlichkeit von Prioritätswarteschlangen besteht in ihrer Flexibilität, da sie die Möglichkeit geben, eine Vielzahl verschiedenartiger Operationen mit Mengen von Datensätzen mit Schlüsseln effizient auszuführen. G.Heyer 2 Algorithmen und Datenstrukturen II Aufbau und Unterhaltung einer Datenstruktur, die Datensätze mit numerischen Schlüsseln (Prioritäten) enthält und einige der folgenden Operationen unterstützt: - Aufbauen (construct) einer Prioritätswarteschlange aus N gegebenen Elementen. - Einfügen (insert) eines neuen Elements. - Entfernen (remove) des größten Elements. - Ersetzen (replace) des größten Elements durch ein neues Element (außer, wenn das neue Element größer ist). - Verändern (change) der Priorität eines Elements. - Löschen (delete) eines beliebigen angegebenen Elements. - Zusammenfügen (join) von zwei Prioritätswarteschlangen zu einer neuen, größeren. G.Heyer 3 Algorithmen und Datenstrukturen II Die Operation replace entspricht fast einem insert, gefolgt von remove, (wobei der Unterschied darin besteht, dass die Prioritätswarteschlange sich vorübergehend um ein Element verlängert); das ist etwas anderes, als wenn einem remove ein insert folgt. In ähnlicher Weise könnte die Operation change als ein delete implementiert werden, dem insert folgt, und construct könnte mit wiederholter Benutzung der Operation insert implementiert werden. Die Operation join erfordert für eine effiziente Implementierung höherentwickelte Datenstrukturen. Hier wird eine klassische Datenstruktur, die Heap genannt wird, behandelt und die effiziente Implementationen der ersten fünf Operationen ermöglicht. G.Heyer 4 Algorithmen und Datenstrukturen II Die Prioritätswarteschlange ist ein ausgezeichnetes Beispiel einer abstrakten Datenstruktur. Sie ist mittels der Operationen, die auf ihr ausgeführt werden , eindeutig definiert, unabhängig davon, wie die Daten in irgendeiner speziellen Implementation organisiert und verarbeitet werden. Elementare Implementationen Eine Methode der Organisation einer Prioritätswarteschlange ist eine ungeordnete Liste, wobei die Elemente einfach in einem Feld a [1] , ..., a [N] aufbewahrt werden, ohne die Schlüssel zu beachten. Hiermit lassen sich construct, insert und remove leicht implementieren. (Reservierung von a[0] und evtl. a[N+1] für Markenwerte). G.Heyer 5 Algorithmen und Datenstrukturen II static int a [maxN + 1], N; construct (int b[], int M) { for ( N = 1; N <= M; N++ ) a [N] = b[N]; } insert (int v) { a[++ N] = v ; } int remove ( ) { int j , max, v ; max = 1 ; for ( j = 2 ; j <= N ; j++ ) if ( a [j] > a [max]) max = j ; v = a [max]; a [max] = a[N --]; return v ; } G.Heyer 6 Algorithmen und Datenstrukturen II Die Operation construct ist lediglich eine Feldkopie und für insert inkrementiert man einfach N und legt das neue Element in a [N] ab, wobei diese Operation eine konstante Zeit benötigt. Remove erfordert dagegen das Durchsuchen des Feldes, um das Element mit dem größten Schlüssel zu finden, wofür eine lineare Zeit benötigt wird ( alle Elemente im Feld müssen betrachtet werden), danach das Vertauschen von a[N] mit dem Element mit dem größten Schlüssel und das Dekrementieren von N. Die Implementation von replace ist ähnlich. Um die Operation change zu implementieren (Verändern der Priorität des Elements in a [k]), kann man einfach den neuen Wert speichern, und um das Element in a[k] zu löschen (delete), kann man es, wie in der letzten Zeile von remove, mit a [N] vertauschen und N dekrementieren. G.Heyer 7 Algorithmen und Datenstrukturen II Eine andere elementare Form der Organisation, die benutzt werden kann, ist eine geordnete Liste, wobei wiederum ein Feld a [1] , ... , a [N] verwendet wird, die Elemente jedoch entsprechend der wachsenden Reihenfolge ihrer Schlüssel aufbewahrt werden. Jeder Algorithmus für Prioritätswarteschlangen kann in einen Sortieralgorithmus umgewandelt werden, in dem wiederholt insert benutzt wird, um eine Prioritätswarteschlange zu erzeugen, die alle zu sortierenden Elemente enthält, und indem danach wiederholt remove benutzt wird, um die Prioritätswarteschlange zu leeren, wobei die Elemente in umgekehrter Reihenfolge erhalten werden. Die Benutzung einer als ungeordnete Liste dargestellten Prioritätswarteschlange entspricht somit Selection Sort, und die Verwendung einer geordneten Liste entspricht Insertion Sort. G.Heyer 8 Algorithmen und Datenstrukturen II Anstelle der oben angegebenen Implementation mit Hilfe eines Feldes können auch verkettete Listen für die ungeordnete Liste oder die geordnete Liste verwendet werden. Die ändert nichts an den grundlegenden Merkmalen der Leistungsfähigkeit für insert, remove oder replace, gibt jedoch die Möglichkeit, delete und join in konstanter Zeit auszuführen. Es lohnt sich diese einfachen Implementationen zu merken, da sie in der Praxis kompliziertere Methoden überlegen sein können. Beispiel: ungeordnete Liste günstig, wenn nur wenig removeOperationen, jedoch sehr viele Einfügungen ausgeführt werden, geordnete Liste ist geeignet, wenn bei den einzufügenden Elementen immer zu erwarten ist, dass sie dem größten Element in der Prioritätswarteschlange nahe kommen. G.Heyer 9 Algorithmen und Datenstrukturen II Die Datenstruktur des Heaps Die Datenstruktur, die man verwendet, um die Operationen mit Prioritätswarteschlangen zu ermöglichen, beinhaltet das Speichern der Datensätze in einem Feld so, dass jeder Schlüssel garantiert größer ist als die Schlüssel auf zwei bestimmten anderen Positionen. Jeder dieser Schlüssel muss wiederum größer sein als zwei weitere Schlüssel usw. Diese Ordnung lässt sich sehr leicht veranschaulichen, in dem man das Feld in Form einer zweidimensionalen Baumstruktur zeichnet, wobei von jedem Knoten Linien nach unten zu den beiden Knoten führen, von denen bekannt ist, dass sie kleiner sind. Diese Struktur wird „vollständiger binärer Baum“ genannt. G.Heyer 10 Algorithmen und Datenstrukturen II Diese Struktur kann erzeugt werden, in dem ein Knoten (Wurzel genannt) gezeichnet wird und dann nach unten und von links nach rechts fortgefahren wird, in dem jeweils zwei Knoten unter einem Knoten der vorangehenden Ebene gezeichnet und mit ihm verbunden werden, bis N Knoten gezeichnet worden sind. Die beiden Knoten unter jedem Knoten werden dessen (direkte) Nachfolger (children) genannt, der Knoten über jedem Knoten heißt dessen (direkter) Vorgänger (parent). Forderung: Die Schlüssel im Baum müssen der Heap-Bedingung genügen: Der Schlüssel in jedem Knoten soll größer sein (oder gleich) als die Schlüssel in seinen Nachfolgern (falls er Nachfolger besitzt). Daraus folgt, dass sich der größte Schlüssel in der Wurzel befindet. G.Heyer 11 Algorithmen und Datenstrukturen II Darstellung eines Heaps als vollständiger Baum 1 X 2 T 4 G 8 A 9 E 3 O 5 S 10 R 6 M 7 N 11 A 12 I Darstellung: vollständiger binärer Bäume sequentiell innerhalb eines Feldes: - die Wurzel auf Position 1 setzen, - ihre Nachfolger auf die Positionen 2 und 3, - die Knoten der nachfolgenden Ebene auf die Positionen 4, 5, 6 und 7 usw. G.Heyer 12 Algorithmen und Datenstrukturen II Darstellung eines Heaps als Feld k 1 2 3 4 5 6 7 8 9 10 11 12 a [k] X T O G S M N A E R A I Diese Darstellungsform ist sehr nützlich, da es sehr leicht ist, von einem Knoten zu seinem Vorgänger und zu seinen Nachfolgern zu gelangen. - Der Vorgänger des Knotens auf Position j befindet sich auf Position j/2 ( abgerundet auf die nächste ganze Zahl, falls j ungerade ist ), und - die beiden Nachfolger des Knotens auf Position j befinden sich auf den Positionen 2j und 2j + 1. Dadurch ist die Traversierung des Baumes sehr einfach. Ein Heap ist ein als ein Feld dargestellter vollständiger binärer Baum, in dem jeder Knoten der Heap-Bedingung genügt. Insbesondere befindet sich der größte Schlüssel stets auf der ersten Position im Feld. G.Heyer 13 Algorithmen und Datenstrukturen II Alle Algorithmen operieren entlang eines Pfades (path) von der Wurzel zum unteren Ende des Heaps (indem sie sich einfach vom Vorgänger zum Nachfolger oder umgekehrt bewegen). In einem Heap mit N Knoten befinden sich auf allen Pfaden etwa lgN Knoten. Es gibt etwa N/2 Knoten auf der untersten Ebene N/4 Knoten mit direkten Nachfolgern auf der untersten Ebene N/8 Knoten mit „Enkeln“ auf der untersten Ebene usw. Jede „Generation“ besitzt halb so viele Knoten wie die nächste, woraus folgt, dass höchstens lg N Generationen existieren können. Folglich können alle Operationen mit Prioritätswarteschlangen (mit Ausnahme von join ) bei Verwendung von Heaps in logarithmischer Zeit ausgeführt werden. G.Heyer 14 Algorithmen und Datenstrukturen II Algorithmen mit Heaps Die Algorithmen für Prioritätswarteschlangen unter Verwendung von Heaps laufen alle in der Weise ab, dass sie zuerst eine einfache Strukturänderung vornehmen, welche die Heap-Bedingung verletzen könnte, und anschließend den Heap durchlaufen und ihn so modifizieren, dass die Erfüllung der Heap-Bedingung überall gewährleistet ist. Einige Algorithmen durchlaufen den Heap von der untersten Ebene zur Spitze, andere von der Spitze nach unten. Bei allen Algorithmen wird vorausgesetzt, dass die Datensätze aus einem Wort bestehende ganzzahlige Schlüssel sind, die in einem Feld a von einer maximalen Größe gespeichert sind, wobei die aktuelle Größe des Heaps in einer ganzen Zahl N gespeichert ist. G.Heyer 15 Algorithmen und Datenstrukturen II Einfügen eines neuen Elements ( P ) in einen Heap X T P G A G.Heyer S E R O A 16 I N M Algorithmen und Datenstrukturen II Aufbau eines Heaps - Implementierung der Operation insert - Inkrementierung von N (da sich die Größe des Heaps um 1 erhöht) - Ablegen des einzufügenden Datensatzes in a[N] - falls die Heap-Bedingung verletzt ist (der neue Knoten ist größer als sein Vorgänge), Austausch der Knoten - falls immer noch Verletzung der Heap-Bedingung, dann wieder Austausch der Knoten usw. bis alle Knoten in der geforderten Reihenfolge liegen. G.Heyer 17 Algorithmen und Datenstrukturen II Beispiel-Programm In diesem Programm fügt insert ein neues Element in a[N] ein und ruft dann upheap (N) auf, um die Verletzung der Heap-Bedingung in N zu korrigieren. upheap ( int k) { int v ; v = a [k] ; a [0] = INT_MAX; while (a [k/2] <= v) { a [k] = a [k/2] ; k = k/2 ; } a [k] = v ; } insert (int v) { G.Heyer a [++ N] = v ; upheap (N) ; } 18 Algorithmen und Datenstrukturen II Die replace-Operation erfordert das Ersetzen des Schlüssels bei der Wurzel durch einen neuen Schlüssel und danach die Bewegung im Heap von oben nach unten, um die HeapBedingung wieder herzustellen. Die Operation „remove the largest“ (Entfernen des größten Elements) erfordert nahezu den gleichen Prozess. Da der Heap nach der Operation ein Element weniger haben wird, ist es erforderlich, N zu dekrementieren. Im Mittelpunkt der Implementation dieser beiden Operationen steht der Prozess der Korrektur eines Heap, in dem die Heap-Bedingung überall erfüllt ist, außer möglicherweise an der Wurzel. Wenn der Schlüssel an der Wurzel zu klein ist, muss er im Heap nach unten bewegt werden, ohne dass dabei die HeapBedingung bei irgendeinem der berührten Knoten verletzt wird. G.Heyer 19 Algorithmen und Datenstrukturen II Weitere Implementationen sind: downheap (int k) { int j, v ; v = A [k]; while ( k <= N/2 ) { j=k+k; if ( j < N && a [j] < a [j+1] ) j++ ; if ( v >= a[j] ) break; a [k] = a [j]; k = j ; } a [k] = v ; } Diese Prozedur bewegt sich im Heap nach unten, wobei der Knoten auf Position k, falls erforderlich, mit dem größeren seiner beiden Nachfolger, ausgetauscht wird. Dieser Prozess bricht ab, wenn der Knoten auf Position k größer ist als beide Nachfolger oder wenn die unterste Ebene erreicht ist. G.Heyer 20 Algorithmen und Datenstrukturen II Bei der remove-Operation wird folgende Implementation angewendet: int remove ( ) { int v= a[1] ; a[1] = a[N --] ; downheap (1) ; return v ; } Die Implementation der replace-Operation hat folgende Form: int replace (int v) { a[0] = v ; downheap (0) ; return a[0] ; } G.Heyer 21 Algorithmen und Datenstrukturen II Eigenschaften des Heap: Alle grundlegenden Operationen insert, remove, replace, (downheap und upheap), delete und change erfordern weniger als 2 lg N Vergleiche, wenn sie für einen Heap mit N Elementen ausgeführt werden. Alle diese Operationen beinhalten eine Bewegung längs eines Pfades zwischen der Wurzel und der untersten Ebene des Heap, welcher für einen Heap der Größe N nicht mehr als lg N Elemente umfasst. Der Faktor 2 entsteht durch downheap, welches in seiner inneren Schleife zwei Vergleiche ausführt; die anderen Operationen erfordern nur lg N Vergleiche. G.Heyer 22 Algorithmen und Datenstrukturen II Heapsort Auf der Grundlage der bisher dargestellten grundlegenden Operationen mit Heaps kann eine elegante und effiziente Sortiermethode definiert werden. Diese Heapsort genannte Methode, verwendet keinen zusätzlichen Speicherplatz und garantiert, dass M Elemente in ungefähr M log M Schritten sortiert werden, unabhängig von den Eingabedaten. Die Idee besteht darin, einen Heap aufzubauen, der die zu sortierenden Elemente enthält, und sie danach alle in der richtigen Reihenfolge zu entfernen. Eine Möglichkeit des Sortierens besteht darin, die Elemente nacheinander in einen ursprünglich leeren Heap einzufügen, wie in den ersten zwei Zeilen des folgenden Codes (welche in Wirklichkeit nur construct (a, N) implementiert), und danach n remove-Operationen auszuführen, wobei das entfernte Element auf den Platz gesetzt wird, der von dem sich verkleinernden Heap gerade freigegeben wurde. G.Heyer 23 Algorithmen und Datenstrukturen II heapsort (int a [], int N ) { int k; construct (a, 0); for ( k = 1 ; k <= N ; k++) insert ( a [k] ) ; for (k = N; k >= 1; k --) a [k] = remove ( ) ; } A S A S A T O S O ... A R ... I Beispiel: ASORTINGEXAMPLE G.Heyer 24 Algorithmen und Datenstrukturen II Vollständiger Heap , wenn die Schlüssel in der angegebenen Reihenfolge in einen ursprünglich leeren Heap eingefügt werden. X T P G A S E R O A I N M L E In Wirklichkeit ist es besser, den Heap aufzubauen, in dem man ihn rückwärts durchläuft und kleine Heaps von unten her erzeugt. Bei dieser Methode wird jede Position im Feld als Wurzel eines kleinen Heaps betrachtet, und es wird die Tatsache ausgenutzt, dass downheap für solche kleinen Heaps ebenso gut arbeitet wie für den großen Heap. Indem der Heap rückwärts durchlaufen wird, ist jeder Knoten die Wurzel eines Heaps, für den die Heap-Bedingung erfüllt ist. G.Heyer 25 Algorithmen und Datenstrukturen II remove kann implementiert werden, in dem zunächst das erste und das letzte Element vertauscht werden, dann N dekrementiert und schließlich downheap (1) aufgerufen wird. Dies führt zu folgender Implementation von Heapsort: heapsort ( int a [ ] , int N ) { int k, t ; for ( k = N / 2 ; k >= 1 ; k -- ) downheap (a, N , k ) ; while ( N > 1) { t = a [ 1 ] ; a [1] = a [N] ; a [N] = t ; downheap (a , --N, 1 ) ; } } G.Heyer 26 Algorithmen und Datenstrukturen II Eigenschaft: Bottom Up Aufbau eines Heaps erfolgt in linearer Zeit. Der Grund für diese Eigenschaft besteht darin, dass die meisten bearbeiteten Heaps klein sind. Um zum Beispiel einen Heap aus 127 Elementen aufzubauen, ruft die Methode dawnheap für (64 Heaps der Größe 1), 32 Heaps der Größe 3, 16 Heaps der Größe 7, 8 Heaps der Größe 15, 4 Heaps der Größe 31, 2 Heaps der Größe 63 und einen Heap der Größe 127 auf, so dass im ungünstigsten Fall 64 * 0 + 32 * 1 + 16 * 2 + 8 * 3 + 4 * 4 + 2 * 5 + 1 * 6 = 120 „Übertragungen“ (doppelt so viele Vergleiche ) benötigt werden. Für N = 2 ist eine obere Schranke für die Anzahl der Vergleiche durch (k - 1 ) 2 n - k = 2 n - n - 1 < N 1 k n gegeben. Ein ähnlicher Beweis gilt, wenn N keine Zweierpotenz ist. G.Heyer 27 Algorithmen und Datenstrukturen II Eigenschaft: Heapsort benötigt für das Sortieren von N Elementen weniger als 2N lg N Vergleiche. Durch diese Eigenschaft ist Heapsort von praktischem Interesse. Die Anzahl der benötigten Schritte für das Sortieren von N Elementen ist für beliebige Eingabedaten garantiert proportional zu N log N. Im Unterschied zu anderen behandelten Methoden gibt es keine „ungünstigen“ Eingabedaten, bei denen Heapsort langsamer abläuft. G.Heyer 28 Algorithmen und Datenstrukturen II Indirekte Heaps Bei vielen Anwendungen von Prioritätswarteschlangen möchte man die Datensätze nicht verschieben. Stattdessen möchte man, dass die Prioritätswarteschlangen-Routine nicht Werte zurück gibt, sondern anzeigt, welcher der Datensätze der größte ist. Das ist mit der schon beschriebenen Idee des „indirekten Sortierens“ oder des „Pointer Sort“ (ZeigerSortieren) vergleichbar. Es ist zweckmäßig, Heaps auf diese Art zu nutzen. Hier arbeiten die Prioritätswarteschlangen-Routinen, anstatt die Schlüssel im Feld a umzuordnen, mit einem Feld p von Indizes, die sich auf das Feld a beziehen. a [p[k]] ist somit der Datensatz, der dem k-ten Element des Heap entspricht, für 1 k N . G.Heyer 29 Algorithmen und Datenstrukturen II Außerdem wird ein Feld q eingeführt, in dem die HeapPosition des k-ten Elements des Feldes gespeichert wird, damit werden die Operationen delete und change ermöglicht. Der Eintrag von q für das größte Element im Feld ist die Zahl 1. Indirekte Heap-Datenstrukturen k 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 a [k] A S O R T I N G E X A M P L E p[k] 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 a [p[k]] X T P R S O N G E A A M I L E q[k] 10 5 6 4 2 13 7 8 9 1 11 12 3 14 15 Die Implementationen werden entsprechend modifiziert. G.Heyer 30 Algorithmen und Datenstrukturen II Weiterentwickelte Implementationen der join-Operation Wenn die join-Operation effizient ausgeführt werden muss, sind die bisher angegebenen Implementationen unzureichend und es werden weiter entwickelte Techniken benötigt. Unter „effizient“ verstehen wir, dass join ungefähr in der gleichen Zeit ausgeführt werden sollte wie die anderen Operationen. Die bisherigen Methoden scheiden aus, da zwei große Heaps nur zusammen gefügt werden können, indem alle Elemente mindestens eines Heaps in ein großer Feld bewegt werden. Bei einer direkten Darstellung mit Verkettungen müßten in jedem Knoten Verkettungen vorgesehen werden, die auf den Vorgänger und beide Nachfolger zeigen. Es erweist sich, dass die Heap-Bedingung selbst zu stark zu sein scheint, als dass eine effiziente Implementation der joinOperation möglich wäre. Alle höherentwickelten Datenstrukturen, die zur Lösung dieses Problems bestimmt sind, schwächen entweder die Heap-Bedingung oder die Ausgewogenheits-Bedingung ab, um die für join benötigte Flexibilität zu erhalten. Diese Strukturen gestatten die Ausführung aller Operationen in logarithmischer Zeit. G.Heyer 31 Algorithmen und Datenstrukturen II