VorrangwarteschlangeIn der Informatik ist eine Vorrangwarteschlange (auch Prioritätenliste, Prioritätsschlange, Prioritätswarteschlange oder englisch priority queue genannt) eine spezielle abstrakte Datenstruktur, genauer eine erweiterte Form einer Warteschlange. Den Elementen, die in die Warteschlange gelegt werden, wird ein Schlüssel mitgegeben, der die Reihenfolge der Abarbeitung der Elemente bestimmt. OperationenVorrangwarteschlangen müssen die folgenden Operationen unterstützen:
Daneben gibt es noch Operationen, die vor allem für Online-Algorithmen wichtig sind:
Weitere häufig verwendete Operationen sind:
ImplementierungDie Implementierung von Vorrangwarteschlangen kann über AVL-Bäume erfolgen. Sowohl insert als auch extractMin können dann in O(log n) Zeit ausgeführt werden. Eine andere Möglichkeit besteht in der Verwendung partiell geordneter Bäume (Heaps). Auch hier liegt die Zeitkomplexität bei O(log n). Beispiele für Datenstrukturen, die Vorrangwarteschlangen effizient implementieren, sind
Die folgende Tabelle zeigt eine Übersicht über die verschiedenen Laufzeiten von einigen Implementierungen in O-Notation.
a amortisiert b n ist die Größe des größeren Heaps AnwendungenPrioritätswarteschlangen können für die Implementierung diskreter Ereignissimulationen genutzt werden. Dabei werden zu den jeweiligen Ereignissen als Schlüssel die Zeiten berechnet, das Zeit-Ereignis-Paar in die Vorrangwarteschlange eingefügt, und die Vorrangwarteschlange gibt dann das jeweils aktuelle (d. h. als Nächstes zu verarbeitende) Ereignis aus. Allgemein werden Vorrangwarteschlangen für viele Algorithmen oder sogar Klassen von Algorithmen benötigt. Zum Beispiel machen Gierige Algorithmen von Prioritätswarteschlangen Gebrauch, da dort häufig das Minimum oder Maximum einer Menge bestimmt werden muss. Einer der bekanntesten Algorithmen dieser Art ist der Algorithmus von Dijkstra zur Berechnung kürzester Wege. Ebenso kann man Vorrangwarteschlangen für Bestensuche-Algorithmen verwenden. Um in einer Iteration den nächsten vielversprechendsten Knoten zu finden, wird meist eine Vorrangwarteschlange benutzt. Ein Beispiel hierfür ist der A*-Algorithmus, um kürzeste Wege in Graphen zu berechnen. Ein spezieller Algorithmus, der auch mit Vorrangwarteschlangen implementiert werden kann, ist die Huffman-Kodierung. Die Vorrangwarteschlange wird hierbei dafür verwendet, die Zeichen mit der kleinsten Wahrscheinlichkeit zu finden. ErweiterungenNeben der klassischen, einendigen Vorrangwarteschlange existieren auch zweiendige. Diese unterstützen zusätzlich eine Operation extractMax, um das Element mit dem größten Schlüssel herauszuziehen. Eine Möglichkeit für die Implementierung solcher Datenstrukturen bieten Doppelheaps. Alternativ können auch Datenstrukturen wie Min-Max-Heaps genutzt werden, die direkt zweiendige Prioritätswarteschlangen unterstützen. Parallele VorrangwarteschlangeUm Vorrangwarteschlangen weiter zu beschleunigen, kann man sie parallelisieren. Dabei gibt es drei verschiedene Möglichkeiten zur Parallelisierung. Die erste Möglichkeit ist es, einfach die einzelnen Operationen (insert, extractMin …) zu parallelisieren. Die zweite Möglichkeit erlaubt den gleichzeitigen Zugriff von mehreren Prozessen auf die Vorrangwarteschlange. Die letzte Möglichkeit ist es, jede Operation auf k Elementen auszuführen, anstatt immer nur auf einem Element. Im Falle von extractMin beispielsweise würde man die ersten k Elemente mit der höchsten Priorität aus der Vorrangwarteschlange herausnehmen. Einzelne Operationen parallelisierenEin einfacher Ansatz um Vorrangwarteschlangen zu parallelisieren ist, die einzelnen Operationen zu parallelisieren. Das heißt, die Warteschlange funktioniert genau so wie eine sequentielle, allerdings werden die einzelnen Operationen durch den Einsatz von mehreren Prozessoren beschleunigt. Der maximale Speedup, der hierbei erreicht werden kann, ist durch beschränkt, da es sequentielle Implementierungen für Vorrangwarteschlangen gibt, deren langsamste Operation in Zeit läuft (z. B. Binomial-Heap). Auf einem „Concurrent Read, Exclusive Write“-PRAM-Modell (CREW), können die Operationen insert, extractMin, decreaseKey und remove in konstanter Zeit durchgeführt werden, wenn Prozessoren zur Verfügung stehen, wobei die Größe der Vorrangwarteschlange ist.[5] Die Warteschlange ist im Folgenden als Binomial-Heap implementiert. Dabei muss nach jeder Operation gelten, dass maximal drei Bäume derselben Ordnung existieren und dass die kleinste Wurzel der Bäume von Ordnung kleiner als alle Wurzeln von Bäumen höherer Ordnung ist.
Nachfolgend ist die Operation in Pseudocode aufgeführt. insert(e) { L[0] = L[0] ∪ e parallelesZusammenfügen() } parallelesZusammenfügen() { für jede Ordnung i parallel: falls |L[i]| >= 3: füge zwei Bäume aus L[i] \ min(L[i]) zu einem neuen Baum b zusammen L[i+1] = L[i+1] ∪ b }
extractMin() { e = min(L[0]) L[0] = L[0]\e für jede Ordnung i parallel: falls |L[i]| >= 1: teile min(L[i]) in zwei Bäume a,b L[i] = L[i] \ min(L[i]) L[i-1] = L[i-1] ∪ {a,b} parallelesZusammenfügen() return e } Das Konzept für konstante Insert- und ExtractMin-Operationen kann erweitert werden, um auch eine konstante Laufzeit für remove zu erreichen. Die DecreaseKey-Operation kann dann durch ein Remove und ein darauffolgendes Insert ebenso in konstanter Zeit realisiert werden.[5] Der Vorteil dieser Parallelisierung ist, dass sie genau so wie eine normale, sequentielle Vorrangwarteschlange angewendet werden kann. Aber es kann auch nur ein Speedup von erreicht werden. Dieser wird in der Praxis noch weiter dadurch beschränkt, dass bei einer parallelen Implementierung zusätzlicher Aufwand für die Kommunikation zwischen den verschiedenen Prozessoren betrieben werden muss. Gleichzeitiger paralleler ZugriffFalls der gleichzeitige Zugriff auf eine Vorrangwarteschlange erlaubt ist, können mehrere Prozesse gleichzeitig Operationen auf die Vorrangwarteschlange anwenden. Dies wirft jedoch zwei Probleme auf. Zum einen ist die Semantik der einzelnen Operationen nicht mehr offensichtlich. Zum Beispiel, wenn zwei Prozesse gleichzeitig extractMin ausführen, sollten beide Prozesse das gleiche Element erhalten oder unterschiedliche? Dies schränkt die Parallelität auf die Ebene der Anwendung ein, welche die Vorrangwarteschlange benutzt. Zum anderen gibt es das Problem des Zugriffskonfliktes, da mehrere Prozesse Zugriff auf das gleiche Element haben. Auf einem „Concurrent Read, Concurrent Write“-PRAM-Modell (CRCW) kann der gleichzeitige Zugriff auf eine Vorrangwarteschlange implementiert werden. Im Folgenden ist die Vorrangwarteschlange als Skip-Liste implementiert.[6][7] Zusätzlich wird eine atomare Synchronisationsprimitive CAS benutzt, um eine Lock-freie Skip-Liste zu ermöglichen. Die Knoten der Skip-Liste bestehen aus einem einzigartigen Schlüssel, einer Priorität, einem Array aus Zeigern auf die nächsten Knoten und aus einem Delete-Kennzeichen. Das Delete-Kennzeichen gibt an, ob der Knoten gerade dabei ist, von einem Prozess gelöscht zu werden. Dadurch wissen die anderen Prozesse, dass der Knoten kurz davor ist gelöscht zu werden und können, je nachdem, welche Operationen sie ausführen, entsprechend darauf reagieren.
Beim Erlauben vom gleichzeitigen Zugriff auf eine Vorrangwarteschlange muss darauf geachtet werden, dass es zu möglichen Konflikten zwischen zwei Prozessen kommen kann. Zum Beispiel kann es zu einem Konflikt kommen, falls ein Prozess versucht, einen Knoten hinzuzufügen, aber ein anderer Prozess schon dabei ist, seinen Vorgängerknoten zu löschen.[6] Die Gefahr besteht, dass der neue Knoten zwar zur Skip-Liste hinzugefügt wurde, aber nun nicht mehr erreichbar ist. (Siehe Bild) K-Element-OperationenBei dieser Art der Parallelisierung von Vorrangwarteschlangen werden neue Operationen eingeführt, die nicht mehr ein einzelnes Element bearbeiten, sondern Elemente gleichzeitig. Die neue Operation k_extractMin löscht dann die kleinsten Elemente aus der Vorrangwarteschlange und gibt sie zurück. Dabei ist die Warteschlange auf verteiltem Speicher parallelisiert. Jeder Prozessor hat seinen eigenen privaten Speicher und eine lokale (sequentielle) Vorrangwarteschlange. Die Elemente der globalen Warteschlange sind also auf alle Prozessoren verteilt. Bei einer k_insert-Operation werden die Elemente zufällig gleichverteilt den Prozessoren zugewiesen und jeweils in die lokalen Vorrangwarteschlangen eingefügt. Nach wie vor können auch einzelne Elemente eingefügt werden. Mit dieser Strategie gilt mit hoher Wahrscheinlichkeit, dass die global kleinsten Elemente in der Vereinigung der lokal kleinsten Elemente aller Prozessoren sind.[8] Jeder Prozessor hält also einen repräsentativen Teil der globalen Vorrangwarteschlange. Diese Eigenschaft wird bei k_extractMin ausgenutzt, indem aus jeder lokalen Warteschlange die kleinsten Elemente entfernt werden und in einer Ergebnismenge gesammelt. Dabei bleiben die Elemente aber noch ihrem ursprünglichen Prozessor zugeteilt. Die Anzahl der Elemente, die aus jeder lokalen Vorrangwarteschlange gelöscht werden, ist dabei abhängig von und der Anzahl der Prozessoren .[8] Durch parallele Selektion werden die kleinsten Elemente der Ergebnismenge bestimmt. Mit hoher Wahrscheinlichkeit sind diese auch global die kleinsten Elemente. Falls nicht, werden erneut Elemente aus jeder lokalen Vorrangwarteschlange gelöscht, bis die global kleinsten Elemente in der Ergebnismenge sind. Jetzt können die kleinsten Elemente zurückgegeben werden. Alle anderen Elemente werden wieder in die lokalen Vorrangwarteschlangen eingefügt, aus denen sie entfernt wurden. Die Laufzeit von k_extractMin ist erwartet , wenn und die Größe der Vorrangwarteschlange beschreibt.[8] Die Vorrangwarteschlange kann noch verbessert werden, indem die Ergebnismenge nach einer k_extractMin-Operation nicht immer direkt wieder in die lokale Warteschlange eingefügt wird. Dadurch erspart man sich, dass Elemente immer wieder aus einer lokalen Warteschlange entfernt und direkt danach wieder eingefügt werden. Durch das Entfernen von mehreren Elementen gleichzeitig kann gegenüber sequentiellen Vorrangwarteschlangen eine deutliche Beschleunigung erreicht werden. Allerdings können nicht alle Algorithmen diese Art von Vorrangwarteschlange nutzen. Zum Beispiel können beim Dijkstra-Algorithmus nicht mehrere Knoten auf einmal bearbeitet werden. Dijkstra nimmt den Knoten mit der kleinsten Distanz zum Startknoten aus der Vorrangwarteschlange und relaxiert dann dessen Kanten. Dadurch kann sich die Priorität der Nachbarknoten, die sich schon in der Vorrangwarteschlange befinden, verändern. Durch das Herausnehmen der Knoten mit der kleinsten Distanz kann es passieren, dass sich durch die Bearbeitung eines der Knoten die Priorität eines der anderen Knoten ändert. Dieser Knoten sollte dann erst später bearbeitet werden, wird nun aber schon früher bearbeitet. Dadurch wird dieser Knoten dann zu früh als besucht gekennzeichnet. In dem Fall hat man zwar einen Weg vom Startknoten zu diesem Knoten gefunden, aber die Distanz ist nicht minimal. Anders gesagt, wird durch das Verwenden von k-Element-Operationen für den Algorithmus von Dijkstra die Label-Setting-Eigenschaft zerstört. Siehe auchLiteratur
Weblinks
Einzelnachweise
|
Portal di Ensiklopedia Dunia