Datenstrukturen und Algorithmen in C++ - Reß - beck

Werbung
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.
Herunterladen