Algorithmen und Datenstrukturen
Werner Struckmann
Wintersemester 2005/06
4. Listen und abstrakte Datentypen
4.1 Abstrakte Datentypen 4.2 Listen
4.3 Keller
4.4 Schlangen
Datentypen
Unter einem Datentyp versteht man die Zusammenfassung von Wertebereichen und Operationen zu einer Einheit.
◮ Abstrakter Datentyp: Schwerpunkt liegt auf den
Eigenschaften, die die Wertebereiche und Operationen besitzen.
◮ Konkreter Datentyp: Realisierung der Wertebereiche und Operationen stehen im Vordergrund.
◮ Primitive Datentypen: bool, char, int, real, . . .
Datentypen
Komplexe Datentypen, sog. Datenstrukturen, werden durch
Kombination primitiver Datentypen gebildet. Sie besitzen selbst spezifische Operationen. Datenstrukturen können vorgegeben oder selbstdefiniert sein.
Dabei wird über das Anwendungsspektrum unterschieden in
◮ Generische Datentypen: Werden für eine Gruppe ähnlicher Problemstellungen entworfen und sind oft im Sprachumfang bzw. der Bibliothek einer Programmiersprache enthalten (Feld, Liste, Keller, Schlange, Verzeichnis, . . . ).
◮ Spezifische Datentypen: Dienen der Lösung einer eng umschriebenen Problemstellung und werden im
Zusamenhang mit einem konkreten Problem definiert.
Entwurfsprinzipien für Datentypen
Anforderungen an die Definition eines Datentyps:
◮ Die Spezifikation eines Datentyps sollte unabhängig von seiner Implementierung erfolgen. Dadurch kann die
Spezifikation für unterschiedliche Implementierungen verwendet werden.
◮ Reduzierung der von außen sichtbaren (zugänglichen)
Aspekte auf die Schnittstelle des Datentyps. Dadurch kann die Implementierung später verändert werden, ohne dass Programmteile, die den Datentyp benutzen, angepasst werden müssen.
Entwurfsprinzipien für Datentypen
Aus diesen Anforderungen heraus ergeben sich zwei Prinzipien:
◮ Kapselung (encapsulation): Alle Zugriffe geschehen immer nur über die Schnittstelle des Datentyps.
◮ Geheimnisprinzip (programming by contract): Die interne Realisierung des Datentyps bleibt dem Benutzer verborgen.
Abstrakte Datentypen
Informatik-Duden: Ein Datentyp, von dem nur die Spezifikation und Eigenschaften (in Form von zum Beispiel Regeln oder
Gesetzmäßigkeiten) bekannt sind, heißt abstrakt. Man abstrahiert hierbei von der konkreten Implementierung.
Dies kann für
◮ eine klarere Darstellung,
◮ für den Nachweis der Korrektheit oder
◮ für Komplexitätsuntersuchungen von Vorteil sein.
Ein abstrakter Datentyp wird kurz als ADT bezeichnet.
Ein ADT wird ohne Kenntnis der internen Realisierung verwendet (Geheimnisprinzip). Dabei wird nur von der Schnittstelle
(Kapselung) Gebrauch gemacht.
Abstrakte Datentypen
Wir werden ADTen durch algebraische Spezifikationen beschreiben:
◮ Eine Signatur bildet die Schnittstelle eines ADTs.
◮ Mengen und Funktionen, die zur Signatur „passen“, heißen Algebren.
◮ Axiome schränken die möglichen Algebren ein.
Der Themenkomplex „algebraische Spezifikationen“ wird hier nur einführend behandelt.
Signaturen
Eine Signatur
Σ = (
S,Ω)
besteht aus◮ einer Menge von Sorten S und
◮ einer Menge von Operatorsymbolen
Ω
.Jedes Operatorsymbol f
:
s1 . . . sn → s besteht aus einem Namen f, einer Folge s1, . . . , sn ∈ S, n ≥ 0, vonArgumentsorten und einer Wertesorte s ∈ S.
Operatorsymbole ohne Parameter heißen Konstante.
Algebren
Es sei eine Signatur
Σ = (
S,Ω)
gegeben. Eine Algebra AΣ= (
AS,AΩ)
zur SignaturΣ
besteht aus◮ den Trägermengen As der Sorten s ∈ S, d. h. AS
=
{As | s ∈ S}, und◮ (partiellen) Funktionen auf den Trägermengen Af
:
As1 × . . . × Asn → As,d. h. AΩ
=
{Af | f:
s1 . . . sn → s ∈Ω
}.Beispiel
Eine Signatur für den ADT „Bool“ sei (vorläufig) gegeben durch:
S
=
{Bool}Ω =
{true:
→ Bool, false:
→ Bool}Mögliche Algebren für diese Spezifikation sind:
ABool
=
{T, F} Atrue ≔ T Afalse ≔ F erwartungskonform ABool=
N Atrue ≔ 1 Afalse ≔ 0 große Trägermenge ABool=
{1} Atrue ≔ 1 Afalse ≔ 1 kleine TrägermengeAxiome
◮ Die Zahl der möglichen Algebren kann durch Axiome eingeschränkt werden.
◮ Axiome sind (hier) Gleichungen, die die Funktionen in ihrer Wirkung einengen.
◮ Eine Menge von Axiomen bezeichnen wir mit
Φ
.Signaturdiagramme
Signaturen lassen sich übersichtlich durch Signaturdiagramme mit Sorten als Knoten und Operatorsymbolen als Kanten darstellen:
sn s0 ...
f
s
Ausblick: Signaturdiagramme sind Beispiele für Graphen, die wir in Kürze betrachten werden.
Notationen für Operatorsymbole
Mit dem Platzhaltersymbol _ für Argumente von Funktionen führen wir die folgenden Notationen ein:
Präfix: f
(
_)
,+ +
_, . . . f(
x)
,+ +
iInfix: _ ≤ _, _
+
_, _ ∨ _, . . . a ≤ b, m+
n, p ∨ q Postfix: _!
, _2, . . . n!
, x2Mixfix: |_|, if_then_else_fi, . . . |x|
Bei der Präfixnotation schreiben wir auch kurz f.
ADT der Wahrheitswerte
S
=
{Bool}Ω =
{true:
→ Bool, false:
→ Bool,¬_
:
Bool → Bool,_ ∨ _
:
Bool × Bool → Bool, _ ∧ _:
Bool × Bool → Bool}Φ =
{x ∧ false=
false ∧ x=
false, x ∧ true=
true ∧ x=
x,x ∨ true
=
true ∨ x=
true, false ∨ false=
false,¬false
=
true, ¬true=
false}Bool true
false
∨,∧
¬
ADT der natürlichen Zahlen
S
=
{Nat}Ω =
{0:
→ Nat,succ
:
Nat → Nat}Φ =
{}Nat
0 succ
◮ Damit wird z. B. die Zahl 3 als
succ
(
succ(
succ(
0))) =
succ3(
0)
dargestellt.◮ Der Term succn
(
0)
stellt die natürliche Zahl n dar. Da es keine Axiome gibt, kann dieser Term nicht vereinfacht werden.ADT der natürlichen Zahlen
S
=
{Nat}Ω =
{0:
→ Nat,succ
:
Nat → Nat,add
:
Nat × Nat → Nat}Φ =
{add(
x, 0) =
x,add
(
x, succ(
y)) =
succ(
add(
x, y))
}Nat
0 succ
add
Dies ist eine formale Spezifikation der natürlichen Zahlen mit der Konstanten 0, der Nachfolgerfunktion und der Addition.
Implementierungen sind nicht verpflichtet, die Operationen gemäß der Axiome zu realisieren. Sie müssen lediglich das durch die
Axiome beschriebene Verhalten gewährleisten.
Beispiel
Es soll 2
+
3 berechnet werden.2
+
3=
add(
succ(
succ(
0))
,succ(
succ(
succ(
0))))
=
succ(
add(
succ(
succ(
0))
, succ(
succ(
0))))
=
succ(
succ(
add(
succ(
succ(
0))
,succ(
0))))
=
succ(
succ(
succ(
add(
succ(
succ(
0))
,0))))
=
succ(
succ(
succ(
succ(
succ(
0)))))
Der ADT Nat erfüllt eine besondere Eigenschaft: Jeder Term
besitzt eine eindeutige Normalform succn
(
0)
. Diese entsteht, wenn man die Gleichungen von links nach rechts anwendet, bis alleadd-Operationssymbole verschwunden sind.
Implementierung eines abstrakten Datentyps
Implementierung eines ADTs heißt:
◮ Realisierung der Sorten s ∈ S durch Datenstrukturen As Beispiel: Nat { Bm (m-stellige Vektoren über {0, 1}
)
◮ Realisierung der Operatoren f
:
s1 . . . sn → s durch Funktionen Af:
As1 × . . . × Asn → AsBeispiel: add
:
Nat × Nat → Nat { _+
_:
Bm × Bm → Bm◮ Sicherstellen, dass die Axiome (in den Grenzen der
darstellbaren Werte bzw. der Darstellungsgenauigkeit) gelten.
Implementierung eines abstrakten Datentyps
Beispiel: ANat
=
BmDarstellung von x ∈ N mit m Binärziffern zm−1, . . . ,z0 ∈ B:
x
=
mX−1 i=0
zi · 2i
Darstellbarer Zahlenbereich: 0 ≤ x ≤ 2m − 1
Die Gültigkeit der Rechengesetze muss gewährleistet sein.
Alternative Notation
Im Folgenden wird alternativ zur mathematischen Schreibweise folgende an Programmiersprachen angelehnte Notation genutzt:
S
=
{Nat}Ω =
{0:
→ Nat,succ
:
Nat → Nat,add
:
Nat × Nat → Nat}Φ =
{add(
x, 0) =
x,add
(
x, succ(
y)) =
succ(
add(
x, y))
}type
Natimport
∅operators
0
:
→ Natsucc
:
Nat → Natadd
:
Nat × Nat → Nataxioms
∀i,j ∈ Natadd
(
i, 0) =
iadd
(
i, succ(
j)) =
succ(
add(
i, j))
Algebraische Spezifikationen
Eine Import-Anweisung erlaubt die Angabe der Sorten, die zusätzlich zur zu definierenden Sorte benötigt werden.
Eine algebraische Spezifikation eines ADTs besteht aus
◮ einer Signatur und
◮ aus Axiomen und ggf. zusätzlich
◮ aus Import-Anweisungen.
Eine Algebra, die die Axiome erfüllt, heißt Modell der Spezifikation.
Auf die Frage nach der Existenz und Eindeutigkeit von Modellen können wir hier nicht eingehen.
Lineare Datentypen
◮ In diesem Kapitel besprechen wir die linearen Datentypen Liste, Keller und Schlange.
◮ Nichtlineare Datentypen sind zum Beispiel Bäume und Graphen, die später behandelt werden.
◮ In vielen Programmiersprachen sind lineare Datentypen und ihre grundlegenden Operationen Bestandteil der Sprache oder in Bibliotheken verfügbar.
◮ Listen spielen in der funktionalen Programmierung eine große Rolle.
Listen
◮ Eine (lineare) Liste ist eine Folge von Elementen eines gegebenen Datentyps.
◮ Es können jederzeit Elemente in eine Liste eingefügt oder Elemente aus einer Liste gelöscht werden.
◮ Der Speicherbedarf einer Liste ist daher dynamisch, d. h., er steht nicht zur Übersetzungszeit fest, sondern kann sich noch während der Laufzeit ändern.
◮ Listen und ihre Varianten sind die wichtigsten dynamischen Datenstrukturen überhaupt.
xn
x1 x2 x3 . . .
Typische Operationen für Listen
◮ Erzeugen einer leeren Liste
◮ Testen, ob eine Liste leer ist
◮ Einfügen eines Elements am Anfang/Ende einer Liste
◮ Löschen eines Elements am Anfang/Ende einer Liste
◮ Rückgabe des ersten/letzten Elements einer Liste
◮ Bestimmen von Teillisten, zum Beispiel: Liste ohne Anfangselement
◮ Testen, ob ein gegebener Wert in einer Liste enthalten ist
◮ Berechnen der Länge einer Liste
◮ Bestimmen des Vorgängers/Nachfolgers eines Listenelements
Parametrisierte Datentypen
◮ Die im Folgenden eingeführten abstrakten Datentypen sind parametrisiert.
◮ Die Spezifikation eines abstrakten Datentyps kann ein oder mehrere Sortenparameter enthalten, die unterschiedlich instanziiert werden können.
◮ Beispiel: In der Spezifikation der Listen tritt der Parameter T auf. List
(
T)
kann dann beispielsweise durch List(
Bool)
,List
(
Nat)
oder List(
List(
Nat))
instanziiert werden.Listen
T Liste Nat
head length
: tail
[ ]
type
List(
T) import
Natoperators
[] :
→ List_
:
_:
T × List → List head:
List → Ttail
:
List → List length:
List → Nataxioms
∀l ∈ List,∀x ∈ T head(
x:
l) =
xtail
(
x:
l) =
l length([]) =
0length
(
x:
l) =
succ(
length(
l))
Implementierungen
◮ Listen können mithilfe verketteter Strukturen implementiert werden. Hier gibt es viele Varianten: einfache und doppelte Verkettung, Zeiger auf das erste und/oder letzte Element der Liste, zirkuläre Listen, . . .
◮ Alternativ können Listen durch Felder implementiert werden.
Die Methoden sehen dann natürlich anders aus.
Beispielsweise müssen beim Einfügen eines Elements am Anfang der Liste alle anderen Elemente um eine Position nach hinten verschoben werden.
◮ Dynamische Datenstrukturen nutzen den zur Verfügung stehenden Speicherplatz weniger effizient aus. Wenn der benötigte Speicherplatz vor dem Programmlauf genau abgeschätzt werden kann, können statische Strukturen sinnvoll sein.
Implementierungen
Eine Liste kann als Verkettung einzelner Objekte implementiert werden. Man spricht von einer einfach verketteten Liste.
Beispiel: Liste von Namen („Oliver“, „Peter“, „Walter“)
head
Oliver Peter Walter
tail
Die Kästen stellen sogenannte Knoten (engl. node) dar. Jeder Knoten enthält einen Wert vom Typ T und eine Referenz auf den nächsten Knoten. Der letzte Knoten enthält den leeren Verweis.
Bestimmen des ersten Listenelements
func
head(l: Liste): T
beginvar
k: <Referenz auf Knoten>;
k
←<Kopf der Liste l>;
if
k <ungültige Referenz>
then return<keinen Wert>;
fi;
return
k.wert;
end
Aufwand:
Θ(
1)
Einfügen eines Listenelements am Anfang
1. Erzeugen eines neuen Knotens.
2. Referenz des neuen Knotens auf den ehemaligen Kopfknoten setzen.
3. Kopfreferenz der Liste auf den neuen Knoten setzen.
head
Oliver Peter Walter
X
Aufwand:
Θ(
1)
Einfügen eines Listenelements am Anfang
func
addFirst(v: T; l: Liste): Liste
begin vark: <Referenz auf Knoten>;
k
←<neuer Knoten>;
k.wert
←v;
k.referenz
←<Kopf der Liste l>;
<Kopf der Liste l>
←k;
return
l;
end
Einfügen eines Listenelements am Ende
1. Navigieren zum letzen Knoten.
2. Erzeugen eines neuen Knotens.
3. Einhängen des neuen Knotens.
head
Oliver Peter Walter
X
Aufwand:
Θ(
n)
Einfügen eines Listenelements am Ende
func
addLast(v: T; l: Liste): Liste
begin vark: <Referenz auf Knoten>;
var
nk: <Referenz auf Knoten>;
k
←<Kopf der Liste l>;
nk
←<neuer Knoten>;
nk.wert
←v;
nk.referenz
←<ungültige Referenz>;
if
k <ungültige Referenz>
then<Kopf der Liste l>
←nk;
returnl;
fi;
while
k.referenz <gültige Referenz>
dok
←k.referenz;
od;
k.referenz
←nk;
returnl;
end
Vorgänger in einfach verketteten Listen
1. Navigieren bis zum Knoten k, dabei Referenz v auf
Vorgängerknoten des aktuell betrachteten Knotens mitführen.
2. Rückgabe des Knoten v. Aufwand:
Θ(
n)
Beispiel: Bestimmung des Vorgängers von „Walter“
head
Oliver Peter Walter
α β γ
Schritt 1 (v=⊥) Betrachteter Knoten: α
Schritt 2 (v=α) Betrachteter Knoten: β
Schritt 3 (v=β) Betrachteter Knoten:γ
Objekt gefunden, R¨uckgabe von v=β
Vorgänger in einfach verketteten Listen
Finden des m-ten Vorgängers eines Knotens k in einer Liste:
1. Navigieren bis zum Knoten k, die Referenz v wird wie vorher mitgeführt, beginnt aber erst am Kopf der Liste, sobald der
(
m+
1)
-te Knoten betrachtet wird.2. Rückgabe von v.
Beispiel: Bestimmung des zweiten Vorgängers von „Walter“
head
Oliver Peter Walter
α β γ
Schritt 1 (v=⊥) Betrachteter Knoten: α
Schritt 2 (v=⊥) Betrachteter Knoten: β
Schritt 3 (v=α) Betrachteter Knoten:γ
Objekt gefunden, R¨uckgabe von v=α
Vorgänger in doppelt verketteten Listen
Alternativ kann man auch die Datenstruktur ändern: Jeder Knoten wird um eine Referenz auf den Vorgängerknoten ergänzt. Die
Suche nach dem m-ten Vorgänger von k erfolgt dann von k aus nach vorne.
head
Oliver Peter Walter
α β γ
Schritt 2 Betrachteter Knoten: α
Schritt 1 Betrachteter Knoten: β
Doppelt verkettete Listen
◮ Der Zugriff auf den Vorgängerknoten wird vereinfacht.
◮ Es wird zusätzlicher Speicherplatz pro Element verbraucht.
◮ Verwaltung des zweiten Zeigers bedeutet zusätzlichen Aufwand.
Beispiel: Löschen eines Knotens head
Oliver Peter Walter
Keller
◮ Ein Keller (stack) ist eine Liste, auf die nur an einem Ende zugegriffen werden kann.
◮ Keller arbeiten nach dem Last-In-First-Out-Prinzip und werden deshalb auch LIFO-Speicher genannt.
x1 x2
x4
x3 x5
x4
Operationen für Keller
◮ Erzeugen eines leeren Kellers (empty)
◮ Testen, ob ein Keller leer ist (empty?)
◮ Rückgabe des ersten Elements eines Kellers (top)
◮ Einfügen eines Elements am Anfang eines Kellers (push)
◮ Löschen eines Elements am Anfang eines Kellers (pop)
Keller dürfen nur mithilfe dieser Operationen bearbeitet werden.
Implementierungen
◮ Realisierung durch eine Liste:
xn . . . x1
top
◮ Realisierung durch ein Feld:
xn x1 x2 x3 . . .
top
Keller
T Stack Bool
top empty?
push pop
empty
Anmerkung:
pop(empty) =⊥ top(empty) =⊥ Diese Fälle bleiben undefiniert.
type
Stack(
T) import
Booloperators
empty
:
→ Stackpush
:
Stack × T → Stack pop:
Stack → Stacktop
:
Stack → Tempty
? :
Stack → Boolaxioms
∀s ∈ Stack,∀x ∈ Tpop
(
push(
s, x)) =
s top(
push(
s,x)) =
x empty?(
empty) =
trueempty
?(
push(
s, x)) =
falseImplementierungen
◮ Es ist sinnvoll, bei der Implementierung von Datenstrukturen auf bereits vorhandene Strukturen zurückzugreifen.
◮ Der abstrakte Datentyp „Keller“ wird durch Rückgriff auf den Datentyp „Liste“ realisiert.
Implementierungen
◮ Die Sorte Stack(T) wird implementiert durch die Menge AList(T) der Listen über T.
◮ Die Operatoren werden durch die folgenden Funktionen implementiert:
empty
= []
push
(
l, x) =
x:
l pop(
x:
l) =
ltop
(
x:
l) =
x empty?([]) =
true empty?(
x:
l) =
falseDie Fehlerfälle pop
(
empty)
und top(
empty)
bleiben unbehandelt.In einer konkreten Realisierung müssen hierfür natürlich
Anwendungen
Keller gehören zu den wichtigsten Datenstrukturen überhaupt. Sie werden zum Beispiel
◮ zur Bearbeitung von Klammerstrukturen,
◮ zur Auswertung von Ausdrücken und
◮ zur Verwaltung von Rekursionen benötigt.
Anwendungsbeispiel: Überprüfung von Klammerstrukturen
◮ Wir werden jetzt an einem konkreten Beispiel erläutern, wie Keller in der Praxis benutzt werden.
◮ Ziel ist es, einen Algorithmus zu entwickeln, der eine Datei daraufhin überprüft, ob die in dieser Datei enthaltenen
Klammern (, ), [, ], { und } korrekt verwendet wurden.
Beispielsweise ist die Folge "( [a] b {c} e )" zulässig, nicht aber
"( ] ]".
Anwendungsbeispiel: Überprüfung von Klammerstrukturen
◮ Es wird ein anfangs leerer Keller erzeugt.
◮ Es werden alle Symbole bis zum Ende der Eingabe gelesen:
◮ Eine öffnende Klammer wird mit push auf den Keller geschrieben.
◮ Bei einer schließenden Klammer passiert folgendes:
◮ Fehler, falls der Keller leer ist.
◮ Sonst wird die Operation pop durchgeführt. Fehler, falls das Symbol, das vom Keller entfernt wurde, nicht mit der
schließenden Klammer übereinstimmt.
◮ Alle anderen Symbole werden überlesen.
◮ Fehler, falls der Keller am Ende der Eingabe nicht leer ist.
◮ Die Eingabe ist zulässig.
Schlangen
◮ Ein Schlange (queue) ist eine Liste, bei der an einem Ende Elemente hinzugefügt und am anderen entfernt werden können.
◮ Schlangen arbeiten nach dem First-In-First-Out-Prinzip und werden deshalb auch FIFO-Speicher genannt.
x2 x3 x4 x5
x5 x1
x1
Operationen für Schlangen
◮ Erzeugen einer leeren Schlange (empty)
◮ Testen, ob eine Schlange leer ist (empty?)
◮ Einfügen eines Elements am Ende einer Schlange (enter)
◮ Löschen eines Elements am Anfang einer Schlange (leave)
◮ Rückgabe des ersten Elements einer Schlange (front)
Schlangen dürfen nur mithilfe dieser Operationen bearbeitet werden.
Implementierungen
◮ Realisierung durch eine Liste:
. . .
Ende Anfang
xn x1
◮ Realisierung durch ein zyklisch verwaltetes Feld:
Ende Anfang
x1 x2 xn . . . x3
- . . .
- - -
Schlangen
T Queue Bool
front empty?
enter leave empty
Anmerkung:
leave(empty) =⊥ front(empty) =⊥ Diese Fälle bleiben undefiniert.
type
Queue(
T) import
Booloperators
empty
:
→ Queueenter
:
Queue × T → Queue leave:
Queue → Queuefront
:
Queue → Tempty
? :
Queue → Boolaxioms
∀q ∈ Queue, ∀x ∈ Tleave
(
enter(
empty,x)) =
empty leave(
enter(
enter(
q,x)
,y)) =
enter
(
leave(
enter(
q,x))
, y)
front(
enter(
empty,x)) =
x front(
enter(
enter(
q, x)
,y)) =
front
(
enter(
q,x))
empty?(
empty) =
trueempty
?(
enter(
q x)) =
falseImplementierungen
Der abstrakte Datentyp „Schlange“ wird ebenfalls durch den Rückgriff auf den abstrakten Datentyp „Liste“ implementiert.
◮ Die Sorte Queue(T) wird implementiert durch die Menge AList(T) der Listen über T.
◮ Die Operatoren werden durch die folgenden Funktionen implementiert:
empty
= []
enter
(
l,x) =
x:
l leave(
x: []) = []
leave
(
x:
l) =
x:
leave(
l)
front
(
x: []) =
xfront
(
x:
l) =
front(
l)
empty?([]) =
true empty?(
x:
l) =
falseAnwendungen
Eine häufige Anwendung sind Algorithmen zur Vergabe von Ressourcen an Verbraucher.
◮ Prozessverwaltung:
◮ Ressource: Rechenzeit
◮ Elemente der Warteschlange: rechenwillige Prozesse
◮ Grundidee: Jeder Prozess darf eine feste Zeit lang rechnen, wird dann unterbrochen und hinten in die Warteschlange wieder eingereiht, falls weiterer Bedarf an Rechenzeit vorhanden ist.
◮ Druckerverwaltung:
◮ Ressource: Drucker
◮ Elemente der Warteschlange: Druckaufträge
◮ Grundidee: Druckaufträge werden nach der Reihenfolge ihres Eintreffens abgearbeitet.
Deques
◮ Eine deque (double-ended queue) ist eine Liste, bei der an beiden Enden Elemente hinzugefügt und entfernt werden können.
◮ Nach den vorangegangenen Beispielen sollte klar sein, welche Operationen eine Deque besitzt und wie diese implementiert werden können.