Skip to content
Home DSA Deque in Java — Sliding Window O(n) and Monotonic Patterns

Deque in Java — Sliding Window O(n) and Monotonic Patterns

Where developers are forged. · Structured learning · Free forever.
📍 Part of: Stack & Queue → Topic 4 of 10
Stack-backed undo corrupted state with concurrent edits.
⚙️ Intermediate — basic DSA knowledge assumed
In this tutorial, you'll learn
Stack-backed undo corrupted state with concurrent edits.
  • 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.
✦ Plain-English analogy ✦ Real code with output ✦ Interview questions
Quick Answer
  • 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
🚨 START HERE

Deque Debugging Quick-Reference

Fix the six most common Deque problems before they reach production. These are the failures every senior engineer has debugged at least once.
🟡

Incorrect sliding window maxima

Immediate ActionStop and check: are you storing array values or indices in the Deque?
Commands
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);
Fix NowChange Deque<Integer> values to Deque<Integer> indices. Replace while( arr[deque.peekLast()] <= val ) with while( deque.peekLast() != null && arr[deque.peekLast()] <= arr[current] ).
🟡

NoSuchElementException on removeFirst/removeLast

Immediate ActionWrap the removal with isEmpty() check or switch to pollFirst()/pollLast()
Commands
grep -rn 'removeFirst\|removeLast' src/ | grep -v 'pollFirst\|pollLast'
Find empty Deque scenarios: grep -rn 'while.*removeFirst\|for.*removeLast' src/
Fix NowReplace all removeFirst() calls with pollFirst() for empty-tolerant code. Only use removeFirst() when the Deque is guaranteed non-empty by a preceding isEmpty() check.
🟡

Memory leak — Deque never shrinks

Immediate ActionCheck if expired elements are removed from the front every iteration
Commands
jmap -histo:live <pid> | grep -i 'ArrayDeque\|LinkedList'
Add a size assertion: assert deque.size() <= windowSize + 2 : "Deque overflow — expired elements not evicted";
Fix NowAdd boundary check: if (deque.peekFirst() != null && deque.peekFirst() < current - windowSize + 1) { deque.removeFirst(); } — place this BEFORE recording the result.
🟡

NullPointerException with ArrayDeque

Immediate ActionReplace ArrayDeque with LinkedList temporarily to verify the bug is null-related
Commands
Deque<String> deque = new LinkedList<>(); // Temporarily — then trace the null source
Add null check: if (value == null) { throw new IllegalArgumentException("ArrayDeque does not accept nulls — value at index " + i); }
Fix NowReplace ArrayDeque with LinkedList, or filter nulls before insertion. If you use LinkedList, document the memory trade-off: each node costs ~24 bytes extra due to prev/next pointers.
Production Incident

The Undo/Redo Crash: How a Stack-Only Design Brought Down a Production Editor

A cloud-based collaborative editor used a single Stack for undo history. When users needed to undo an operation that wasn't at the top — because another user's edit was injected between — the entire history tree collapsed into data corruption.
SymptomUndo operations would silently skip or corrupt document state. Users reported losing entire paragraphs after clicking undo. The bug was intermittent and only appeared with three or more concurrent editors.
AssumptionThe team assumed all undo operations are stack-like: last operation always gets undone first. In a single-user editor that's true. In a multi-user editor, remote operations can arrive in any order and need to be undone at arbitrary positions.
Root causeA java.util.Stack backed the document history. When a remote edit arrived with a sequence number between two local edits, the team tried to remove it from the middle of the stack. They wrote a loop that popped and re-pushed everything above it — O(n) per undo, with pointer corruption when multiple users triggered it simultaneously.
FixSwitched to ArrayDeque used as a Deque (not a stack). Remote operations were inserted at the front (addFirst) with their sequence numbers. Local operations went at the back (addLast). Undo by sequence ID used removeLastOccurrence on a LinkedList wrapping the Deque — until a proper operation-ID map was built.
Key Lesson
A Stack assumes a single linear timeline. Real systems have concurrent timelines.If you need to remove from arbitrary positions, a Deque with indexed access is still wrong — consider a LinkedHashMap or versioned list.The production fix wasn't 'make it a Deque' — it was 'stop treating history as stack-like'.Test undo with concurrent editors, not just sequential single-user flows.
Production Debug Guide

Deque problems rarely throw obvious errors. They manifest as silent data loss, performance degradation, or algorithm failures. Here's how to diagnose them.

Sliding window algorithm returns incorrect maxima — some windows have the wrong max or skip values entirelyCheck that you're storing INDICES not VALUES in the Deque. Without indices, you can't tell when the front element slides out of the window boundary. Add debug logging for each window: print the Deque contents and the result index.
Deque operations throw NoSuchElementException intermittentlyYou're calling removeFirst() or removeLast() on an empty Deque. Replace with pollFirst()/pollLast() which return null on empty, or guard every removal with !deque.isEmpty(). Check for race conditions if multiple threads access the Deque.
Memory grows unbounded — Deque keeps accumulating elementsYou're adding to the Deque but never removing. In sliding window problems, verify that expired indices are removed from the FRONT every iteration. Add a size check: if the Deque grows beyond window size + buffer, something's wrong.
Algorithm is O(n²) instead of O(n) when using a DequeYou're probably scanning the Deque linearly instead of using the front/back O(1) operations. Check for loops that iterate over the Deque's contents using poll, peek, or iteration. The Deque must only touch elements at its ends.
NullPointerException when using ArrayDequeArrayDeque forbids null elements — it uses null internally as a sentinel. If your data legitimately contains nulls, switch to LinkedList (which implements Deque and allows nulls). Document the trade-off: LinkedList costs more memory per element.

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].

📊 Production Insight
The sliding window pattern looks elegant in code but breaks silently in production when window boundaries are off-by-one.
Every time you test with window size = 1 or window = full array, you catch the off-by-one before it corrupts real data.
Rule: always validate edge-case window sizes — 1, 2, and array length — in your test suite.
🎯 Key Takeaway
A Deque is a superset of both Stack and Queue.
It enables O(1) algorithms that neither structure can match alone.
The monotonic Deque pattern proves it: O(n) sliding max, impossible with anything else.

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.

io/thecodeforge/deque/DequeCoreBehavior.java · JAVA
1234567891011121314151617181920212223242526272829303132333435363738394041424344
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]
    }
}
▶ Output
Initial task queue: [render-homepage, send-email, resize-image]
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]
💡Pro Tip: addFirst vs offerFirst
ArrayDeque has two flavors for each operation: add/remove (throws exception on failure) and offer/poll (returns null/false on failure). For queue-style code where failure means a bug, use add/remove so you get a loud exception. For polling in loops where 'empty' is expected, use offer/poll to avoid try-catch boilerplate.
📊 Production Insight
ArrayDeque's circular array doubles when full — that resize costs O(n) and stalls your latency-sensitive thread.
For real-time systems, pre-allocate with new ArrayDeque<>(capacity) to avoid resizes entirely.
And never use LinkedList for Deque in high-throughput paths: cache misses kill throughput by 3-5x.
🎯 Key Takeaway
ArrayDeque is your default Deque — doubly-linked list only if you need nulls.
Pre-allocate capacity when you can.
Resize cost is O(n); LinkedList memory cost is ~24 extra bytes per node.

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.

io/thecodeforge/deque/DequeAsStackAndQueue.java · JAVA
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869
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();
    }
}
▶ Output
Bracket check '{[()]}': true
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
⚠ Watch Out: Never use Java's Stack class in new code
java.util.Stack extends Vector, which synchronizes every single operation even in single-threaded programs. This is a relic from Java 1.0. Use ArrayDeque instead — it's what the official Java documentation explicitly recommends, it's faster, and it doesn't expose methods like .elementAt(index) that break the stack abstraction entirely.
📊 Production Insight
Mixing stack and queue modes on the same Deque is powerful but dangerous — you can accidentally dequeue from the wrong end.
We've seen a production scheduler push urgent jobs with addFirst and process them with removeFirst — but also drain the queue with removeLast on a timeout, skipping the urgent jobs entirely.
The rule: if you mix modes, wrap the Deque in a facade that only exposes the operations each consumer needs.
🎯 Key Takeaway
Deque replaces both Stack and Queue.
Never use java.util.Stack — it's a legacy wrapper over Vector.
If you mix modes, wrap the Deque to prevent accidental misuse.

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.

io/thecodeforge/deque/SlidingWindowMaximum.java · JAVA
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394
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));
    }
}
▶ Output
Daily temperatures: [31, 27, 34, 22, 19, 35, 28, 30, 24, 33]
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]
🔥Interview Gold: Why O(n) and not O(n log n)?
Each index is added to the Deque exactly once and removed at most once — from either the front or the back. So the total number of Deque operations across all n elements is bounded by 2n, giving O(n) overall. This is the argument interviewers want to hear. Don't just say 'it's efficient' — explain that each element touches the Deque at most twice.
📊 Production Insight
The monotonic Deque pattern is O(n) in theory but can be slow in practice if you're boxing integers or using LinkedList.
ArrayDeque<int> doesn't exist in Java — every operation boxes/unboxes the index, adding allocation pressure.
For latency-sensitive sliding windows, use an int[] ring buffer instead of ArrayDeque to eliminate boxing entirely.
🎯 Key Takeaway
Sliding window maximum is O(n) because each index enters and exits the Deque at most once.
Store indices, not values — you need the position to check window boundaries.
For high-throughput systems, an int[] ring buffer beats ArrayDeque on GC pressure.

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.

📊 Production Insight
ArrayDeque's resize can stall latency-sensitive threads — a 4MB copy at 1M elements kills sub-ms SLAs.
Pre-allocate with new ArrayDeque<>(capacity) when you know your max size.
LinkedList's node overhead adds 240MB per 10M elements — that's a real memory budget hit in containerized environments.
Rule: measure your actual working set size. If it fits in a pre-allocated ring buffer, build one. Don't let the JVM guess for you.
🎯 Key Takeaway
ArrayDeque for 99% of cases: cache-friendly, low memory overhead.
LinkedList only when you need nulls or can't tolerate resize stalls.
For ultra-low-latency: build a fixed-size int[] ring buffer — no boxing, no resizing, no GC.
🗂 Deque Implementation Comparison
ArrayDeque vs LinkedList for Deque operations in production Java systems
Feature / AspectStack (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/backFront onlyFront only✅ Both ends
Time complexity (all ops)O(1) amortisedO(1) amortisedO(1) amortised
Null elements allowed❌ No (ArrayDeque)❌ No (ArrayDeque)❌ No (ArrayDeque)
Thread-safe?❌ No❌ No❌ No (use ConcurrentLinkedDeque)
Ideal forDFS, undo history, call stack simBFS, task queues, event buffersSliding 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 behaviorDoubles array, O(n) copy on fullDoubles array, O(n) copyDoubles 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

    Calling removeFirst() on an empty Deque
    Symptom

    NoSuchElementException is thrown intermittently in production when the Deque is drained by a concurrent consumer before your consumer reaches it. The stack trace points to the removal call, but the root cause is a race condition or logic error in empty-checking.

    Fix

    Replace removeFirst() with pollFirst() which returns null on empty. Or always guard with if (!deque.isEmpty()) before calling removeFirst(). For concurrent access, use ConcurrentLinkedDeque or add synchronization around empty checks and removals.

    Storing values instead of indices in the sliding window Deque
    Symptom

    The sliding window maximum algorithm returns the wrong maximum for some windows, especially after the first few windows. The algorithm looks correct but produces incorrect results intermittently because you can't tell when the front element has slid outside the window boundary.

    Fix

    Always store indices in the Deque for monotonic sliding window problems. Use deque.peekFirst() to get the index, then check if that index is less than the current window's left boundary. If so, removeFirst(). Only use the index to look up the actual value from the array when you need to record the result.

    Using LinkedList as your default Deque implementation
    Symptom

    Memory usage is 3-5x higher than expected for large deques. Throughput degrades by 40-60% under load compared to ArrayDeque. JFR profiling shows high cache miss rates and GC pressure from Node object allocation.

    Fix

    Switch to ArrayDeque as the default Deque implementation. LinkedList implements Deque but allocates a separate Node object per element with prev/next pointers. Stick with ArrayDeque unless you specifically need null element support (which ArrayDeque forbids). If you need null support, document the memory trade-off and monitor heap usage.

    Using Deque where a simple Queue or Stack would suffice
    Symptom

    Code review feedback flags unnecessary complexity. The Deque is used only as a stack (addFirst/removeFirst) or only as a queue (addLast/removeFirst), but the broader team expects the simpler abstraction. New team members misuse the unused operations, introducing bugs.

    Fix

    Wrap the Deque in a Stack or Queue facade. If you only need stack behavior, expose push/pop/peek that delegate to addFirst/removeFirst/peekFirst. If only queue behavior, expose enqueue/dequeue/peek that delegate to addLast/removeFirst/peekFirst. This prevents misuse and communicates intent clearly.

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
    A Queue only supports insert at the back (addLast) and remove from the front (removeFirst) — FIFO ordering. A Deque supports all four directional operations: add/remove at both ends. The sliding window maximum problem is the classic case where a Queue fails. You need to remove smaller candidates from the back (which a Queue can't do) and remove expired indices from the front (which a Queue can do). Only a Deque gives you O(1) access to both ends. Without it, you'd need O(k) per window or a heap with O(log k) per operation.
  • 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
    The problem: given an array of size n and window size k, find the maximum in every contiguous subarray of size k. Brute force is O(nk) — scanning k elements per window. A Deque solves it in O(n). Approach: Maintain a Deque of indices with values in decreasing order. For each new element at index i: 1. Remove indices from the back whose values ≤ arr[i] — they can never be the max while arr[i] is in the window. 2. Remove indices from the front that are < i - k + 1 — they've slid out of the window. 3. Add index i to the back. 4. The front of the Deque is the max for windows starting at i - k + 1. Proof of O(n): Each index is added to the Deque exactly once and removed at most once, from either the front or the back. Total operations ≤ 2n, so O(n). Why a Deque is required: you need O(1) removal from both ends. A heap gives O(log k) per pop. A queue doesn't support back removals. A stack doesn't support front removals.
  • 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
    ArrayDeque uses null internally as a sentinel to detect empty slots in its circular array. If you store null, the size tracking breaks because it can't distinguish 'empty slot' from 'stored null.' Three approaches: 1. Use LinkedList instead — it implements Deque, allows nulls, but costs ~24 bytes extra per element for prev/next pointers and has worse cache locality. 2. Wrap nulls in Optional<T> — Deque<Optional<MyType>> uses Optional.empty() to represent null values. This adds one object allocation per element but keeps ArrayDeque's cache performance. 3. Use a sentinel constant — if your data type allows it, define a static final NULL_SENTINEL = new MyType(...) that represents null. This avoids Optional overhead but adds code complexity. For most cases, I'd prefer Optional wrapping with ArrayDeque over switching to LinkedList, unless the null rate is very high and the memory overhead of Optional becomes significant.
  • QCompare and contrast ArrayDeque and LinkedList as Deque implementations in Java. When would you choose each one?SeniorReveal
    ArrayDeque: backed by a circular resizable array. Provides O(1) amortised time for all four operations. Cache-friendly — contiguous memory access. No thread safety. Forbids null elements. Resize costs O(n) when the array doubles. LinkedList: backed by doubly-linked list of Node objects. Provides O(1) worst-case for all four operations (no resize). Allows null elements. Cache-unfriendly — scattered node allocations. Higher memory overhead (~24 bytes per element for prev/next pointers). Choose ArrayDeque for 99% of cases: better cache locality, lower memory overhead, faster iteration. Choose LinkedList only when: (1) you need to store null elements, (2) you cannot tolerate the O(n) resize cost (e.g., real-time systems with strict latency budgets), or (3) you need O(1) worst-case guarantees rather than amortised. In practice, I've only needed LinkedList as a Deque once — for a low-latency trading engine where a single ArrayDeque resize would have violated our 100-microsecond SLA. Every other case was ArrayDeque.

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).

🔥
Naren Founder & Author

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.

← PreviousPriority Queue and HeapNext →Monotonic Stack Pattern
Forged with 🔥 at TheCodeForge.io — Where Developers Are Forged