Datenstrukturen und Algorithmen in C++ von Harald Reß, Günter Viebeck überarbeitet Datenstrukturen und Algorithmen in C++ – Reß / Viebeck schnell und portofrei erhältlich bei beck-shop.de DIE FACHBUCHHANDLUNG Hanser München 2002 Verlag C.H. Beck im Internet: www.beck.de ISBN 978 3 446 22075 1 Inhaltsverzeichnis: Datenstrukturen und Algorithmen in C++ – Reß / Viebeck CARL HANSER VERLAG Harald Reß, Günter Viebeck Datenstrukturen und Algorithmen in C++ 3-446-22075-5 www.hanser.de 4 Stapel und Schlangen Stapel und Schlangen sind grundlegende und einfache Datenstrukturen, die vielfache Anwendungen haben. Ihr Prinzip realisiert schon ein einfacher Zettel mit Notizen über anstehende Arbeiten. Ausgeführte Arbeiten werden im Zettel gestrichen und neu hinzukommende ergänzend notiert. Das vorgenommene Pensum ist vollbracht, wenn der Zettel keine offenen Posten mehr enthält. Stapel und Schlangen dienen nicht wie z.B. Tabellen der Bereithaltung permanent gespeicherter Daten, sondern ihre Komponenten haben mehr die Bedeutung von gespeicherten Aufträgen, die auf Bedienung warten und danach aus der Struktur entfernt werden. Stapel, je nach Verwendung auch als Keller, Stack, Pushdown-Liste, LIFO-Liste (last in, first out) oder LCFS-Liste (last comes, first served) bezeichnet, finden Anwendung vor allem beim Erkennen und Verarbeiten geschachtelter Strukturen und Prozesse. Eintreffende Aufträge werden auf die bereits wartenden gepackt. Die Reihenfolge der Bedienung ist von oben nach unten, d.h. der oben auf dem Stapel liegende Auftrag wird als erster bedient und dabei aus dem Stapel genommen. Das Prinzip des Stapels lässt sich am Beispiel des Postkorbes eines Sachbearbeiters, in dem sich die zu bearbeitenden Akten „stapeln“, verdeutlichen. Neue Akten werden oben auf den Stapel zur Bearbeitung gelegt und die nächste Akte zur Verarbeitung wird oben vom Stapel geholt. Die Verweildauer im Postkorb ist für oben liegende Akten kürzer als für weiter unten liegende. Ähnlich sind die Einkaufswagen in einem Supermarkt in Form eines Stapels aneinandergekettet und der Stapel gibt immer nur den zuletzt zurückgegebenen Wagen zur Benutzung wieder frei. Eine gleich wichtige Struktur wie der Stapel ist die Schlange, die auch als Warteschlange, Queue, FIFO-Liste (first in, first out) oder FCFS-Liste (first comes, first served) bezeichnet wird. Typische Anwendungen in der Informatik sind die Simulation realer Warteschlangen und die Verwendung von Puffern bei der Synchronisation unabhängig laufender Prozesse. Auch die Aufträge in einer Schlange warten auf Bedienung. Es wird aber, im Unterschied zum Stapel, der am längsten wartende Auftrag zuerst bedient. Das Prinzip der Schlange entspricht dem des realen Postschalters, vor dem eine Reihe von Personen auf Bedienung warten. Jeder, der hinzukommt, schließt sich am Ende der Reihe (queue) an und wartet, bis alle, die vor ihm gekommen sind, bedient wurden (first comes, first served). In Betriebssystemen werden Anforderungen an dieselbe Systemkomponente, z.B. einen Dru- 146 4 Stapel und Schlangen cker, typischerweise in einer Schlange behandelt und nacheinander gewährt, um so gegenseitige Störungen auszuschließen. 4.1 Abstrakter Datentyp Stack Die grundlegenden Operationen zur Bearbeitung eines Stapels sind Push und Pop. Mit Push werden Aufträge auf einen Stapel gelegt und mit Pop wieder geholt und verschwinden dann aus dem Stapel. Push legt oben auf einem Stapel ab und Pop holt oben vom Stapel. Damit liegt der Auftrag mit der kürzesten Verweildauer im Stapel oben (top) und wird auch als Erstes geholt, während der Auftrag mit der längsten Verweildauer unten (bottom) liegt und erst dann erreichbar wird, wenn alle davor stehenden Aufträge entnommen sind. Push Auftragn ... Auftrag3 Pop top Auftrag2 Auftrag1 bottom Abbildung 4-1: Schema eines Stapels Notwendig ist auch eine Operation, die prüft, ob sich überhaupt Daten im Stapel befinden. Abhängig von der Implementation kann eine Prüfung, ob ein Stapel noch einen weiteren Auftrag annehmen kann, erforderlich sein. Ein Stapel muss vor seiner Verwendung als leerer Stapel initialisiert werden. Erforderlich ist auch eine Operation zur Freigabe eines Stapels nach seiner Verwendung. Manche Aufgaben benötigen noch eine Operation, die das Entnehmen vom Stapel vom Inhalt des betreffenden Auftrags abhängig machen. Wir sehen dafür die Operation Top vor, die den Wert des obersten Auftrags im Stapel liefert, ihn aber nicht aus dem Stapel entfernt. Unabhängig von ihrer Implementation werden alle diese Operationen in O(1) ausgeführt. Der folgende public-Teil der Klasse Stack beschreibt einen generischen ADT für einen Stapel mit Records vom beliebigen Datentyp TR. template <class TR> // Stapel für Records vom beliebigen Typ TR class Stack { public: Stack(); // kreiert leeren Stapel Stack(unsigned max); // leerer Stapel für maximal max // Records ~Stack(); // Destruktor void Push(TR r); // legt Record r oben auf Stapel TR Pop(); // holt obersten Record vom Stapel 4.2 TR Top(); unsigned Length(); bool IsEmpty(); bool IsFull(); // // // // Implementation des ADT Stack 147 liefert den Wert des obersten Records Anzahl der Records im Stapel true, wenn Stapel leer, sonst false true, wenn Stapel voll, sonst false } ADT 4-1: Abstrakter Datentyp Stack Für einen leeren Stapel ist ein initaler – durch den Konstruktor der Klasse TR generierter – Record Rückgabewert der Operationen Pop und Top. Push hat keine Wirkung für einen vollen Stapel. Eine Anwendung des ADT Stack demonstriert das folgende kleine Programm, das eine Folge von Ganzzahlen in umgekehrter Reihenfolge ausgibt. Die Ganzzahlen werden zunächst eine nach der anderen auf einen Stapel gelegt, der dann in einem zweiten Schritt sukzessive abgearbeitet wird. int x; Stack<int> s(20); while ((cin >> x)&&!s.IsFull()) s.Push(x); // auf Stack ablegen while (!s.IsEmpty()) cout << s.Pop() << endl; // vom Stack holen 4.2 Implementation des ADT Stack Für den ADT Stack werden hier zwei Implementationen, eine als Arraystruktur und eine andere als gekettete Liste, vorgestellt. Stack(max) generiert einen leeren Array und Stack() eine leere Liste. 4.2.1 Implementation als Array Wenn bekannt ist, dass ein Stapel gleichzeitig nie mehr als m Werte hat, bietet sich seine Implementierung als Array mit m Komponenten an. Der Stapel kann dann über eine Indexvariable top gesteuert werden. Die Indexvariable bezeichnet die Position für die nächste Eingabe und ihr um 1 verminderter Wert die Position, aus der als Nächstes entnommen wird. Die Variable wird mit jeder Eingabe um 1 erhöht und mit jeder Ausgabe um 1 vermindert. Der Stapel ist leer, wenn die Indexvariable den Wert 0, voll, wenn sie den Wert m aufweist. 148 4 Stapel und Schlangen top → m m-1 m-1 m-1 top → i 2 top → 1 1 1 0 0 0 Stapel leer Stapel voll Abbildung 4-2: Stapel in Arraydarstellung Die Bestimmungsgrößen der folgenden Implementierung der abstrakten Datenstruktur Stack als Array sind die Zeigervariable a mit der Adresse des Arrays, die Variable m, die die Länge des Arrays beschreibt und die Variable top, die auf den nächsten freien Platz oben im Array zeigt. Die Variable top wird mit dem Indexwert 0 initialisiert. template <class TR> class Stack // Stapel für Records vom beliebigen Typ TR { private: unsigned m; // Arraylänge; m: max Anzahl Records unsigned top;// zeigt auf oberste Position im Stack TR *a; // Zeiger auf Array }; Der Konstruktor Stack(max) reserviert Speicherplatz für einen Array der Länge max Records vom Typ TR und speichert die Adresse des Arrays in der Zeigervariablen a. Außerdem werden die Variablen top und m initialisiert. // Konstruktor: legt Speicherplatz für m Records an template <class TR> Stack<TR>::Stack(unsigned max):top(0),m(max) { a=new TR[m]; } Das Stapeln eines Records setzt voraus, dass der Array nicht voll ist. Für einen vollen Stapel gilt top=m. Die Boolesche Elementfunktion IsFull() hat den Wert true, wenn der Stapel voll ist, sonst ist ihr Wert false. // prüfen, ob Stapel voll ist template <class TR> bool Stack<TR>::IsFull() { 4.2 Implementation des ADT Stack 149 if (top==m) return true; else return false; } Entsprechend hat IsEmpty() den Wert true, wenn die Tabelle leer ist. Für eine leere Tabelle gilt top=0. // prüfen, ob Stapel leer ist template <class TR> bool Stack<TR>::IsEmpty() { if (top==0) return true; else return false; } Die Elementfunktion Push(r) legt den Record in der Variablen r oben im Stapel ab. Der Stapel darf nicht voll sein. Die Variable top wird um eins erhöht. // oben im Stapel ablegen, Bedingung: Stapel nicht voll template <class TR> void Stack<TR>::Push(TR r) { if (!IsFull()) { a[top]=r; top++; } } Die Elementfunktion Pop() holt den im Stapel oben liegenden Record und überträgt ihn in die Variable r. Der oben im Stapel liegende Record hat den Index top-1. Durch Verminderung der Variablen top um 1 wird der Arrayplatz, aus dem gerade entnommen wurde, für eine nachfolgende Push-Operation wieder frei. Der bisherige zweitobere Record im Array nimmt jetzt die obere Position ein. // obersten Record holen und entfernen // Bedingung: Stapel nicht leer template <class TR> TR Stack<TR>::Pop() { TR r; // initialer Record if (!IsEmpty()) { top--; r=a[top]; } return r; } 150 4 Stapel und Schlangen Die Elementfunktion Top() liefert den Wert des obersten Records, ohne ihn jedoch aus dem Stapel zu entfernen. // Wert des obersten Records im nicht leeren Stapel holen template <class TR> TR Stack<TR>::Top() { TR r; if (!IsEmpty()) r=a[top-1]; return r; } Die Elementfunktion Length() liefert die Anzahl der Records im Stapel // Anzahl der Records im Stapel template <class TR> unsigned Stack<TR>::Length() { return top; } 4.2.2 Implementation als gekettete Liste Die Records werden nicht in einem zusammenhängenden Speicherplatz abgelegt, sondern jeder Recordwert wird zusammen mit einem Zeiger auf den nächsten Record als Knoten für sich gespeichert. Die Knoten können beliebig im Speicher verstreut sein; sie hängen über die mitgespeicherten Zeigerwerte zusammen. Der Anfang wird durch eine Variable, deren Wert ein Zeiger auf den ersten Knoten des Stapels ist, bestimmt. Durch den Wert dieser Variablen wird der Stapel repräsentiert. Das Ende des Stapels ist durch den Zeigerwert NULL im letzten Knoten festgelegt. Der erste Knoten in der geketteten Liste ist der oberste Knoten im Stapel. a1 s a2 ... ai 1.Knoten ... an-1 an NULL letzter Knoten Abbildung 4-3: Stapel als gekettete Liste Abbildung 4-3 stellt einen Stapel als gekettete Liste dar, die Variable s zeigt auf den ersten Knoten des Stapels; durch den Zeigerwert in s wird der Stapel erreichbar. Im Stapel stehen gerade n Aufträge. Ein Objekt der Klasse Stack realisiert einen Stapel, dessen Knoten über Zeiger verkettete Records vom Typ StackNode sind. Ein Stapel wird durch einen Wert der Zeigervariablen s vom Typ StackNode, der den ersten Knoten des Stapels adressiert und im Fall eines leeren 4.2 Implementation des ADT Stack 151 Stapels NULL ist, repräsentiert. Ein Record vom Typ StackNode setzt sich aus einem Wert des generischen Datentyps TR und einem Wert vom Zeigertyp StackNode zusammen. Die Struktur ist ein Beispiel einer Schachtelung von Klassen. Die Klasse StackNode ist nur innerhalb der Klasse Stack bekannt. Elementfunktionen einer geschachtelten Klasse können nicht direkt auf die private-Elementdaten der umgebenden Klasse zugreifen. Für eine umgebende Klasse gilt entsprechendes. Hier soll im Prinzip sichtbar gemacht werden, dass die Klasse StackNode nur von Stack verwendet wird. template <class TR> class Stack { private: class StackNode { public: StackNode() : next(NULL) {} // Konstruktor: setzt den Zeiger auf NULL TR TRvalue; // Wert eines Records vom Typ TR StackNode *next; // Zeiger auf einen Record vom Typ StackNode }; StackNode *s; // Zeiger auf Stack unsigned n; // Anzahl Records im Stapel }; Der Konstruktor Stack() generiert einen leeren Stapel, für den s=NULL ist. NULL s // Konstruktor: generiert leeren Stapel template <class TR> Stack<TR>::Stack() { n=0; // Recordanzahl s=NULL; // leerer Stack } Das einfache Konzept des Stapels führt auch zu sehr einfachen Algorithmen, in denen der leere Stapel nicht gesondert betrachtet werden muss, wie folgende Überlegungen zeigen: 152 4 Stapel und Schlangen • Einfügen in einen leeren Stapel: vorher: p nachher: x NULL s NULL TRvalue next s StackNode *p=new StackNode; p->TRvalue=x; p->next=s; s=p; n++; • Einfügen in einen nicht leeren Stapel: p x y NULL s s x NULL neuer Knoten StackNode *p=new StackNode; p->TRvalue=y; p->next=s; s=p; n++; • Holen aus einem anschließend leeren Stapel: p x NULL NULL s s StackNode *p=s; TR r=s->TRvalue; s=s->next; delete p; n--; return r; • Holen aus einem anschließend nicht leeren Stapel: p y s x NULL x s NULL 4.2 Implementation des ADT Stack StackNode *p=s; TR r=s->TRvalue; s=s->next; delete p; n--; return r; Die Elementfunktionen Push, Pop und Top lauten somit: // Legt Record r oben auf nicht vollen Stapel template <class TR> void Stack<TR>::Push(TR r) { StackNode *p=new StackNode; p->TRvalue=r; p->next=s; s=p; n++; } // holt den obersten Record aus nicht leerem Stapel template <class TR> TR Stack<TR>::Pop() { StackNode *p=s; TR r; if (!IsEmpty()) // nur holen, wenn Stapel nicht leer { r=s->TRvalue; s=s->next; delete p; n--; } return r; } // Wert des obersten Records im nicht leeren Stapel holen template <class TR> TR Stack<TR>::Top() { TR r; if (!IsEmpty()) r=s->TRvalue; return r; } 153 154 4 Stapel und Schlangen Für IsFull und IsEmpty gelten: // prüfen, ob der Stapel voll ist template <class TR> bool Stack<TR>::IsFull() { return false;// Stapel kann nicht voll werden } // prüfen, ob der Stapel leer ist template <class TR> bool Stack<TR>::IsEmpty() { if (n==0) return true; else return false; } Length() lautet wie bei der Array-Implementation: // Anzahl der Records im Stapel template <class TR> unsigned Stack<TR>::Length() { return n; } 4.3 Anwendungen von Stapeln Der Anwendungsschwerpunkt für Stapel liegt bei den vielfältigen Aufgaben der Auflösung geschachtelter Prozesse. Typische Beispiele sind: das Auswerten arithmetischer Ausdrücke, die Simulation rekursiver Programme und die Auflösung geschachtelter Unterprogramme, von denen wir das erste hier und das zweite als Aufgabe 9 in 4.9 behandeln. Bei der konventionellen Darstellung eines arithmetischen Ausdrucks, auch als Infixnotation bezeichnet, können die Operationen nicht so ohne weiteres in der Anordnung ihrer Folge von links nach rechts ausgewertet werden. Man erhält das Ergebnis, indem man den Ausdruck in Abhängigkeit von Klammern und Prioritäten vorwärts und rückwärts unter Bildung und Einsetzung von Zwischenergebnissen durchläuft. So wird im Beispiel des einfachen Ausdrucks 5+3·2/3 zuerst multipliziert, dann dividiert und erst zum Schluss addiert. Dies ist ein recht komplizierter Vorgang, weshalb Compiler die Postfixnotation, die einen Ausdruck so darstellt, dass er zeichenweise von links nach rechts durchlaufen und dabei direkt mit Hilfe eines Stapels ausgewertet werden kann, bevorzugen. 4.3 Anwendungen von Stapeln 155 Präfix- und Postfixnotation Die Postfixnotation ist eine klammer- und prioritätenfreie Schreibweise, bei der die Operatoren den Operanden folgen. Die Präfixnotation ist die zur Postfixnotation analoge Darstellung, bei der die Operatoren den Operanden vorangehen. Beide Notationen wurden von dem polnischen Mathematiker Lukasiewicz eingeführt. Die Präfixnotation ist auch als polnische und die Postfixnotation als umgekehrte polnische Notation bekannt. Beispiele für binäre Operationen, ausgedrückt in diesen Notationen, sind: Infixnotation Präfixnotation Postfixnotation x+y x·y +xy ·xy xy+ xy· Abbildung 4-4: Einfache Infix-, Präfix- und Postfixdarstellung Ein Ausdruck in Infixnotation geht in einen äquivalenten Ausdruck der Postfixnotation über, wenn man die Operatoren systematisch so umordnet, dass sie nicht mehr zwischen sondern hinter ihren Operanden stehen. Entsprechend werden die Operatoren bei der Umwandlung in die Präfixnotation vor ihren Operanden angeordnet. Infixnotation Präfixnotation Postfixnotation a·(b+c) a·[+bc] ·a+bc [·ab]+c +·abc a·[+bc]·(d+e·f)+g a·[+bc]·(d+[·ef])+g a·[+bc]·[+d·ef]+g a·[·+bc+d·ef]+g [·a·+bc+d·ef]+g +·a·+bc+d·efg a·[bc+] abc+· [ab·]+c ab·c+ a·[bc+]·(d+e·f)+g a·[bc+]·(d+[ef·])+g a·[bc+]·[def·+]+g a·[bc+def·+·]+g [abc+def·+··]+g abc+def·+··g+ a·b+c a·(b+c)·(d+e·f)+g Abbildung 4-5: Ausdrücke in Infix-, Präfix- und Postfixnotation Die eckigen Klammern in den Beispielen der Abbildung 4-5 markieren die Auflösungsstufen, die sich nach den Vorrangregeln für die Infixnotation bestimmen. Die Reihenfolge der Operanden ist in allen Notationen die gleiche. Als Beispiel behandeln wir die Aufgabe, einen in Infixnotation gegebenen Ausdruck in die Postfixnotation zu übersetzen und ihn dann in dieser Form auszuwerten. Übersetzen von Infix- in Postfixnotation Wir betrachten zunächst einen vollständig geklammerten Ausdruck in Infixnotation, z.B. (a·((b+c)·(d+(e·f)))) 156 4 Stapel und Schlangen und durchlaufen ihn von links nach rechts. Öffnende Klammern zeigen den Beginn eines Teilausdrucks an, der möglicherweise auch geschachtelt sein kann und immer aus zwei Operanden und einem Operator besteht. Die Operanden eines Teilausdrucks werden direkt in die Ergebnisausgabe übertragen und der Operator wird auf einen Stapel gelegt. Eine schließende Klammer besagt, dass der zuletzt begonnene Teilausdruck beendet ist und ihre Operanden bereits ausgegeben sind, während der zugehörige Operator oben im Stapel liegt und jetzt von dort geholt werden muss. Öffnende und schließende Klammern selbst werden übergangen. Der Postfix-Ausdruck für das obige Beispiel entwickelt sich wie folgt: " " + + + + + + + + " " " " " " " " " " " " " " " " " " " " " " " " " " " " Stapel ↑ Infixnotation ( a " ( ( b + c ) " ( d + ( e " f ) ) ) ) ↓ Ausgabe a b c + d e f " + " " Ausdruck in Postfixnotation: abc+def"+"" Abbildung 4-6: Beispiel für die Umwandlung von Infix-in Postfixnotation Das folgende Programm transformiert vollständig geklammerte arithmetische Ausdrücke in Infixnotation in die Postfixnotation. Zugelassen sind die Grundoperationen Addition, Subtraktion, Multiplikation und Division in beliebiger Schachtelung. Die Operanden sollen der Einfachheit halber Variablennamen sein, die aus je einem Kleinbuchstaben gebildet sind. Der Ausdruck wird zeichenweise von links nach rechts eingelesen, transformiert und sukzessive wieder ausgegeben. char c; Stack<char> s(10); while (cin >> c) // Einlesen nächstes Zeichen des Ausdrucks { if (c==')') cout << s.Pop (); if (c== '+' || c=='-' || c=='*' || c=='/') s.Push(c); if (c>='a' && c<='z') cout << c; } Im Fall eines nicht vollständig geklammerten arithmetischen Ausdrucks kann ein Teilausdruck mehr als zwei Operanden und entsprechend mehr als einen Operator enthalten. Es ist 4.3 Anwendungen von Stapeln 157 daher notwendig, auch öffnende Klammern, die den Beginn eines neuen Teilausdrucks anzeigen, in den Stapel zu bringen. Operanden werden wie bei der Transformation von vollständig geklammerten Ausdrücken direkt ausgegeben. Für einen Operator muss seine Prioritätenfolge beachtet werden. Deswegen müssen beim Auftreten eines Operators alle Operatoren im gleichen Teilausdruck mit gleicher oder höherer Priorität dem Stapel entnommen und ausgegeben werden, erst danach wird der neue Operator in den Stapel gebracht. Eine schließende Klammer beendet einen Teilausdruck und bewirkt, dass so lange Operatoren dem Stapel entnommen und ausgegeben werden, bis die den Teilausdruck begrenzende öffnende Klammer gefunden wird, die danach aus dem Stapel entfernt wird. Wird das Endezeichen des Ausdrucks festgestellt, werden alle noch im Stapel befindlichen Operatoren ausgegeben. Der Algorithmus ist in Form einer Entscheidungstabelle für die Verarbeitung des nächsten Zeichens des Ausdrucks gegeben: s.Top Operand Leer O nächstes Zeichen im Ausdruck +,",/ ( ) S S S Err Endezeichen +,- O E1 S S E2 ",/ O E1 E1 S E2 E3 ( O S S S E2 E3 S: O: Err: E1: E2: E3: E3 auf Stapel legen Ausgeben Fehler Entnehmen aus Stapel und Ausgeben, bis Operator mit kleinerer Priorität erreicht ist; dann stapeln Entnehmen aus Stapel und ausgeben, bis öffnende Klammer erreicht ist, Klammer aus Stapel entnehmen Stapel komplett ausgeben Abbildung 4-7: Entscheidungstabelle für die Umwandlung von In- in Postfixnotation Als Beispiel verfolgen wir in Abbildung 4-8 die Umwandlung des Ausdrucks a"(b+c)"(d-e/f). Auswerten eines Ausdrucks in Postfixnotation Der auszuwertende Ausdruck in Postfixnotation wird zeichenweise von links nach rechts eingelesen. Dabei angetroffene Operanden werden auf Stapel gelegt, während Operatoren sofort mit den beiden obersten im Stapel liegenden Werten ausgeführt werden. Die PopOperation liefert zunächst den rechten und dann den linken Operanden und das Ergebnis 158 4 Stapel und Schlangen der Operation kommt in den Stapel. Am Schluss steht das Ergebnis des Ausdrucks als einziger Wert im Stapel. Wir verfolgen die schrittweise Auswertung für das Beispiel abc+·def/-· in Abbildung 4-9. Der Teil der Abbildung oberhalb des Pfeils demonstriert, wie sich der Ausdruck in Infixnotation schrittweise im Stapel entwickelt. Der Ausdruck in Postfixnotation ist unterhalb des Pfeils dargestellt. / / + + Stapel - - - - - ( ( ( ( ( ( ( ( ( ( ( ( " " " " " " " " " " " " " " " " ↑ Infixnotation a " ( b + c ) " ( d - e / f ) ↓ Ausgabe a b c + " d e f / - " Ausdruck in Postfixnotation: abc+"def/-" Abbildung 4-8: Beispiel für die Umwandlung von Infix-in Postfixnotation f c b b b+c a a a a a· (b+c) e e e/f d d d d d-e/f a· (b+c) a· (b+c) a· (b+c) a· (b+c) a· (b+c) a·(b+c)· (d-e/f) f / - · ⇑ a b c + · d e Abbildung 4-9: Beispiel für die Auswertung in Postfixnotation Das folgende Programm liefert den Wert eines in Postfixnotation gegebenen und als Folge von char-Zeichen dargestellten gültigen arithmetischen Ausdrucks mit den Operationen für die Addition, Subtraktion, Multiplikation und Division von reellen Zahlen. Die Operanden repräsentieren durch je einen Kleibuchstaben benannte Variablen. Die möglichen Variablen a, b, c, ..., z eines Ausdrucks sind in dieser Ordnung in einem Array t der Länge 26 zusammengefasst; die Arraywerte sind die Variablenwerte vom Typ float. float t[26]; char c; 4.4 Abstrakter Datentyp Queue 159 Stack<float> s(20); while (cin >> c) // nächstes Zeichen { while (c==' ') cin >> c; // Leerstellen werden überlesen if (c>='a' && c<='z') // Operand ? { s.Push(t[c-'a'].GetValue()); // Wert der Variablen im Stack ablegen continue; // Fortsetzen Eingabeschleife } switch (c) // Operator ? { case '+' : s.Push(s.Pop()+s.Pop()); break; case '-' : s.Push(-(s.Pop()-s.Pop())); break; case '*' : s.Push(s.Pop()*s.Pop()); break; case '/' : s.Push(1/(s.Pop()/s.Pop())); } } cout << s.Pop() << endl; Die äußere while-Schleife liest einen Ausdruck zeichenweise ein. Leerstellen werden in der inneren while-Schleife übergangen. Liegt eine Variable vor, wird ihr Wert aus dem Array t entnommen und im Stapel gespeichert. Ein Operator wird auf die beiden obersten im Stapel liegenden Werte, die über die Pop-Operation entnommen werden, angewendet, das Ergebnis geht wieder in den Stapel. Am Schluss steht der Wert des Ausdrucks im Stapel und wird von dort ausgegeben. 4.4 Abstrakter Datentyp Queue Die fundamentalen Operationen auf einer Schlange sind Enqueue und Dequeue. Enqueue legt einen Auftrag in einer Schlange ab und Dequeue entnimmt einen Auftrag aus einer Schlange. Enqueue fügt am Ende (rear) der Schlange ein und Dequeue entfernt am Anfang (front) der Schlange. Damit befindet sich der Auftrag mit der längsten Verweildauer am Anfang einer Schlange und wird als nächster geholt, während der Auftrag mit der kürzesten Verweildauer am Ende liegt und erst dann verfügbar wird, wenn alle vorher stehenden Aufträge entnommen sind. In Abbildung 4-10 stehen n Aufträge in der Schlange. 160 4 Stapel und Schlangen Auftrag1 Auftrag2 Auftrag3 ... Auftragn front rear Abbildung 4-10: Schema einer Schlange Notwendig ist auch eine Operation, die prüft, ob sich überhaupt Daten in einer Schlange befinden. Abhängig von der Implementation kann eine Prüfung, ob eine Schlange noch einen weiteren Auftrag annnehmen kann, erforderlich sein. Eine Schlange muss vor ihrer Verwendung als leere Schlange initialisiert werden. Erforderlich ist auch eine Operation zur Freigabe einer Schlange nach ihrer Verwendung. Manche Aufgaben benötigen noch eine Operation, die die Entnahme aus einer Schlange vom Inhalt des betreffenden Auftrags abhängig machen. Wir sehen dafür die Operation Front vor, die den Wert des obersten Auftrags in der Schlange liefert, ihn aber nicht aus der Schlange entfernt. Unabhängig von ihrer Implementation werden alle diese Operationen – so wie die Grundoperationen auf Stapeln – in O(1) ausgeführt. Der folgende public-Teil der Klasse Queue beschreibt einen generischen ADT für eine Schlange mit Records vom beliebigen Datentyp TR. template <class TR> class Queue { public: Queue(); Queue(unsigned m); // ~Queue void Enqueue(TR &r); TR Dequeue(); TR Front(); unsigned Length(); bool IsEmpty(); bool IsFull(); }; // leere Schlange leere Schlange f. maximal m Elemente // gibt Schlange frei // fügt Record r am Ende ein // holt am längsten wartenden Record // Wert des am längsten wartenden Rec. // Anzahl Records in der Schlange // true, wenn Schlange leer ist // true, wenn Schlange voll ist ADT 4-2: Abstrakter Datentyp Queue 4.5 4.5 Implementation des ADT Queue 161 Implementation des ADT Queue Auch für den ADT Queue geben wir hier Implementationen der Schlange sowohl für eine Arraystruktur als auch für eine gekettete Liste. Der Konstruktor Queue() generiert eine leere gekettete Liste und der Konstruktor Queue(m) einen leeren Array. 4.5.1 Implementation des ADT Queue als Array Wenn bekannt ist, dass eine Schlange gleichzeitig nicht mehr als m Records enthält, könnte man, wie beim Stapel, an ihre Implementation als Array denken. Ein möglicher Ansatz wäre: der erste Record wird im ersten Arrayplatz abgelegt, nachfolgende Records belegen die anschließenden Plätze. Entsprechend wird der erste Record vom ersten Arrayplatz entnommen, der zweite vom zweiten usw. Dabei wird zwar von vorn her wieder Platz frei, die Schlange wird hinten aber sehr schnell an die Arraygrenze stoßen. Um dies zu verhindern, könnte man bei jeder Entnahme alle Records (ab dem zweiten Arrayplatz) um eine Position nach vorn schieben, sodass immer vom ersten Arrayplatz entnommen wird und der Array tatsächlich m Records aufnehmen kann. Das wäre aber sehr aufwendig. Man erreicht dasselbe, ohne Records verschieben zu müssen, wenn man sich den Speicherplatz des Arrays zirkular vorstellt und dem Arrayplatz mit dem Index (m-1) den Arrayplatz mit dem Index 0 folgen lässt. in m-1 0 a3 Queue a2 1 a1 out Abbildung 4-11: Schlange in zirkularer Arraydarstellung Ist der Arrayplatz mit dem Index (m-1) belegt, der für den Index 0 aber frei, wird der nächste Record in diesem abgelegt; Entsprechendes gilt für das Entnehmen. Freigewordene Plätze werden mit Zugängen überschrieben. Die Variable in bezeichnet den Arrayplatz, in den als Nächstes eingegeben wird und die Variable out den Platz, aus dem als Nächstes entnommen wird. Die Schlange ist voll, wenn sie m Einträge hat, sie ist leer, wenn sie 0 Einträge aufweist. Die Schlange wird mit in=0, out=0 und der Anzahl der aktuellen Records n=0 initialisiert. Die Bestimmungsgrößen der folgenden Implementierung sind die Zeigervariable a mit der Adresse des Arrays, die Variable m, die die Länge des Arrays beschreibt, die Variable n für 162 4 Stapel und Schlangen die aktuelle Länge der Schlange, und die Variablen in und out, die den nächsten Platz der Eingabe und den der Entnahme bezeichnen. class Queue { private: unsigned m; TR * a; unsigned n; int in; int out; // // // // // // maximale Länge des Arrays Zeiger auf den Array aktuelle Anzahl Queue-Elemente Index des nächsten freien Arrayplatzes Index des nächsten zu entnehmenden Arrayplatzes }; Der Konstruktor Queue(max) reserviert Speicherplatz für einen Array der maximalen Länge max Records vom Typ TR und speichert die Adresse des Arrays in der Zeigervariablen a. Außerdem werden die Variable n, in und out initialisiert. // Konstruktor: legt Speicherplatz für max Records an template <class TR> Queue<TR>::Queue(unsigned max):m(max) { a=new TR[m]; n=in=out=0; } Das Ablegen eines Records setzt voraus, dass der Array nicht voll ist. Für eine volle Schlange gilt n=m. Die Boolesche Elementfunktion IsFull() hat den Wert true, wenn der Array voll ist, sonst ist er false. // prüfen, ob die Schlange voll ist template <class TR> bool Queue<TR>::IsFull() { if (n==m) return true; else return false; } Entsprechend hat IsEmpty() den Wert true, wenn die Schlange leer ist. Für eine leere Schlange gilt n=0. // prüfen, ob die Schlange leer ist template <class TR> bool Queue<TR>::IsEmpty() { if (n==0) return true; else return false ; } 4.5 Implementation des ADT Queue 163 Die Elementfunktion Enqueue(r) fügt den Record in der Variablen r am Ende einer Schlange ein. Die Schlange darf nicht voll sein. Der nächste freie Platz am Ende der Schlange hat den Index in. Anschließend wird in um eins erhöht und der neue Wert modulo m betrachtet. Die Modulo-Funktion ist wegen des zirkularen Additionsschritts (m-1)+1=0 notwendig. Außerdem wird die Variable n um eins erhöht. // am Ende der Schlange ablegen template <class TR> void Queue<TR>::Enqueue(TR &r) { if (IsFull()) return; a[in]=r; // Record ablegen n++; // Anzahl Queue-Elemente um 1 erhöhen in=(in+1)%m; // nächster freier Platz im Array } Die Elementfunktion Dequeue() holt den Record am Anfang der Schlange. Die Schlange darf nicht leer sein. Das Element am Anfang der Schlange hat den Index out. Anschließend wird out um 1 erhöht und der neue Wert modulo m betrachtet. Die Modulo-Funktion ist wegen des zirkularen Additionsschritts (m-1)+1=0 notwendig. Außerdem wird die Variable n um eins vermindert. Ist die Schlange leer, wird ein initialer Record zurückgegeben. // vom Anfang der Schlange holen template <class TR> TR Queue<TR>::Dequeue() { TR r; if (IsEmpty()) return r; r=a[out]; // Record holen n--; // Anzahl Queue-Elemente um 1 vermindern out=(out+1)%m; // nächster Platz für die Ausgabe return r; } Die Elementfunktion Front(r) liefert den Wert des ersten Records, ohne ihn jedoch aus der Schlange zu entfernen. // den Wert des zuerst gespeicherten Records holen template <class TR> TR Queue<TR>::Front() { TR r; if (IsEmpty()) return r; else return r=a[out]; } 164 4 Stapel und Schlangen 4.5.2 Implementation als gekettete Liste In der folgenden Implementation realisiert ein Objekt der Klasse Queue eine Schlange, deren Komponenten Records vom Typ QueueNode sind und die über Zeiger verkettet sind. Eine Schlange wird durch einen Wert der Zeigervariablen q vom Typ QueueNode repräsentiert. Im Fall einer leeren Schlange hat q den Wert NULL, sonst die Adresse eines Listenelements. Ein Objekt vom Typ QueueNode setzt sich aus einem Wert des generischen Datentyps TR und einem Wert vom Zeigertyp QueueNode zusammen. template <class TR> class Queue { private: class QueueNode // Knoten einer Liste { public: QueueNode(TR r):TRvalue(r),next(NULL){} // Konstruktor für Listenknoten TR TRvalue; // Knotenwert, Typ TR QueueNode *next;// Zeiger auf nächsten Knoten }; QueueNode *q; // Repräsentation der Schlange // Zeiger auf letzten Knoten der Schlange int n; // Anzahl Records in der Schlange }; Bei der Implementation der Schlange als gekettete Liste werden neue Records immer am Listenende eingefügt und Records am Listenanfang entnommen. Das legt die Einführung eines Zeigers für den Anfang der Schlange und die eines weiteren Zeigers für das Schlangenende nahe. Man kann jedoch einen dieser Zeiger einsparen, wenn die gekettete Liste, wie wir dies bei der Arraydarstellung von Schlangen praktiziert haben, zirkular ausgelegt und im letzten Listenrecord anstelle des Zeigerwerts NULL der Zeiger auf den ersten Listenrecord platziert wird. Dann kann mit einem äußeren Zeiger auf das Listenende gleichzeitig auch der Listenanfang bestimmt werden. Im Beispiel der Abbildung 4-12 adressiert q den letzten Record und q->next den ersten Record der Liste. a1 q a2 ... ai ... 1.Knoten (front) Abbildung 4-12: Schlange als zirkular gekettete Liste an-1 an letzter Knoten (rear) 4.5 165 Implementation des ADT Queue Ist eine Schlange leer, gilt: q=NULL NULL q und enthält sie nur einen Record, fallen ihr erster und letzter Record zusammen. Die Bedingung hierfür ist q->next=q x : q TRvalue next Der Konstruktor Queue() generiert eine leere Schlange, für die q=NULL ist: NULL q // Konstruktor: kreiert leere Schlange template <class TR> Queue<TR>::Queue() { q=NULL; n=0; } Für das Einfügen eines neuen Records am Listenende sind zwei Fälle zu unterscheiden: • Einfügen in eine leere Schlange: p x NULL q q TRvalue next QueueNode *p=new QueueNode(x); p->next=p; q=p; • Einfügen in eine nichtleere Schlange p x y q x q QueueNode *p=new QueueNode(z); p->next=q->next; q->next=p; q=p; Die beiden Fälle fasst die Elementfunktion Enqueue zusammen: // am Queueende einfügen y z 166 4 Stapel und Schlangen template <class TR>; void Queue<TR>::Enqueue(TR r) { QueueNode *p=new QueueNode(r); if (q==NULL) p->next=p; else { p->next=q->next; q->next=p; } q=p; n++; } Aus einer Schlange kann nur entnommen werden, wenn sie noch mindestens einen Record enthält. Nach der Entnahme kann eine Schlange leer sein oder sie enthält noch mindestens einen weiteren Record. Diese beiden Fälle müssen im Algorithmus für die DequeueOperation unterschieden werden. • Die Schlange ist nach einer Entnahme leer. Die Bedingung dafür ist q=q->next. p NULL x q q TRvalue next QueueNode *p=q->next; TR r=p->TRvalue; q=NULL; delete p; • Die Schlange ist nach einer Entnahme nicht leer. p x y q z y q QueueNode *p=q->next; TR r=p->TRvalue; q->next=p->next; delete p; Die beiden Fälle sind in der Elementfunktion Dequeue zusammengefasst: template <class TR> z 4.6 Anwendungen von Schlangen 167 TR Queue<TR>::Dequeue() { QueueNode *p =q->next; TR r=p->TRvalue; if (p==q) q=NULL; else q->next=p->next; delete p; n--; return r; } Die Elementfunktionen IsEmpty und IsFull lauten: // prüfen, ob die Schlange leer ist template <class TR> bool Queue<TR>::IsEmpty() { if (q==NULL)return true; else return false; } // prüfen, ob die Schlange voll ist template <class TR> bool Queue<TR>::IsFull() { return false;// Schlange kann nicht voll werden } 4.6 Anwendungen von Schlangen Es folgen noch einige Beispiele, die unter Aspekten der Anwendung äußerst interessant sind und die Nützlichkeit von Stapel und Schlangen für die verschiedenartigsten Aufgaben veranschaulichen. Sie sollen außerdem demonstrieren, dass auch im Fall komplexerer Aufgabenstellungen die Programme bei Verwendung abstrakter Datentypen recht kurz und sehr übersichtlich werden. 4.6.1 Radix Sortieren Die allein auf dem Schlüsselvergleich beruhenden allgemeinen Sortierverfahren für Arrays haben einen Mindestzeitbedarf von Ω(n·log n). Radix Sortieren vergleicht ziffernweise und nutzt beim Sortieren die Darstellung und Länge der Schlüsselwerte aus. Dadurch lässt sich eine Zeitverbesserung bis hin zu O(n) erzielen. Nachteilig ist jedoch, dass zusätzlicher 168 4 Stapel und Schlangen Speicherplatz benötigt wird. Das Radix Sortieren eignet sich, wenn die Schlüsselwerte Ganzzahlen oder kurze Strings sind. Radix Sortieren greift auf eine Methode zurück, die auf früheren Lochkartensortiermaschinen benutzt wurde. Die Schlüsselwerte werden als Zahlen gleicher Länge l zur Basis r aufgefasst. Im Beispiel dreistelliger Ganzzahlschlüssel ist l=3 und r=10 und für alphabetische Schlüssel der Länge 5 ist l=5 und r=26. Die Methode kombiniert in einem Durchlauf zwei Schrittfolgen: zunächst werden die Records auf r „Fächer“ (buckets) verteilt und dann werden sie in bestimmter Weise wieder eingesammelt. Die Anzahl der Fächer entspricht der Anzahl der zur Basis r gegebenen Ziffern. In einem Durchlauf werden die Records so verteilt, dass sich in einem Fach Records mit der gleichen Ziffer in einer bestimmten Schlüsselposition sammeln. In einem Fach hinzukommende Records werden den bereits vorhandenen angefügt. Beim Einsammeln werden die Records fachweise, beginnend mit dem Fach für die niedrigste und endend mit dem Fach für die höchste Ziffer, zusammengefügt. Die relative Anordnung der Records innerhalb eines Fachs bleibt dabei erhalten. Für jede Ziffernposition des Schlüssels ist ein kompletter Durchlauf erforderlich. Das sind l Durchläufe, beginnend an der niedrigsten Schlüsselposition, dann Stelle für Stelle nach links fortschreitend, bis nach der letzten Verteilung und der Wiedereinsammlung der Records für die höchste Position des Schlüssels die Records sortiert sind. Wir verfolgen das Verfahren am Beispiel einer Folge von Records mit den Schlüsselwerten 433,115,648,340,161,337,119,430,983,224,226. Die Schlüsselwerte sind Dezimalzahlen. Die Records werden in dieser Reihenfolge nacheinander auf 10 Fächer verteilt. Jedes Fach ist einer bestimmten Dezimalziffer zugeordnet, in dem Records mit dieser Ziffer in einer bestimmten Position des Schlüssels gesammelt werden. Beim ersten Durchlauf werden die Records in Abhängigkeit der Endziffer ihres Schlüssels verteilt. Abbildung 4-13 stellt diese Verteilung dar. Fach 0 1 340 430 161 2 3 4 5 6 7 8 9 433 983 224 115 226 337 648 119 Abbildung 4-13: Radix Sortieren, Fachverteilung 1. Durchlauf Man sieht, dass die Records in einem Fach dieselbe relative Ordnung wie in der originalen Folge einhalten. Das anschließende Einsammeln der Records, bei der die Records zunächst der Reihe nach aus dem Fach 0, dann aus dem Fach 1 usw. und schließlich aus dem Fach 9 genommen werden, führt auf die Anordnung 340,430,161,433,983,224,115,226,337,648,119. Die Records sind nach der niedrigsten Schlüsselposition aufsteigend sortiert und werden im nächsten Durchlauf nach der mittleren Schlüsselposition sortiert. Dabei bleibt die Sortierordnung nach der niedrigsten Stelle erhalten. Entsprechendes gilt für die weiteren 4.6 Anwendungen von Schlangen 169 Durchläufe einer Sortierung. Das kann man sich leicht durch folgende Überlegung klar machen: Gelangen nämlich bei einer Verteilung für die Ziffernposition i zwei Records in dasselbe Fach, so bilden sie dort bezüglich der Ziffernpositionen von 0 bis i eine sortierte Folge, da der erste dieser Records in der durch den letzten Durchlauf (Ziffernposition i-1) hergestellten Ordnung dem zweiten Record vorausgeht. Abbildung 4-14 zeigt die weiteren Durchläufe des Radix Sortierens für unser Beispiel. Fachverteilung 2. Durchlauf: 0 1 2 3 115 119 224 226 430 433 337 Fach 4 5 340 648 6 7 161 8 9 983 Sammelphase 2. Durchlauf: 115, 119, 224, 226, 430, 433, 337, 340, 648, 161, 983, Fachverteilung 3. Durchlauf 0 1 2 3 Fach 4 115 119 161 224 226 337 340 430 433 5 6 7 648 8 9 983 Sammelphase 3. Durchlauf 115,119,161,224,226,337,340,430,433,648,983 Abbildung 4-14: Radix Sortieren, 2. und 3. Durchlauf Bei der Verteilung werden Records in einem Fach am Ende eingefügt und beim anschließenden Einsammeln vom Anfang her geholt. Das legt für die Fächer die Struktur der Schlange nahe. Wir wählen ihre gekettete Form und sehen eine Schlange für jedes Fach vor. Ihre Darstellung als Array wäre unzweckmäßig, da bei einer Verteilung möglicherweise alle Records in ein einziges Fach gelangen und alle Schlangen somit die Länge der zu sortierenden Recordfolge haben müssten. Für die Verwaltung der Schlangen führen wir aber einen Array ein, dessen Komponenten Zeiger auf die einzelnen Schlangen (Fächer) sind. Wir nehmen an, dass die zu sortierende Recordfolge bereits als gekettete Schlange vorliegt. Die Records werden auf die Fächer verteilt und anschließend in dieser Schlange wieder aufgereiht. Die Schrittfolge wird wiederholt, bis die Records in der Schlange sortiert sind. Wir demonstrieren für unser obiges Beispiel den ersten Durchlauf für diese Implementation. Die anfängliche Recordfolge ist: 170 4 Stapel und Schlangen 433 115 430 983 648 224 340 337 161 119 226 Zu Beginn sind die Schlangen für die Fachverteilung leer. Fach 0 Fach 1 Fach 2 Fach 3 Fach 4 Fach 5 Fach 6 Fach 7 Fach 8 Fach 9 NULL NULL NULL NULL NULL NULL NULL NULL NULL NULL Die Entwicklung der Schlangen nach der Verteilungsphase und die in der niedrigsten Position des Schlüssels geordnete Recordfolge nach der Sammelphase zeigt Abbildung. 4-15. NULL 340 161 433 430 224 115 226 337 648 119 983 340 226 430 337 161 648 433 983 224 115 119 Abbildung 4-15: Radix Sortieren mit Schlangen, 1. Durchlauf Der folgende Algorithmus RadixSort sortiert die in einer geketteten Schlange a gegebene Folge von Records. Jeder Record hat einen ganzzahligen Schlüssel key, der als r-adische Zahl der Länge l aufgefasst wird. r und l sind fest gegeben. Die niedrigste Position des Schlüssels ist key[l-1], die höchste key[0]. Die die Fächer repräsentierenden Schlangen werden im Array b der Länge r verwaltet. Zu Beginn einer Verteilung sind alle Schlangen leer. Während einer Verteilung werden die Records nacheinander aus der Schlange a entnommen und in Abhängigkeit der Ziffer digit in der durch den Durchlauf bestimmten Position des Schlüssels in die Schlange b[digit], 0≤digit<r, eingefügt. Am Ende der Verteilung ist die Schlange a leer. In der Sammelphase werden die Schlangen b[digit] der Reihe nach 4.6 Anwendungen von Schlangen 171 in der anfangs leeren Schlange a aufgereiht. Die anfängliche Folge der Records in a ist nach l Durchläufen sortiert. Die sortierte Folge steht in a. Die zu sortierenden Records seien Objekte der Klasse Record und die Klasse enthalte eine Elementfunktion GetKey(), die den Schlüssel eines Records liefert. void RadixSort(Queue<Record> &a,unsigned r,unsigned l) { Queue<Record> *b=new Queue<Record>[r]; unsigned div=1,key,digit,j; Record x; for (unsigned i=1; i<=l; i++) // Verteilungsphase { while (!a.IsEmpty()) { x=a.Dequeue(); // nächster Record aus a key=x.GetKey(); // Schlüssel dieses Records key/=div; // Sortierziffer an letzter Stelle digit=key%r; // Sortierziffer abspalten b[digit].Enqueue(x); // Record in Fach ablegen } div*=r; // div=r,r*r,... for (j=0; j<r; j++) // Sammelphase { if (!(b[j].IsEmpty())) a.Concate(b[j]); b[j].Delete(); } } } Die Funktion RadixSort ruft in der Sammelphase die Elementfunktion Concate der Klasse Queue auf, die zwei Schlangen miteinander verkettet. Für die Implementation sehen wir die Schlangen als zirkular gekettete Listen (Abbildung 4-12) vor. template <class TR> void Queue<TR>::Concate(Queue<TR> &p) { if (q==NULL) q=p.q; // 1. Schlange leer else { QueueNode *p0=q->next; // 1. Element der 1. Schlange sichern q->next=p.q->next; // 1. Element der 2. Schlange an // das Ende der 1. Schlange ketten 172 4 Stapel und Schlangen q=p.q; // q zeigt auf letztes Element der 2. Schlange q->next=p0; // letztes Element der 2. Schlange zeigt // auf das 1. Element der 1. Schlange } n+=p.n; // Anzahl der Elemente aktualisieren } Die Analyse des Algorithmus RadixSort führt auf die Laufzeitkomplexität O(l·(n+r)). Die charakteristischen Operationen beim Verteilen sind Dequeue und Enqueue, die einen Record der Schlange a entnehmen und in eine Schlange b einfügen. Bei gegebenen n Records sind während einer Verteilung je n solcher Operationen, jede in O(1), auszuführen. Die charakteristische Operation der Sammelphase ist die Aneinanderreihung von zwei Schlangen, die durch die Operation Concate erledigt wird. Notwendig sind r solcher Operationen, jede in O(1). Die Laufzeitkomplexität pro Durchlauf ist somit O(n+r) und O(l·(n+r)) bei l Durchläufen. Sind, wie in unserem Beispiel, r und l fest gegeben, ist die Laufzeitkomplexität linear in O(n). RadixSort zeigt jedoch enttäuschende Ergebnisse für kleine n. In solchen Fällen überwiegt der von n unabhängige und auf die Verwaltung der Schlangenorganisation fallende Aufwandsanteil O(l·r). 4.6.2 Permutationen Die gegebenen Elemente einer Menge können auf mehrere Arten angeordnet werden. Ein einzelnes Element hat eine einzige Anordnung, zwei Elemente a und b haben die Anordnungen ab und ba und drei Elemente, a,b und c, die sechs Anordnungen bca, cba, acb, cab, abc und bac, usw. Die verschiedenen Anordnungen von n Elementen einer Menge heißen ihre Permutationen. Ihre Anzahl ist das Produkt aller Zahlen von 1 bis n, also n!. Man erhält diese Anordnungen, indem man jedes der n Elemente an die letzte Stelle bringt und dann die übrigen (n-1) Elemente in je (n-1)! Anordnungen davorsetzt. Das hier behandelte rekursive Programm für die Bestimmung der Permutationen einer Menge von Elementen wird durch eine Schlange und einen Stapel unterstützt. Der Einfachheit halber sollen die Elemente einzelne Zeichen vom Typ char sein, und wir nehmen an, dass sie auch bereits als Objekte einer Schlange q gegeben sind. Das Programm verläuft in Zyklen. Zu Beginn eines Zyklus wird das erste Element aus der Schlange geholt und in den leeren Stapel s gebracht; für dieses Element werden dann innerhalb des Zyklus alle Permutationen über die restlichen (n-1) Elemente in der Schlange ermittelt. Für die Gewinnung einer Permutation sind (n-1) Rekursionsschritte erforderlich, wobei immer ein Element aus der Schlange in den Stapel übertragen wird. Ist die Schlange leer, wird die Permutation ausgegeben und die vorausgegangenen und noch nicht beendeten (n-1) Rekursionssschritte werden nacheinander wieder aufgenommen und bringen jeweils das zuoberst 4.6 Anwendungen von Schlangen 173 im Stapel liegenden Elemente zurück in die Schlange, jetzt aber in umgekehrter Folge. Dieser Ablauf innerhalb eines Zyklus wiederholt sich insgesamt (n-1) Mal. Am Schluß des Zyklus wird auch das ausgewählte Element aus dem Stapel genommen und als letztes Element in die Schlange gebracht. Da auch die übrigen Elemente in der Schlange zyklisch vertauscht wurden, beginnt der nächste Zyklus wieder mit der Übertragung des ersten Schlangenelements in den leeren Stapel. void Permutation(Queue<character> &q,Stack<character> &s) { static int nperm=0; if (q.IsEmpty()) { // Schlange leer nperm++; cout << " (" << nperm << ")"; s.StackOut(); // Permutation ausgeben } else { for (unsigned i=1;i<=q.Length();i++) { // i-ter Rekursionsschritt s.Push(q.Dequeue()); // von der Schlange in den Stapel Permutation(q,s); q.Enqueue(s.Pop()); // vom Stapel in die Schlange } } } FunktionsAktivierung Schlange q Stapel s Beginn (1) (2) (3) (4) (3) (2) (5) (6) (7) (6) (5) (1) abc bc c leer leer c cb b leer leer b bc bca leer a ba cba cba ba a ca bca bca ca a leer Permutation cba bca Abbildung 4-16: Protokoll des Programms Permutation für 3 Elemente 174 4 Stapel und Schlangen Wir illustrieren den Ablauf am Beispiel der Elemente a, b und c. Das Protokoll veranschaulicht die Belegung der Schlange q und des Stapels s für die einzelnen Aktivierungen der Funktion im Zyklus für das Element a. 4.7 Stapel und Schlangen mit Freiplatzverwaltung Die nur zeitweilige Speicherung von Records in Schlangen und Stapel führt zu einer wenig ökonomischen Speicherausnutzung, wenn jede der Operationen Push bzw. Enqueue Speicherplatz vom System anfordert und jede der Operationen Pop bzw. Dequeue frei gewordenen Speicher an das System zurückgibt. Ein Recordbereich z.B. eines Stapels, der durch Pop an die Speicherverwaltung zurückgegeben wird, wird in genau derselben Größe von der nächsten Push-Operation für den Stapel wieder angefordert. Effizienter ist es, wenn frei gewordene Recordbereiche nicht an das System zurückgegeben, sondern in einer eigenen Freiplatzliste auf Vorrat gehalten und bei Bedarf wieder verwendet werden. Zusätzlicher Speicher muss nur noch dann angefordert werden, wenn die Freiplatzliste leer ist. Das folgende Beispiel illustriert eine Schlange mit Freiplatzverwaltung. Die Freiplatzliste könnte als Stapel oder Schlange realisiert werden, wir wählen den Stapel als die einfachere der beiden Strukturen. Die Recordstruktur ist in beiden Listen, der Schlange und der Freiplatzliste, dieselbe. Ein Record wird nach seiner Entnahme aus der Schlange in die Freiplatzliste gestellt und beim Einfügen in die Schlange wird der nächste Record aus der Freiplatzliste genommen. Ein Record verändert nicht seine Position im Speicher, beim „Umspeichern“ werden lediglich Zeigerwerte verändert. Ein Record ist entweder mit aktuellen Daten belegt und gehört dann zur Schlange, oder er ist bereit für die Wiederverwendung und steht dann in der Freiplatzliste. In den folgenden Programmstücken wird die Schlange durch den Zeigerwert der Variablen q und die Freiplatzliste durch den der Variablen s repäsentiert. Die Recordstruktur sieht das Feld TRvalue für den zu speichernden Auftrag und das Zeigerfeld next für die Kettung der Records in der Schlange bzw. in der Freiplatzliste vor: class QueueNode { public: QueueNode() : next(NULL) { private: TR TRvalue; QueueNode * next; } } Wir betrachten als Erstes das Einfügen eines neuen Auftrags in die Schlange. Dazu wird der nächste Record p aus der Freiplatzliste geholt. Ist sie leer, muss ein neuer Record vom System angefordert werden. Abbildung 4-17 beschreibt den Ablauf vor und nach einer Einfügung in die Schlange für den Fall der nichtleeren Freiplatzliste. Das folgende Programmstück beschreibt den Algorithmus für das Einfügen. 4.7 Stapel und Schlangen mit Freiplatzverwaltung 175 QueueNode *p; // Hilfsvariable if (s==NULL) p=new QueueNode; // neuer Record von Speicherverwaltung else { p=s; // Record aus Freiplatzliste s=p->next; // neuer Stack-Anfang } p->TRvalue=r; // speichern neuen Auftrag if (q==NULL) p->next=p; // Einfügen in leere Schlange else { p->next=q->next; // Anhängen am Queue-Ende q->next=p; } q=p; // Zeiger auf Queueende vorher q front rear s top bottom NULL nachher q front s r rear NULL top bottom Abbildung 4-17: Einfügen in eine Schlange mit eigener Speicherplatzverwaltung 176 4 Stapel und Schlangen Das nächste Programmstück liefert den Algorithmus für das Entnehmen des nächsten Auftrags aus einer nicht leeren Schlange. Es wird der Record am Anfang der Schlange entnommen und der damit frei gewordene Record wird an den Anfang der Freiplatzliste gekettet. Abbildung 4-18 beschreibt den Ablauf vor und nach dem Entnehmen. QueueNode *p; TR r; if (q!=NULL) { p=q->next; r=p->TRvalue; if (p==q) q=NULL; else q->next=p->next; p->next=s; s=p; } // Hilfsvariable // // // // // // Record aus Schlange entnommenen Auftrag sichern Schlange nach Entnahme leer neuer Schlangenanfang freien Record in Stapel bringen neuer Stapelanfang vorher q rear front NULL s top bottom nachher r q top front rear NULL s bottom Abbildung 4-18: Entnehmen aus einer Schlange mit Speicherplatzverwaltung 4.8 4.8 Deques 177 Deques Eine Deque (double-ended queue) ist eine Datenstruktur, die das Schlangen- und Stapelprinzip verallgemeinert und zulässt, dass sowohl am Anfang als auch an ihrem Ende Aufträge eingefügt oder entnommen werden können. DeleteFront InsertRear Auftrag1 Auftrag2 ... Auftragn InsertFront DeleteRear front rear Abbildung 4-19: Schema einer Deque Die fundamentalen Operationen sind InsertFront und DeleteFront zum Einfügen und Entnehmen am Anfang einer Deque und InsertRear und DeleteRear zum Einfügen und Entnehmen an ihrem Ende. Eine Deque kann in Anwendungen sowohl als Stapel als auch als Schlange benutzt werden. Deques ermöglichen z.B. eine einfache Vorrangverarbeitung, indem Records an einer Seite entnommen und, falls sie noch nicht an der Reihe sind, an der anderen wieder eingefügt werden. Mittels IsEmpty bzw. IsFull wird geprüft, ob ein weiterer Record entnommen bzw. eingefügt werden kann. Andere Operationen, die der folgende ADT Deque vorsieht, sind Front und Rear. Front liefert den Wert des am Anfang und Rear den Wert des am Ende stehenden Records, ohne dabei den Record selbst zu entfernen. Der Konstruktor Deque generiert eine leere Deque und der Destruktor ∼Deque gibt den belegten Speicher frei. template <class TR> class Deque // Deque für Records vom beliebigen Typ TR, { public: Deque (); // kreiert leere Deque Deque (unsigned m); // kreiert leere Deque für maximal m Records ∼Deque (); // gibt Deque frei void InsertFront(TR r); // fügt Record r am Anfang der Deque ein void InsertRear(TR r); // fügt Record r am Ende der Deque ein TR DeleteFront(); // entnimmt Record am Anfang der Deque // der Record wird aus der Deque entfernt 178 4 Stapel und Schlangen TR DeleteReart(); // entnimmt Record am Ende der Deque // der Record wird aus der Deque entfernt TR Front(); // Wert des Records am Anfang der Deque // der Record verbleibt in der Deque TR Rear(); // Wert des Records am Ende der Deque // der Record verbleibt in der Deque bool IsEmpty(); // true, wenn Deque leer, sonst false bool IsFull(); // true, wenn Deque voll, sonst false }; ADT 4-3: Absrakter Datentyp Deque Als kleine Anwendung des ADT Deque betrachten wir die Editierung eines in einem Eingabepuffer stehenden Textes. Der Text enthält spezielle Zeichen, die durch bestimmte Korrekturmaßnahmen während der Eingabe automatisch in den Text gelangt sind. Als solche Editierungszeichen verwenden wir hier das Zeichen # als Korrekturzeichen und @ als Zeichen für das Entfernen. Beim Auftreten eines Korrekturzeichens wird das vorangegangene Zeichen und beim Auftreten eines Entfernungszeichen werden alle vorausgehenden Zeichen aus dem Text entfernt. So wird z.B. die Textzeile abc#d##e als Zeichenkette ae und die Zeile abc@xy als Zeichenkette xy editiert. Die folgende Funktion verwendet für die Editierung der Textzeile t eine Deque zunächst als Stapel. Der editierte Text wird dann zeichenweise vom Ende der Deque her entnommen und am Bildschirm präsentiert. void Edit(char *t,int n) { Deque() d; // Deque kreieren for (int i=0;i<n;i++) // editieren { if (t[i]==’\0’) break; if (t[i]==’@’) // Löschzeichen while (!d.IsEmpty()) d.DeleteFront(); else if (t=='#') d.DeleteFront(); // Korrekturzeichen if (t[i]=='@' || t[i]=='#') continue; // Korrekturzeichen überlesen d.InserFront(t[i]); // Zeichen stapeln } 4.8 Deques 179 while (!d.IsEmpty()) cout << d.DeleteRear(); // Entnehmen und Ausgeben String vom Ende her } Eine Deque kann als Verbund zweier Stapel, die von zwei Seiten aufeinander zulaufen, aufgefasst werden. Bei der Implementation als Array ist es zweckmäßig, den verfügbaren Arrayspeicher nicht anfänglich fest aufzuteilen, sondern ihn den Stapeln je nach Bedarf frei zuzuordnen. Wir fassen die Operationen InsertFront, DeleteFront und Front als Operationen Push, Pop und Top für einen Stapel Stack1, und InsertRear, DeleteRear und Rear als Operationen Push, Pop und Top für einen Stapel Stack2 auf. Stack1 entwickelt sich im Array von links her und Stack2 entsprechend von rechts her. Stack1 ist die Indexvariable top1 und Stack2 die Indexvariable top2 zugeordnet. Eine Indexvariable bezeichnet die Position der nächsten Eingabe in den betreffenden Stapel. top1 wird mit jeder Eingabe um 1 erhöht und mit jeder Ausgabe um 1 vermindert. Entsprechend wird top2 mit jeder Eingabe um 1 vermindert und mit jeder Entnahme um 1 erhöht. Stack1 ist leer, wenn top1 den Wert 0 hat, und entsprechend ist Stack2 leer, wenn top2 den Wert m-1 hat. Solange top1≠top2, kann jede der Operationen InsertFront und InsertRear ausgeführt werden. Die Implementation der Deque als Array ist Thema von Aufgabe 7 in 4.9. 0 1 2 i-1 Stack1 i k ↑ ↑ top1 top2 k+1 m-1 Stack2 Abbildung 4-20: Schema eines Doppelstapels Für die Darstellung der Datenstruktur Deque als gekettete Liste empfiehlt es sich, die Liste wie bei der Schlange zirkular auszulegen. Dann können die Operationen InsertFront, InsertRear und DeleteFront bequem, wie in 4.2.2 besprochen, implementiert werden. Dagegen kann bei der bisher besprochenen Einfachverkettung der Records die Operation DeleteRear nicht mehr so einfach ausgeführt werden. Es wäre dazu notwendig, das Zeigerfeld im Vorgängerrecord, der aber vom Listenende aus nicht erreichbar ist, zu aktualisieren. Eine Lösung liefert die Doppelverkettung der Records, die später besprochen wird. a1 q a2 ... ai 1.Knoten (front) Abbildung 4-21: Die Deque als zirkulare Liste ... an-1 an letzter Knoten (rear) 180 4 Stapel und Schlangen 4.9 Aufgaben 1. Der Ausdruck in Postfixnotation 15 13 5 2 / + · 2 3 / + ist auszuwerten. 2. Schreiben Sie als Anwendung des ADT Stack eine Boolesche Funktion, die prüft, ob ein gegebener Ausdruck ein korrekter Klammerausdruck ist. In einem korrekten Klammerausdruck muss für jede schließende Klammer vorher eine öffnende Klammer stehen. 3. Schreiben Sie für den als Entscheidungstabelle in Abbildung 4-7 gegebenen Algorithmus eine Funktion. 4. Schreiben Sie als Anwendung des ADT Queue eine Funktion, die zwei sortierte Queues in eine Einzige mischt. 5. Eine Schlange kann unter Verwendung von zwei Stapeln, zwischen denen die gespeicherten Daten hin und her transportiert werden, implementiert werden. Sehen Sie den ADT Stack als gegeben an und leiten Sie daraus die Implementation des ADT Queue her. 6. Implementieren Sie den ADT Stack als verkettete Liste mit Freiplatzverwaltung. 7. Implementieren Sie den ADT Deque für eine Arraystruktur. 8. Ist Radix Sortieren ein stabiles Sortierverfahren ? 9. Mit Hilfe eines Stapels kann ein gegebener rekursiver Algorithmus in ein iteratives Programm aufgelöst werden. Da Rechner prinzipiell nicht über rekursive Fähigkeiten verfügen, ist dies eine grundlegende Aufgabe eines jeden Compilers, sie stellt sich auch für Sprachen, die die Rekursion nicht unterstützen. Die Lösung besteht darin, dass das Programm für jeden rekursiven Aufruf einen Sicherungsrecord mit den Werten der lokalen Variablen generiert und auf Stapel legt. Anschließend werden die gesicherten Daten nacheinander für ihre Verarbeitung vom Stapel geholt. Ein Programm terminiert, wenn der Stapel leer ist. a) Man transformiere den in 1.5.1 behandelten rekursiven Algorithmus für die Darstellung einer Ganzzahl x als Folge ihrer Ziffernzeichen in ein iteratives Programm. b) Verfahren Sie ebenso für das in 1.5.1 rekursiv behandelte Türme-von-HanoiProblem.