Zum Inhalt springen

Deque-Datenstruktur: Was ist ein Deque (Double-ended Queue)?

Sieben Kugeln in Petrol und Creme in einem liegenden Glasrohr, das an beiden Enden offen ist, vor Apricot; an jedem Ende führt ein leuchtender Pfeil eine Kugel hinein und einer eine Kugel hinaus

In diesem Tutorial lernst du die Datenstruktur „Deque“ kennen (ausgesprochen „Deck“):

  • Was ist ein Deque?
  • Welche Operationen bietet ein Deque an – und wie heißen sie in Java?
  • Welche Varianten des Deques gibt es?
  • Was sind die Anwendungsgebiete für Deques?
  • Wie aufwendig sind die Deque-Operationen?

Was ist ein Deque?

Ein Deque ist eine Folge von Elementen, bei der die Elemente sowohl auf der einen als auch auf der anderen Seite eingefügt und entnommen werden können. Deque steht für „Double-ended Queue“, also für eine Queue mit zwei Enden:

Deque-Datenstruktur: eine Reihe von acht Elementen mit Enqueue- und Dequeue-Pfeilen an beiden Enden
Deque: Elemente werden an beiden Seiten eingefügt und entnommen

Ein Deque kann sowohl als Queue als auch als Stack eingesetzt werden:

  • Als Queue (FIFO, first-in-first-out), indem Elemente auf einer Seite eingefügt und auf der anderen Seite wieder entnommen werden.
  • Als Stack (LIFO, last-in-first-out), indem Elemente an derselben Seite eingefügt und wieder entnommen werden.

Wir müssen uns beim Deque aber nicht auf FIFO- oder LIFO-Funktionalität beschränken. Wir können die Elemente jederzeit an einer beliebigen Seite einfügen und wieder entnehmen.

Deque-Operationen

Die Operationen des Deques heißen analog zur Queue auf beiden Seiten „Enqueue“ und „Dequeue“; dazu kommt „Peek“:

  • „Enqueue at front“: Einfügen eines Elements am Kopf des Deques
  • „Enqueue at back“: Einfügen eines Elements am Ende des Deques
  • „Dequeue at front“: Entnehmen eines Elements vom Kopf des Deques
  • „Dequeue at back“: Entnehmen eines Elements vom Ende des Deques
  • „Peek at front“ und „Peek at back“: Betrachten des Elements am Kopf bzw. am Ende, ohne es zu entnehmen (optional)

Aus einem leeren Deque können die Dequeue-Operationen nichts entnehmen. Hat ein Deque eine feste Kapazität und ist es voll, können die Enqueue-Operationen nichts einfügen. Wie eine Implementierung darauf reagiert – mit einem speziellen Rückgabewert oder mit einer Exception –, legt sie selbst fest.

Die Deque-Operationen in Java

Das Deque-Interface des JDK bietet jede der sechs Operationen in zwei Varianten an: Die eine wirft eine Exception, wenn die Operation nicht möglich ist, die andere liefert einen speziellen Rückgabewert.

Operationim Fehlerfall: Exceptionim Fehlerfall: Rückgabewert
Enqueue at frontaddFirst()offerFirst() – false, wenn das Deque voll ist
Enqueue at backaddLast()offerLast() – false, wenn das Deque voll ist
Dequeue at frontremoveFirst()pollFirst() – null, wenn das Deque leer ist
Dequeue at backremoveLast()pollLast() – null, wenn das Deque leer ist
Peek at frontgetFirst()peekFirst() – null, wenn das Deque leer ist
Peek at backgetLast()peekLast() – null, wenn das Deque leer ist

addFirst() und addLast() werfen eine IllegalStateException, die übrigen vier Methoden der mittleren Spalte eine NoSuchElementException. Von den Deque-Implementierungen des JDK kann übrigens nur LinkedBlockingDeque eine Kapazitätsgrenze haben – alle anderen sind nie voll.

Varianten des Deques

Wie die Queue gibt es auch das Deque in mehreren Abwandlungen:

  • Bounded und Blocking Deque: Ein bounded Deque hat eine feste Kapazität. Ein blocking Deque lässt den aufrufenden Thread warten, bis ein Element verfügbar ist (beim Entnehmen) bzw. bis Platz frei ist (beim Einfügen). Das JDK bietet dafür das BlockingDeque-Interface mit der Implementierung LinkedBlockingDeque an.
  • Threadsicheres, nicht blockierendes Deque: Mehrere Threads dürfen gleichzeitig an beiden Seiten einfügen und entnehmen. Anders als beim blocking Deque wartet dabei kein Thread: Aus einem leeren Deque liefert pollFirst() sofort null. Das JDK bietet dafür die Klasse ConcurrentLinkedDeque an.
  • Input-restricted und Output-restricted Deque: Beim ersten ist das Einfügen nur an einer Seite erlaubt, beim zweiten das Entnehmen. Diese beiden Varianten stammen aus der Lehrbuch-Theorie; das JDK hat keine eigenen Klassen dafür.

Anwendungsgebiete für Deques

Das klassische Anwendungsgebiet für Deques ist eine Undo-Liste. Jeder ausgeführte Bearbeitungsschritt wird auf das Deque gelegt. Beim Aufruf der „Undo“-Funktion wird die zuletzt auf das Deque gelegte Bearbeitung entnommen und wieder rückgängig gemacht.

Bis hierhin ist das ein klassisches LIFO-Prinzip, könnte also auch mit einem Stack realisiert werden.

Aus Speichergründen sollten wir die Undo-Historie allerdings limitieren, z. B. auf 100 Einträge. Bei Verwendung eines Stacks würden die ältesten Elemente am Boden des Stacks liegen und könnten dort nicht entfernt werden. Bei einem Deque stellt das jedoch kein Problem dar, da Elemente an beiden Seiten entnommen werden können.

Die folgende Grafik zeigt eine Undo-Historie, die auf fünf Einträge begrenzt ist. Die Zahlen nummerieren die Bearbeitungsschritte; die Historie war mit den Schritten 1 bis 5 voll. Für den neuen Schritt 6 entnimmt die Dequeue-Operation am Ende den ältesten Schritt 1, und die Enqueue-Operation legt Schritt 6 am Kopf ab:

Deque mit den Bearbeitungsschritten 5, 4, 3 und 2; „Enqueue at front“ fügt Schritt 6 am Kopf ein, „Dequeue at back“ entnimmt Schritt 1 am Ende
Undo-Historie mit Limit: Der neue Schritt kommt am Kopf hinzu, der älteste wird am Ende entnommen

Die „Undo“-Funktion entnimmt den jüngsten Schritt am Kopf – an derselben Seite, an der die Enqueue-Operation ihn abgelegt hat.

Auch das JDK selbst setzt Deques ein: Jeder Worker-Thread eines ForkJoinPool – auf dem z. B. parallele Streams laufen – verwaltet seine Aufgaben in einem eigenen Deque. Neue Teilaufgaben legt er an einer Seite ab und nimmt sie im Standardmodus dort auch wieder heraus, also nach dem LIFO-Prinzip.

Hat ein Thread keine Aufgaben mehr, holt er sich welche aus dem Deque eines anderen Threads – und zwar von dessen anderer Seite, nach dem FIFO-Prinzip. Dieses Verfahren heißt Work Stealing. Der Thread, dem das Deque gehört, und der Thread, der Aufgaben daraus holt, arbeiten so an verschiedenen Seiten.

Zeitkomplexität der Deque-Operationen

Eine Einführung in das Thema Zeitkomplexität findest du im Artikel „O-Notation und Zeitkomplexität – anschaulich erklärt“.

Deques werden in der Regel mit Arrays oder verketteten Listen implementiert. Bei beiden Varianten ist der Aufwand für alle sechs Operationen jeweils konstant, d. h., er ändert sich nicht mit der Länge des Deques.

Die Zeitkomplexität dieser Operationen lautet also: O(1).

Eine Ausnahme gibt es beim Array: Ist es voll und soll das Deque weiter wachsen, muss die Enqueue-Operation ein neues, größeres Array anlegen und alle Elemente dorthin umkopieren – dieser eine Aufruf kostet O(n). Weil das Array dabei jedes Mal proportional zu seiner Größe wächst (im ArrayDeque dieser Serie auf das 1,5- bis 2-Fache), kommt das selten vor, und über viele Aufrufe gemittelt bleibt der Aufwand konstant. Man spricht von amortisiert O(1).

Ausblick

Im nächsten Teil des Tutorials lernst du das Deque-Interface von Java im Detail kennen: seine Methoden, die Klassenhierarchie und welche der JDK-Implementierungen du wann einsetzt. Wie du ein Deque selbst mit einem Array implementierst, zeige ich dir später in dieser Serie.

War dieser Artikel hilfreich für dich? Dann freue ich mich, wenn du dir kurz Zeit für eine Bewertung auf meinem ProvenExpert-Profil nimmst.

👉 Bewertung abgeben

Datenstrukturen wirklich verstehen?

Dieser Artikel zeigt eine Datenstruktur. „Mastering Data Structures in Java“ zeigt dir alle – und vor allem, wann du welche einsetzt und warum das über die Laufzeit deiner Anwendung entscheidet.

Der Premium-Online-Kurs ist gerade geschlossen. Auf der Warteliste erfährst du als Erste:r, wenn er wieder öffnet – und bekommst das Angebot vor allen anderen.

Zur Warteliste

Werde ein:e bessere:r Java-Entwickler:in

Mit meinem kostenlosen Newsletter bleibst du vorn. Modernes Java: neue Versionen & Features, Performance und JVM-Insights – 1x im Monat.

Suche