Algorithmen und Datenstrukturen Übung 13: Priority Queues (Vorrangwarteschlangen1) Definition Eine Priority Queue ist eine Datenstruktur zur Speicherung einer Menge von Elementen, für die eine Halbordnung (Prioritätssteuerung) definiert ist, so dass folgende Operationen ausführbar sind Initialisieren Einfügen eines Elements (insert()) Minimum suchen (min()) Maximum suchen Entfernen beliebiger Elemente (delete()) Herabsetzen eines Schlüssels um einen vorgegebenen Wert (decreaseKey-Operation). Hierbei wird allerdings in der Regel vorausgesetzt, dass die Position der Schlüssel, die man entfernen möchte oder dessen Wert erniedrigen möchte, bekannt ist. Zusammenfügen (Verschmelzen) zweier elementfremder Priority Queues (Java-) Interface für Priority Queues interface PriorityQueue { boolean isEmpty (); Object insert (Object p); /* inserts a node with key equal to key and information attribute equal to entry in the priority queue */ void meld (PriorityQueue Q); /* the priority queue with the priority queue Q */ Object deleteMin (); /* deletes the minimum and returns the entry with the minimum key */ void decreaseKey (double k, Object p); /* decreases the key of p to k */ Object min (); /* returns the entry with the minimum key */ void delete (Object p); /* deletes the node with Object p in the priority queue */ int size (); /* returns the number of entries in the priority queue */ void print (); /* Erzeugt eine Ausgabe der Elemente in der priority queue */ } 1 vgl. Ottmann, T. u. Widmayer, P.: Algorithmen und Datenstrukturen, Mannheim / Wien / Zürich 1991 1 Algorithmen und Datenstrukturen Heap-Implemtierung einer Priority-Queue Heaps sind eine mögliche Implementierungsbasis für Priority Queues. Ein Heap mit N Schlüsseln erlaubt das Einfügen eines neuen Elements und das Entfernen des Minimums (bzw. Maximums) in O(logN) Schritten. Da das Minimum (bzw. Maximum) stets am Anfang des Heap steht, kann die Operation in konstanter Zeit ausgeführt werden. Da Heaps eine sehr starre Struktur besitzen ist es besonders schwierig, zwei Heaps schnell zu einem neuen zusammen zu fügen. Zwei Möglichkeiten bieten sich an: 1. Man kann sämtliche Elemente des kleineren Heap in den größeren Heap einfügen. Das ist in O(k log( N k )) Schritten ausführbar, wobei k die Anzahl der Elemente des kleineren Heap und N die des größeren Heap ist. 2. Auflösen der vorhandenen Strukturen und Aufbau eines neuen Heap mit allen N+k Elementen. Der Aufbau ist in O( N k ) Schritten durchführbar. Implementierung einer Priority-Queue in C++ ("priority_queue" container adaptor) mit den HeapAlgorithmen der STL neue Einträge Eintrag Eintrag Eintrag Eintrag Eintrag pop() push() niedrigste ........... dritthöchste zweithöchste höchste Priorität Abb.: Eine Priority Queue Die "priority_queue" nutzt die "Container"-Klassen "vector" bzw. deque.Typische Deklarationen zu einer "priority_queue" sind: priority_queue< vector<node> > x; priority_queue< deque<string>,case_insensitive_compare > y; priority_queue< vector<int>, greater<int> > z; Die Implementierung der "priority-queue" in der STL-Version von Hewlett Packard zeigt folgende Aussehen: template <class Container, class Compare> class priority_queue { protected : Container c; Compare comp; … public : bool empty() const { return c.empty(); } size_type size() const { return c.size() } value_type& top() const { return c.front(); } void push(const value_type& x { c.push_back(x); push_heap(c.begin(), c.end(), comp); } void pop() { pop_heap(c.begin(), c.end(), comp); c.pop_back(); } } Die STL-Funktionen push_heap() und pop_heap() werden zur Implementierung herangezogen. push() fügt das neue Element an das Ende des Container, push_heap() bringt danach das 2 Algorithmen und Datenstrukturen Element an seinen richtigen Platz. pop() ruft zuerst pop_heap() auf. pop_heap() bringt das Element an das Ende des Container, pop_back() entfernt das Element und verkürzt den Container. Man kann aus dieser Anwendung ersehen, wie leicht es wurde, eine Container-Klasse mit Hilfe der STL einzurichten. Die "priority_queue" der STL ist ein Adapter und stützt sich ab auf die Klassen vector bzw. deque. template <class T, class Container = deque<T>, class Compare = less<typename Container::value_type> > class priority_queue Präsentiert wird ein Interface, das zusätzlich zu den üblichen Container-Methoden fünf MemberFunktionen bereitstellt: bool empty() const gibt zurück, ob die Priority-Queue leer ist. size_type size() const gibt die Anzahl der in der const value_type& top() const gibt das erste Element zurück, d.h. das mit der größten Priorität void push(const value_type& x) fügt das Element x ein void pop() entfernt das erste Element in der Priority_Queue Bsp.: Huffman Coding2 mit "priority_queue"-Container der STL Gegeben ist eine Datei, z.B. mit folgendem Inhalt AAAAAAAAAAAAAA BBB C D E F GGG HHHH Gesucht ist eine geeignete Binärcodierung (Code variabler Länge), die die Häufigkeit des Auftretens der Zeichen berücksichtigt. Der Huffman-Algorithmus realisiert die Binärcodierung mit Hilfe eines Binärbaums, der folgendes Aussehen haben könnte: 2 vgl. 3.1.2.2 3 Algorithmen und Datenstrukturen 28 0 1 14 14 0 1 A 6 0 3 8 1 3 0 1 4 4 0 B G 1 H 2 2 0 Abb.: Huffman-Codierungs-Baum 1 0 1 1 1 1 1 C F D E Jeder Knoten hat ein Gewicht, der den Platz im Huffman-Codierungs-Baum festlegt. Die STL "priority_queue" wird zunächst zur Aufnahme der eingelesenen Zeichen benutzt. Je nach Häufigkeit des Vorkommens der Zeichen erfolgt die Einordnung. Danach werden die beiden Einträge (Knoten) mit der niedrigsten Priorität entfernt, ein neuer interner Knoten (Addition der Gewichte) gebildet. Der neue Knoten wird wieder in die priority_queue gebracht. Das wird solange wiederholt bis nur ein einziger Knoten in der priority_queue vorliegt. // // pqhuff.cpp // /* Das Programm liest Zeichen aus der Datei input.dat, baut daraus einen Huffman-Codierungsbaum auf und benutzt dazu die STL priority queue. Die resultierende Tabelle wird dann ausgegeben */ #include <iostream.h> #include <iomanip.h> #include <fstream> #include <vector> #include <stack> #include <queue> #include <functional> #include <string> using namespace std; // Die Knoten-Klasse struct node { int weight; unsigned char value; const node *child0; const node *child1; // Konstruktor Blattknoten für Zeichen c node(unsigned char c = 0,int i = -1) { value = c; weight = i; child0 = 0; child1 = 0; } 4 Algorithmen und Datenstrukturen // Konstruktor interner Knoten mit den Nachfolgerb c1 und c2 node(const node* c0, const node* c1) { value = 0; weight = c0->weight + c1->weight; child0 = c0; child1 = c1; } // Vergleichsoperatoren zur Herstellung der // Ordnungsbeziehung der priority queue bool operator < (const node &a) const { return weight < a.weight; } bool operator > (const node &a) const { return weight > a.weight; } void traverse(string code = "") const; }; // Die Member-Funktion traverse() dient zur Ausgabe // des Code fuer einen gegebenen Knoten. // Falls der Wurzelknoten angegeben ist, wird der // ganze Baum ausgegeben. void node::traverse(string code) const { if (child0) { child0->traverse(code + "0"); child1->traverse(code + "1"); } else { const char* s = code.c_str(); cout << " " << value << " "; cout << setw(2) << weight; cout << " " << s << endl; } } // Die folgende Routine zaehlt die Zeichen der // Eingabe-Datei. void count_chars(int * counts) { for (int i = 0; i < 256; i++) counts[i] = 0; ifstream file("input.dat"); if (!file) { cerr << "Kein Oeffnen der Eingabedatei moeglich!\n"; throw "abort"; } file.setf(ios::skipws); for (; ;) { unsigned char c; file >> c; if (file) counts[c]++; else break; } } main() 5 Algorithmen und Datenstrukturen { int counts[256]; count_chars(counts); priority_queue< node, vector< node >, greater< node > > q; // // Einlagern der Blattkonten in die queue // for (int i = 0; i < 256; i++) if (counts[i]) q.push(node( i, counts[i])); // // Die Schleife entfernt die beiden kleinsten Knoten // aus der Schlange. Es wird ein neuer interner Knoten // erzeugt, der diese beiden Knoten als Kinder besitzt // Der neue interne Knoten wird dann in die priority // queue eingefuegt. Falls es nur noch einen Knoten in der // priority queue gibt, dann ist der Baum vollstaendig // while (q.size() > 1) { node *child0 = new node(q.top() ); q.pop(); node *child1 = new node(q.top() ); q.pop(); q.push(node(child0, child1)); } // Ausgabe des Resultats cout << "Zeichen Symbol Code" << endl; q.top().traverse(); return 1; } Abb. 6