Deque in Java — Sliding Window O(n) and Monotonic Patterns
- 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.
- Deque (Double-Ended Queue) lets you add and remove from both ends in O(1) time
- Four core operations: addFirst, addLast, removeFirst, removeLast — everything else is a convenience wrapper
- ArrayDeque is Java's default choice: O(1) amortised, cache-friendly circular array
- Use it as a stack (addFirst/removeFirst) or queue (addLast/removeFirst) interchangeably
- Sliding window maximum requires a Deque — no other single structure gives O(1) on both ends
- Biggest mistake: storing values instead of indices in monotonic Deque problems — you lose the window boundary check
Deque Debugging Quick-Reference
Incorrect sliding window maxima
System.out.println("Deque: " + deque + " | Values: " + deque.stream().map(i -> arr[i]).collect(Collectors.toList()));Log window boundaries: System.out.printf("Window [%d, %d] — front=%d, isExpired=%b%n", left, right, deque.peekFirst(), deque.peekFirst() < left);NoSuchElementException on removeFirst/removeLast
grep -rn 'removeFirst\|removeLast' src/ | grep -v 'pollFirst\|pollLast'Find empty Deque scenarios: grep -rn 'while.*removeFirst\|for.*removeLast' src/Memory leak — Deque never shrinks
jmap -histo:live <pid> | grep -i 'ArrayDeque\|LinkedList'Add a size assertion: assert deque.size() <= windowSize + 2 : "Deque overflow — expired elements not evicted";NullPointerException with ArrayDeque
Deque<String> deque = new LinkedList<>(); // Temporarily — then trace the null sourceAdd null check: if (value == null) { throw new IllegalArgumentException("ArrayDeque does not accept nulls — value at index " + i); }Production Incident
Production Debug GuideDeque problems rarely throw obvious errors. They manifest as silent data loss, performance degradation, or algorithm failures. Here's how to diagnose them.
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 — Plain English and Patterns
A deque (double-ended queue) supports O(1) insertions and deletions from both ends. It is more general than both a stack (one end) and a queue (FIFO). Python's collections.deque implements this efficiently.
Core operations (all O(1)): 1. append(x): add to right end. 2. appendleft(x): add to left end. 3. pop(): remove from right end. 4. popleft(): remove from left end.
Key pattern — sliding window maximum (monotonic deque): Maintain a deque of indices in decreasing order of value. For each new element: 1. Remove indices from the back that have values <= current (they can never be max). 2. Remove indices from the front that are outside the current window. 3. Append current index to back. 4. Front of deque = index of maximum in current window.
Worked example — sliding max, window k=3, arr=[1,3,-1,-3,5,3,6,7]: i=0(1): deque=[0]. i=1(3): 3>arr[0]=1 → pop 0. deque=[1]. i=2(-1): -1<3. deque=[1,2]. Window full. max=arr[1]=3. i=3(-3): -3<-1. deque=[1,2,3]. Front 1 still in window [1,3]. max=arr[1]=3. i=4(5): pop 3(-3),2(-1),1(3). deque=[4]. max=arr[4]=5. i=5(3): 3<5. deque=[4,5]. max=arr[4]=5. i=6(6): pop 5(3),4(5). deque=[6]. max=arr[6]=6. i=7(7): pop 6(6). deque=[7]. max=arr[7]=7. Sliding maxima: [3,3,5,5,6,7].
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.
package io.thecodeforge.deque; 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.
package io.thecodeforge.deque; 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.
package io.thecodeforge.deque; 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]
Deque Performance: Memory, Cache Locality & Choosing the Right Implementation
Not all Deques are created equal. The implementation you choose has a direct impact on throughput, memory footprint, and GC pressure. Here's what actually matters in production.
ArrayDeque's circular array gives sequential memory access patterns. When you iterate or poll from the front, the CPU prefetcher pulls in adjacent cache lines. That means ArrayDeque can process elements at L1 cache speed (~1ns per element) when the deque fits in cache. LinkedList, by contrast, scatters nodes across the heap — every removeFirst() is a cache miss, and every node traversal chases a pointer through memory.
But ArrayDeque has a hidden cost: resizing. When the internal array fills up, it doubles and copies all elements. For a deque that grows to 1 million elements, that's a 4MB copy operation that stalls your thread. If you're running latency-sensitive code (sub-millisecond SLAs), that copy can blow your budget.
LinkedList avoids the resize stall entirely — every addFirst/addLast is just a pointer swap. But you pay for it with memory: each element in LinkedList requires a Node object with two references (prev, next) plus the element itself. That's roughly 24 bytes of overhead per element on a 64-bit JVM with compressed OOPs. For a deque holding 10 million elements, that's 240MB of overhead that ArrayDeque doesn't need.
The decision rule: use ArrayDeque by default. Switch to LinkedList only when you need null elements or when the resize cost is unacceptable for your latency budget. And if you're really in a tight spot, build your own ring buffer with a fixed-size int[] array — no boxing, no resizing, no GC pressure.
| 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 |
| Memory per element (ArrayDeque) | ~4 bytes (reference, zero overhead) | ~4 bytes | ~4 bytes + resize buffer slack |
| Memory per element (LinkedList) | ~28 bytes (Node + refs) | ~28 bytes | ~28 bytes + prev/next pointers |
| Resize behavior | Doubles array, O(n) copy on full | Doubles array, O(n) copy | Doubles array, O(n) copy |
🎯 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.
- Pre-allocate ArrayDeque capacity when you know the max size — it eliminates the O(n) resize cost and prevents latency spikes in production.
- Store indices, not values, in monotonic Deque algorithms — without the index you can't check whether the front element has slid out of the current window.
⚠ Common Mistakes to Avoid
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?SeniorReveal
- 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?SeniorReveal
- 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?Mid-levelReveal
- QCompare and contrast ArrayDeque and LinkedList as Deque implementations in Java. When would you choose each one?SeniorReveal
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.
What makes deque O(1) at both ends while list is O(n) at the front?
Python's deque is backed by a doubly-linked list of fixed-size blocks. Adding or removing from either end just updates a pointer — O(1). Python's list is backed by a contiguous array; removing from position 0 requires shifting all other elements left — O(n).
When should I use a deque vs a regular queue?
Use a regular queue (deque with only append+popleft) for BFS. Use a full deque when you need both-end access: sliding window maximum (add to right, remove expired from left), palindrome checking (compare front and back), and implementing a stack-queue hybrid (like a deque acting as both).
Is ArrayDeque thread-safe for concurrent access?
No — ArrayDeque is not thread-safe. If multiple threads access it concurrently, use ConcurrentLinkedDeque instead, or wrap ArrayDeque with synchronized blocks. ConcurrentLinkedDeque uses lock-free CAS operations for thread-safe access but has higher per-operation overhead than ArrayDeque.
How do I iterate over a Deque without modifying it?
Use for-each or iterator: for (T element : deque) { ... }. ArrayDeque's iterator is fail-fast — if the Deque is structurally modified after the iterator is created, it throws ConcurrentModificationException. For safe iteration with concurrent modification, copy the Deque first: List<T> snapshot = new ArrayList<>(deque).
Developer and founder of TheCodeForge. I built this site because I was tired of tutorials that explain what to type without explaining why it works. Every article here is written to make concepts actually click.