Deque (Double-Ended Queue) Explained — Operations, Use Cases & Java Examples
Every program that manages ordered data eventually hits an awkward wall: a plain Queue is too rigid (insert at back, remove from front — period), and a Stack is the opposite extreme (everything through one end only). But real problems rarely fit neatly into either box. Your browser's back-and-forward history needs insertions and removals at both ends. A sliding-window algorithm needs to add to the back while evicting old entries from the front. A task scheduler needs to push urgent work to the front without rebuilding the entire structure. That's the gap the Deque was born to fill.
A Deque — pronounced 'deck,' short for Double-Ended Queue — is a linear data structure that lets you add and remove elements from both its front and its back in O(1) time. It's not a hack on top of a Queue; it's a first-class citizen in every serious language's standard library. Java has ArrayDeque. Python has collections.deque. C++ has std::deque. The reason they all ship one is that too many critical algorithms need this exact shape.
By the end of this article you'll understand precisely why a Deque exists, when to reach for it over a Stack or Queue, how to implement core operations from scratch, and how to use Java's ArrayDeque to solve a real sliding-window problem that shows up constantly in technical interviews. No hand-waving, no abstract diagrams divorced from code — just concrete understanding you can use tomorrow.
How a Deque Works Internally — The Four Core Operations
A Deque supports exactly four directional operations: addFirst, addLast, removeFirst, and removeLast. Everything else (peekFirst, peekLast, size, isEmpty) is just a convenience wrapper on top of these four. Understanding that simplicity is important — a Deque is not complicated, it's just unrestricted.
Under the hood, most implementations use either a doubly-linked list or a circular resizable array. Java's ArrayDeque uses the circular array approach, which gives better cache locality than a linked list because elements are stored contiguously in memory. When the array fills up, it doubles in capacity — the same growth strategy as ArrayList.
The doubly-linked list approach (like LinkedList in Java, which also implements Deque) has O(1) worst-case for all four operations but pays a memory overhead for the prev and next pointers on every node, plus it thrashes the CPU cache because nodes are scattered in the heap.
For 99% of use cases, ArrayDeque is the right pick. The Java docs themselves say: 'This class is likely to be faster than Stack when used as a stack, and faster than LinkedList when used as a queue.' That's a direct endorsement from the authors. Use ArrayDeque unless you specifically need null elements (ArrayDeque forbids them) or guaranteed O(1) worst-case at the cost of memory.
import java.util.ArrayDeque; import java.util.Deque; public class DequeCoreBehavior { public static void main(String[] args) { // ArrayDeque is the go-to Deque implementation in Java. // It's backed by a resizable circular array — fast and cache-friendly. Deque<String> taskQueue = new ArrayDeque<>(); // --- addLast: normal enqueue, like a regular queue --- taskQueue.addLast("render-homepage"); // rear -> [render-homepage] taskQueue.addLast("send-email"); // rear -> [render-homepage, send-email] taskQueue.addLast("resize-image"); // rear -> [render-homepage, send-email, resize-image] System.out.println("Initial task queue: " + taskQueue); // Output: [render-homepage, send-email, resize-image] // --- addFirst: urgent task jumps to the FRONT --- // This is the operation no plain Queue can do without rebuilding itself. taskQueue.addFirst("URGENT-payment-gateway"); System.out.println("After urgent task injected at front: " + taskQueue); // Output: [URGENT-payment-gateway, render-homepage, send-email, resize-image] // --- peekFirst / peekLast: look without removing --- System.out.println("Next task to process: " + taskQueue.peekFirst()); // URGENT-payment-gateway System.out.println("Last task in line: " + taskQueue.peekLast()); // resize-image // --- removeFirst: process tasks from the front --- String processed = taskQueue.removeFirst(); System.out.println("Processed: " + processed); System.out.println("Remaining tasks: " + taskQueue); // Output: [render-homepage, send-email, resize-image] // --- removeLast: cancel the last-added task (like an undo) --- String cancelled = taskQueue.removeLast(); System.out.println("Cancelled (last task removed): " + cancelled); System.out.println("Final queue: " + taskQueue); // Output: [render-homepage, send-email] } }
After urgent task injected at front: [URGENT-payment-gateway, render-homepage, send-email, resize-image]
Next task to process: URGENT-payment-gateway
Last task in line: resize-image
Processed: URGENT-payment-gateway
Remaining tasks: [render-homepage, send-email, resize-image]
Cancelled (last task removed): resize-image
Final queue: [render-homepage, send-email]
Deque as a Stack AND a Queue — One Structure, Two Personalities
Here's the insight that makes a Deque so valuable: it's a strict superset of both Stack and Queue. You can use it as a pure stack by only calling addFirst and removeFirst. You can use it as a pure queue by calling addLast and removeFirst. Or you can mix both modes in the same algorithm.
Java's old Stack class extends Vector, which is synchronized (thread-locking every call even in single-threaded code) and carries decades of legacy baggage. The official Java recommendation since Java 6 has been: use a Deque instead of Stack. Specifically, ArrayDeque when you don't need thread safety.
This matters beyond style. When you push and pop using a Deque-as-stack, you're getting a leaner, faster structure with no synchronization overhead and no broken inheritance chain (Stack inheriting from Vector means you can call .get(3) on a 'stack,' which violates the entire abstraction).
A Queue interface in Java also doesn't give you addFirst. So when you're building a BFS that needs to occasionally push a node back to the front of the queue (priority re-insertion), a plain Queue forces you to use a PriorityQueue or rebuild the collection. A Deque handles it in one method call.
Think of Deque as the Swiss Army knife that ships with your runtime — it costs you nothing extra to use the more capable tool.
import java.util.ArrayDeque; import java.util.Deque; public class DequeAsStackAndQueue { // ── Using Deque as a STACK (LIFO) ──────────────────────────────────────── // Real use case: parsing nested brackets in a code editor static boolean isBracketsBalanced(String sourceCode) { Deque<Character> bracketStack = new ArrayDeque<>(); for (char ch : sourceCode.toCharArray()) { // Opening bracket: push onto the top of our stack if (ch == '(' || ch == '{' || ch == '[') { bracketStack.addFirst(ch); // addFirst = push } // Closing bracket: check if it matches the last opened bracket else if (ch == ')' || ch == '}' || ch == ']') { if (bracketStack.isEmpty()) { return false; // Closed something that was never opened } char lastOpened = bracketStack.removeFirst(); // removeFirst = pop if (!isMatchingPair(lastOpened, ch)) { return false; // Mismatch: { closed by ) for example } } } // If stack is empty, every opened bracket was closed correctly return bracketStack.isEmpty(); } static boolean isMatchingPair(char open, char close) { return (open == '(' && close == ')') || (open == '{' && close == '}') || (open == '[' && close == ']'); } // ── Using Deque as a QUEUE (FIFO) ──────────────────────────────────────── // Real use case: print job spooler — jobs processed in arrival order, // but a SYSTEM job can jump to the front. static void simulatePrintSpooler() { Deque<String> printQueue = new ArrayDeque<>(); // Normal jobs arrive at the back printQueue.addLast("user-invoice.pdf"); // addLast = enqueue printQueue.addLast("user-report.pdf"); printQueue.addLast("user-photo.png"); // System diagnostic needs to print NOW — inject at the front printQueue.addFirst("SYSTEM-diagnostic.pdf"); System.out.println("Print spooler order:"); while (!printQueue.isEmpty()) { System.out.println(" Printing: " + printQueue.removeFirst()); // removeFirst = dequeue } } public static void main(String[] args) { // ── Stack mode: bracket validation ── System.out.println("Bracket check '{[()]}': " + isBracketsBalanced("{[()]}")); // true System.out.println("Bracket check '({[})': " + isBracketsBalanced("({[})")); // false System.out.println("Bracket check '((()))': " + isBracketsBalanced("((()))")); // true System.out.println(); // ── Queue mode: print spooler ── simulatePrintSpooler(); } }
Bracket check '({[})': false
Bracket check '((()))': true
Print spooler order:
Printing: SYSTEM-diagnostic.pdf
Printing: user-invoice.pdf
Printing: user-report.pdf
Printing: user-photo.png
Real-World Algorithm: Sliding Window Maximum Using a Deque
The sliding window maximum problem is THE canonical Deque interview question, and it perfectly illustrates why no other structure can do this elegantly.
The problem: given an array of numbers and a window of size k, find the maximum value in each window as it slides from left to right. Brute force is O(n*k) — for each window position, scan all k elements. With a Deque, you can do it in O(n).
The trick is to use the Deque as a monotonic decreasing queue of indices. You maintain the invariant that the front of the Deque always holds the index of the largest element in the current window. When you slide the window: 1. Remove from the back any elements smaller than the incoming element (they can never be the max while the new element exists in the window). 2. Remove from the front any index that's fallen outside the window boundary. 3. The front is always your current window maximum.
This is not intuitive at first. But notice what makes a Deque mandatory here: you need to remove stale indices from the FRONT (out-of-window), and you need to remove useless candidates from the BACK (smaller values). No other single structure gives you O(1) on both ends. A heap would give O(log k) per operation. A queue gives you nothing for the back. A stack gives you nothing for the front.
This pattern generalises to sliding window minimum, next greater element, and monotonic stack problems with a window constraint.
import java.util.ArrayDeque; import java.util.Arrays; import java.util.Deque; public class SlidingWindowMaximum { /** * Finds the maximum value in every sliding window of size windowSize. * * The Deque stores INDICES (not values) of candidate maximum elements. * Invariant: elements in the Deque are always in decreasing order of their * corresponding values in the input array. * * @param temperatures The input array (e.g., daily temperature readings) * @param windowSize Number of days in each sliding window * @return Array of maximum temperatures per window */ static int[] slidingWindowMax(int[] temperatures, int windowSize) { if (temperatures == null || temperatures.length == 0 || windowSize <= 0) { return new int[0]; } int totalDays = temperatures.length; int[] windowMaxima = new int[totalDays - windowSize + 1]; // one max per window int resultIndex = 0; // The Deque holds indices into the temperatures array. // Front = index of current window's maximum. // Back = index of the smallest 'candidate' we haven't discarded yet. Deque<Integer> candidateIndices = new ArrayDeque<>(); for (int currentDay = 0; currentDay < totalDays; currentDay++) { // STEP 1: Evict from the BACK — remove any past candidates whose // temperature is <= today's. They can NEVER be a future window max // while today's value is still inside the window. while (!candidateIndices.isEmpty() && temperatures[candidateIndices.peekLast()] <= temperatures[currentDay]) { candidateIndices.removeLast(); // this candidate is useless now } // STEP 2: Add today's index as the newest candidate at the BACK candidateIndices.addLast(currentDay); // STEP 3: Evict from the FRONT — if the front index is outside // the current window's left boundary, it's no longer valid. int windowLeftBoundary = currentDay - windowSize + 1; if (candidateIndices.peekFirst() < windowLeftBoundary) { candidateIndices.removeFirst(); // slid out of the window } // STEP 4: Once we've filled the first window, record the max. // The front of the Deque is always the index of the window's max. if (currentDay >= windowSize - 1) { windowMaxima[resultIndex++] = temperatures[candidateIndices.peekFirst()]; } } return windowMaxima; } public static void main(String[] args) { // Scenario: 10 days of temperature readings, window = 3 days int[] dailyTemperatures = {31, 27, 34, 22, 19, 35, 28, 30, 24, 33}; int windowSize = 3; System.out.println("Daily temperatures: " + Arrays.toString(dailyTemperatures)); System.out.println("Window size: " + windowSize + " days\n"); int[] maxPerWindow = slidingWindowMax(dailyTemperatures, windowSize); System.out.println("Max temperature in each 3-day window:"); for (int i = 0; i < maxPerWindow.length; i++) { int windowStart = i; int windowEnd = i + windowSize - 1; System.out.printf(" Days %d-%d %s → Max: %d°C%n", windowStart + 1, windowEnd + 1, Arrays.toString(Arrays.copyOfRange(dailyTemperatures, windowStart, windowEnd + 1)), maxPerWindow[i]); } // Edge case: window size = 1 (each element is its own max) System.out.println(); int[] singleDayWindow = slidingWindowMax(dailyTemperatures, 1); System.out.println("Window size 1 (sanity check): " + Arrays.toString(singleDayWindow)); // Edge case: window = full array (single max of entire array) int[] fullWindow = slidingWindowMax(dailyTemperatures, dailyTemperatures.length); System.out.println("Window = full array: " + Arrays.toString(fullWindow)); } }
Window size: 3 days
Max temperature in each 3-day window:
Days 1-3 [31, 27, 34] → Max: 34°C
Days 2-4 [27, 34, 22] → Max: 34°C
Days 3-5 [34, 22, 19] → Max: 34°C
Days 4-6 [22, 19, 35] → Max: 35°C
Days 5-7 [19, 35, 28] → Max: 35°C
Days 6-8 [35, 28, 30] → Max: 35°C
Days 7-9 [28, 30, 24] → Max: 30°C
Days 8-10 [30, 24, 33] → Max: 33°C
Window size 1 (sanity check): [31, 27, 34, 22, 19, 35, 28, 30, 24, 33]
Window = full array: [35]
| Feature / Aspect | Stack (ArrayDeque) | Queue (ArrayDeque) | Full Deque (ArrayDeque) |
|---|---|---|---|
| Insert at front | ✅ addFirst | ❌ Not supported | ✅ addFirst |
| Insert at back | ❌ Not idiomatic | ✅ addLast | ✅ addLast |
| Remove from front | ❌ Not idiomatic | ✅ removeFirst | ✅ removeFirst |
| Remove from back | ✅ removeFirst (top) | ❌ Not supported | ✅ removeLast |
| Peek front/back | Front only | Front only | ✅ Both ends |
| Time complexity (all ops) | O(1) amortised | O(1) amortised | O(1) amortised |
| Null elements allowed | ❌ No (ArrayDeque) | ❌ No (ArrayDeque) | ❌ No (ArrayDeque) |
| Thread-safe? | ❌ No | ❌ No | ❌ No (use ConcurrentLinkedDeque) |
| Ideal for | DFS, undo history, call stack sim | BFS, task queues, event buffers | Sliding window, palindrome check, scheduler with priority |
🎯 Key Takeaways
- A Deque is a strict superset of both Stack and Queue — wherever you use either, a Deque can do the job and usually does it faster (especially vs Java's legacy Stack class).
- ArrayDeque is your default choice in Java: O(1) amortised for all four directional operations, better cache locality than LinkedList, and no synchronization overhead from the old Vector-based Stack.
- The monotonic Deque pattern (sliding window maximum/minimum) achieves O(n) by guaranteeing each element enters and exits the Deque at most once — this is the key insight interviewers want you to articulate.
- addFirst/removeFirst model a stack; addLast/removeFirst model a queue; mixing all four unlocks algorithms that neither structure alone can solve in optimal time.
⚠ Common Mistakes to Avoid
- ✕Mistake 1: Calling removeFirst() on an empty Deque and expecting null — It throws NoSuchElementException, not null. Fix: use pollFirst() instead, which returns null when the Deque is empty, or guard with isEmpty() before calling removeFirst().
- ✕Mistake 2: Storing values instead of indices in the sliding window Deque — If you store the actual values, you can't tell whether the front element has slid outside the current window boundary. You need the index to calculate (currentIndex - frontIndex >= windowSize). Always store indices in monotonic Deque problems.
- ✕Mistake 3: Using LinkedList as your Deque implementation — LinkedList also implements Deque, so it compiles and runs fine, but it allocates a separate Node object per element with two extra pointer fields. For large or high-throughput data, this blows up memory usage and hurts cache performance. Stick with ArrayDeque unless you have a specific reason (like needing null element support) to use LinkedList.
Interview Questions on This Topic
- QWhat is the difference between a Deque and a Queue, and can you name a real algorithm where a Deque is required and a Queue is not sufficient?
- QWalk me through the sliding window maximum problem. Why is a Deque the optimal data structure, and what is the time complexity of your solution with proof?
- QIf ArrayDeque doesn't allow null elements, how would you handle a stream that legitimately contains null values — and what alternative Deque implementation would you use?
Frequently Asked Questions
Is a Deque the same as a doubly linked list?
No — a Deque is an abstract data type defined by its behaviour (insert and remove at both ends). A doubly linked list is one possible implementation of that behaviour. Java's ArrayDeque implements the Deque interface using a circular resizable array, not a linked list, yet it satisfies all Deque operations in O(1) amortised time with better memory characteristics.
When should I use a Deque over a PriorityQueue?
Use a Deque when your access pattern is positional — you always want the front or back element specifically. Use a PriorityQueue when you need the globally minimum or maximum element regardless of insertion order. The sliding window maximum uses a Deque precisely because you need the max within a bounded positional window, not the global max.
Why does Java's ArrayDeque forbid null elements?
ArrayDeque uses null internally as a sentinel value to detect empty slots in its circular array. Allowing you to store null would make it impossible to distinguish 'this slot is empty' from 'this slot holds a null the user inserted,' which would break the size tracking and iteration logic. If you need null support, use LinkedList, but be aware of the memory and performance trade-offs.
Written and reviewed by senior developers with real-world experience across enterprise, startup and open-source projects. Every article on TheCodeForge is written to be clear, accurate and genuinely useful — not just SEO filler.