3.2 Beschränkter Zugriff: Stapel (Stack) und Warteschlangen (Queue)
Im ersten Kapitel lernen wir einige Datenstrukturen kennen, die sich hervorragend eignen, wenn die Daten in einer festen, einfachen Reihenfolge erzeugt, gelesen und entfernt wereden.
Stapel (Stacks)
Ein Stapel ist eine Datenstruktur, der zwei Operationen unterstützt :
push x
: das Element \(x\) wird auf einen Stapel bereits existierender Elemente "obenauf" gelegt.pop
: das oberste Element des Stapels wird entfernt.
Stack mit Hilfe von Arrays
Stack
sollte
die folgenden zwei Funktionen implementieren:
public String pop(); public void push (String x);
Mit new Stack()
sollte ein leerer Stack erstellt werden.
Hinweis. Speichern Sie Ihre Daten in einem Array der Länge \(n\). Führen Sie als Objektvariable eine Zahl \(s\), mit der Sie sich die Anzahl der Elemente in Ihrem Stack merken. Wenn \(s = n\) erreicht ist, ziehen Sie um in ein doppelt so großes Array. Wenn \(s = n/4\) erreicht ist, ziehen Sie um in ein halb so großes Array.
Stack mit Hilfe von verketteten Listen
Node
mit zwei Variablen:
public class Node { Node next; String content; }
Warteschlangen (Queues)
Eine Warteschlange unterstützt die selben Funktionenpush x
und pop
wie
ein Stack, allerdings verhalten die sich anders. Ein push x
fügt x
am Ende der Schlange ein, und pop
entfernt das erste/vorderste
Element
und gibt es zurück.
Queues mit einem Array und zwei Pointern
Eine Queue können wir ganz simpel als zyklisches Array implementieren. Unsere Datenstruktur besteht aus einem Array der Größe \(n\) und zwei Zeigernstart
und end
,
die die Werte \(0,\dots,n-1\) annehmen können. Der Wert start
zeigt auf
den Anfang der Warteschlange (also ganz vorne), und end-1
ist das letzte Element.
Am Anfang gilt start = end
. Das Array ist als Wrap-Around-Array gedacht, Ihre
Warteschlange
kann also folgende Form haben:
Die Operation pop
würde nun start
auf 7 erhöhen und den Buchstaben
m
zurückgeben; push e
würde in Zelle 1 den Buchstaben e
schreiben und end
auf 2 erhöhen. Die Größe der Queue, also die Anzahl der
belegten Zellen, errechnen wir mittels
\(s = {\rm end} - {\rm start}\), falls diese nicht negativ ist, und
\(s = {\rm end} - {\rm strat} + n\), falls \(s < 0\) gilt. Im obigen Beispiel also
\(1 - 6 + 11 = 6\).
Wenn \(s = n-1\) erreicht worden ist, ziehen wir in ein Array der Größe \(2n\) um;
falls \(s\) auf \(n/4\) gefallen ist, ziehen wir in ein Array der Größe \(n/2\) um, wie beim
Stack.
Queues als doppelt verkettete Listen
Alternativ zur Array-Lösung können wir Queues auch mit doppelt verketteten Listen realisieren. Da bei derpop
-Operation das älteste Element entfernt wird,
muss dieses "wissen", welches Element nach ihm eingefügt wurde. Wir brauchen also
auch Pointer von "alt" nach "neu".
DoublyLinkedNode
mit drei
inneren Variablen:
public class DoubleLinkedNode { String content; DoublyLinkedNode before; DoublyLinkedNode behind; }
Nehmen Sie meine Datei QueueDLL.java als Vorlage. Zur Erinnerung können Sie sich meine Implementierung einer (einfach) verketteten Liste anschauen: LinkedList.java
Erinnern Sie sich an Elm. Dort hatten wir ja einen Datentyp
type LinkedList = EmptyNode LinkNode String LinkedListso dass wir mit Hilfe des Datenkonstruktors
LinkNode
Werte verknüpfen konnten, also zum Beispiel
LinkNode "M" (LinkNode "o" (LinkNode "n" (LinkNode "t" (LinkNode "a" (LinkNode "g" EmptyNode)))))Nur, dass es in Elm diesen Datentyp bereits gibt:
List a
mit dem Syntaxzucker,
dass wir statt Datenkonstruktor LinkNode
den Konstruktor ::
mit
Infixschreibweise verwenden dürfen, also
"M" :: ("o" :: ("n" :: ("t" :: ("a" :: ("g" :: [])))))beziehungsweise in noch kompakterer Schreibweise
["M", "o", "n", "t", "a", "g"]Aber lassen Sie sich nicht täuschen: trotz der Schreibweise mit
[...]
handelt
es sich hier um eine verkettete Liste, deren Zellen verstreut im Speicher liegen, und nicht
um ein Array mit random access;
eine Funktion getLastElement: List a -< Maybe a
hätte somit
unvermeidlich Laufzeit \(O(n)\), weil es die ganze Liste durchlaufen müsste, und eben nicht
\(O(1)\) wie bei einem Array.
Queue
implementieren können,
der push x
und pop
in konstanter amortisierter Zeit
unterstützt;
dass heißt, die Laufzeit einer einzelnen pop
-Operation hängt unter Umständen von
der
Länge der Queue ab, eine Folge von \(k\) vielen Operationen braucht aber in jedem Fall
höchstens \(O(k)\) Zeit.
Weitere Beispiele: Listen mit Reinschieben
Stellen Sie sich vor, wir wollen unsere aufgereihten Daten durchgehen können und ohne großen Aufwand der derzeitigen Stelle \(i\) ein neues Element "reinschieben" können, also
Wenn Sie das Prinzip der doppelt verketteten Liste verstanden haben, sollte es für Sie kein großes Problem sein, das in Java zu implementieren. Wie schaut es mit rein funktionalen Sprachen wie Elm aus?
moveLeft
,
moveRight
, currentElement
, writeHere
und insertRight
unterstützt. Die Laufzeit sollte \(O(1)\) amortisiert sein,
d.h., einzelne Operationen dürfen große Laufzeit haben, eine Folge von \(n\) Operationen
muss aber in \(O(n)\) über die Bühne gehen.
moveLeft, moveRight, insertRight
etc.)