Deque: Double-Ended Queue Operations

A deque (pronounced 'deck') is a double-ended queue that supports insertion and removal at both ends in constant time. Think of it as a hybrid between a stack and a queue—you get the best of both...

Key Insights

  • Deques provide O(1) insertion and removal at both ends, making them more versatile than stacks or queues while maintaining the same performance characteristics.
  • ArrayDeque outperforms LinkedList in almost every scenario—use it as your default deque implementation unless you specifically need null values or list-based operations.
  • The sliding window pattern is the killer use case for deques, enabling O(n) solutions to problems that would otherwise require O(n²) time complexity.

Introduction to Deques

A deque (pronounced “deck”) is a double-ended queue that supports insertion and removal at both ends in constant time. Think of it as a hybrid between a stack and a queue—you get the best of both worlds without sacrificing performance.

Standard queues enforce FIFO (first-in, first-out) ordering. Stacks enforce LIFO (last-in, first-out). Deques don’t enforce either—they give you the flexibility to choose. This makes them incredibly useful for algorithms that need to process elements from either direction.

The key operations all run in O(1) time:

  • Add to front or back
  • Remove from front or back
  • Peek at front or back

This constant-time guarantee at both ends is what separates deques from other data structures. A standard array gives you O(1) at one end but O(n) at the other. A deque gives you O(1) everywhere that matters.

Core Operations: Adding Elements

Deques provide two styles of insertion methods: throwing and non-throwing. The throwing variants (addFirst, addLast) raise exceptions when the operation fails. The non-throwing variants (offerFirst, offerLast) return a boolean instead.

import java.util.ArrayDeque;
import java.util.Deque;

public class DequeInsertionDemo {
    public static void main(String[] args) {
        Deque<String> deque = new ArrayDeque<>();
        
        // Adding to the front
        deque.addFirst("second");
        deque.addFirst("first");  // Now at front
        
        // Adding to the back
        deque.addLast("third");
        deque.addLast("fourth");  // Now at back
        
        System.out.println(deque);  // [first, second, third, fourth]
        
        // offer variants return boolean (useful for bounded deques)
        boolean added = deque.offerFirst("zero");
        System.out.println("Added: " + added);  // true
        
        // For unbounded ArrayDeque, offer and add behave identically
        // The difference matters for bounded implementations
    }
}

Python’s collections.deque uses a simpler naming convention:

from collections import deque

d = deque()

# Adding to ends
d.append("right")       # Add to right/back
d.appendleft("left")    # Add to left/front

print(d)  # deque(['left', 'right'])

# Extend with multiple elements
d.extend([1, 2, 3])           # Add multiple to right
d.extendleft(['a', 'b', 'c']) # Add multiple to left (reversed order!)

print(d)  # deque(['c', 'b', 'a', 'left', 'right', 1, 2, 3])

Note the extendleft behavior—elements are added one at a time to the left, so the final order is reversed. This catches many developers off guard.

Core Operations: Removing Elements

Removal operations follow the same throwing vs. non-throwing pattern. Use removeFirst/removeLast when an empty deque indicates a bug. Use pollFirst/pollLast when an empty deque is a valid state.

import java.util.ArrayDeque;
import java.util.Deque;
import java.util.NoSuchElementException;

public class DequeRemovalDemo {
    public static void main(String[] args) {
        Deque<Integer> deque = new ArrayDeque<>();
        deque.addLast(1);
        deque.addLast(2);
        deque.addLast(3);
        
        // Throwing variants - use when empty is unexpected
        int first = deque.removeFirst();  // Returns 1
        int last = deque.removeLast();    // Returns 3
        
        System.out.println("Removed: " + first + ", " + last);
        System.out.println("Remaining: " + deque);  // [2]
        
        // Empty the deque
        deque.clear();
        
        // Non-throwing variants - use when empty is expected
        Integer polled = deque.pollFirst();  // Returns null, not exception
        System.out.println("Polled from empty: " + polled);  // null
        
        // Throwing variant on empty deque
        try {
            deque.removeFirst();  // Throws NoSuchElementException
        } catch (NoSuchElementException e) {
            System.out.println("Caught expected exception");
        }
    }
}
from collections import deque

d = deque([1, 2, 3])

# Remove from ends
left = d.popleft()   # Returns 1
right = d.pop()      # Returns 3

print(f"Removed: {left}, {right}")  # Removed: 1, 3
print(d)  # deque([2])

# Python raises IndexError on empty deque
d.clear()
try:
    d.popleft()
except IndexError as e:
    print("Caught expected error")

Peeking and Inspection

Peeking lets you examine elements without removing them. This is essential for algorithms that need to make decisions based on the front or back element before committing to removal.

import java.util.ArrayDeque;
import java.util.Deque;

public class DequePeekDemo {
    public static void main(String[] args) {
        Deque<String> deque = new ArrayDeque<>();
        deque.addLast("alpha");
        deque.addLast("beta");
        deque.addLast("gamma");
        
        // Non-throwing peeks - return null if empty
        String first = deque.peekFirst();  // "alpha"
        String last = deque.peekLast();    // "gamma"
        
        System.out.println("First: " + first + ", Last: " + last);
        System.out.println("Size unchanged: " + deque.size());  // 3
        
        // Throwing variants - use getFirst/getLast
        String getFirst = deque.getFirst();  // "alpha" or throws
        String getLast = deque.getLast();    // "gamma" or throws
        
        // Safe peek pattern for processing
        while (deque.peekFirst() != null) {
            String current = deque.peekFirst();
            if (shouldProcess(current)) {
                deque.removeFirst();
                process(current);
            } else {
                break;
            }
        }
    }
    
    static boolean shouldProcess(String s) { return s.length() < 10; }
    static void process(String s) { System.out.println("Processing: " + s); }
}

Common Implementations

Java provides two main deque implementations: ArrayDeque and LinkedList. Choose wisely—the performance difference is significant.

import java.util.*;

public class DequeImplementationComparison {
    public static void main(String[] args) {
        // ArrayDeque: Resizable array, better cache locality
        Deque<Integer> arrayDeque = new ArrayDeque<>();
        
        // LinkedList: Doubly-linked nodes, more memory overhead
        Deque<Integer> linkedDeque = new LinkedList<>();
        
        int operations = 1_000_000;
        
        // Benchmark ArrayDeque
        long start = System.nanoTime();
        for (int i = 0; i < operations; i++) {
            arrayDeque.addLast(i);
        }
        for (int i = 0; i < operations; i++) {
            arrayDeque.removeFirst();
        }
        long arrayTime = System.nanoTime() - start;
        
        // Benchmark LinkedList
        start = System.nanoTime();
        for (int i = 0; i < operations; i++) {
            linkedDeque.addLast(i);
        }
        for (int i = 0; i < operations; i++) {
            linkedDeque.removeFirst();
        }
        long linkedTime = System.nanoTime() - start;
        
        System.out.printf("ArrayDeque: %d ms%n", arrayTime / 1_000_000);
        System.out.printf("LinkedList: %d ms%n", linkedTime / 1_000_000);
        // ArrayDeque is typically 2-3x faster
    }
}

ArrayDeque wins on almost every metric:

  • Memory: No node object overhead, just the elements
  • Cache locality: Contiguous memory means fewer cache misses
  • Speed: Typically 2-3x faster for most operations

Use LinkedList only when you need null values (ArrayDeque prohibits them) or when you need to implement List interface operations alongside deque operations.

Practical Use Cases

The sliding window maximum problem demonstrates deques at their best. Given an array and window size k, find the maximum element in each window position.

import java.util.*;

public class SlidingWindowMaximum {
    public static int[] maxSlidingWindow(int[] nums, int k) {
        if (nums.length == 0 || k == 0) return new int[0];
        
        int[] result = new int[nums.length - k + 1];
        Deque<Integer> deque = new ArrayDeque<>();  // Stores indices
        
        for (int i = 0; i < nums.length; i++) {
            // Remove indices outside current window
            while (!deque.isEmpty() && deque.peekFirst() < i - k + 1) {
                deque.removeFirst();
            }
            
            // Remove indices of smaller elements (they'll never be max)
            while (!deque.isEmpty() && nums[deque.peekLast()] < nums[i]) {
                deque.removeLast();
            }
            
            deque.addLast(i);
            
            // Record result once we have a full window
            if (i >= k - 1) {
                result[i - k + 1] = nums[deque.peekFirst()];
            }
        }
        
        return result;
    }
    
    public static void main(String[] args) {
        int[] nums = {1, 3, -1, -3, 5, 3, 6, 7};
        int k = 3;
        int[] result = maxSlidingWindow(nums, k);
        System.out.println(Arrays.toString(result));  // [3, 3, 5, 5, 6, 7]
    }
}

This algorithm runs in O(n) time because each element enters and leaves the deque exactly once. A naive approach checking all k elements per window would be O(nk).

Other practical applications include:

  • Undo/redo stacks: Push operations to the back, undo pops from back, redo pushes back
  • Work-stealing schedulers: Workers steal from the opposite end to reduce contention
  • BFS with priority: Process high-priority items from front, add new items to appropriate end

Best Practices and Pitfalls

Thread safety is the biggest pitfall. Standard deque implementations are not thread-safe. For concurrent access, use ConcurrentLinkedDeque or wrap with synchronization.

import java.util.*;
import java.util.concurrent.*;

public class ThreadSafeDequeDemo {
    public static void main(String[] args) throws InterruptedException {
        // Option 1: ConcurrentLinkedDeque (lock-free, best for high contention)
        Deque<String> concurrent = new ConcurrentLinkedDeque<>();
        
        // Option 2: Synchronized wrapper (simpler, fine for moderate contention)
        Deque<String> synchronized_ = Collections.synchronizedCollection(
            new ArrayDeque<String>()
        ) instanceof Deque ? null : null;  // Note: no direct synchronized Deque wrapper
        
        // For synchronized Deque, create your own wrapper or use locks
        Deque<String> safeDeque = new ConcurrentLinkedDeque<>();
        
        // Demonstrate concurrent access
        ExecutorService executor = Executors.newFixedThreadPool(4);
        
        for (int i = 0; i < 1000; i++) {
            final int val = i;
            executor.submit(() -> {
                if (val % 2 == 0) {
                    safeDeque.addFirst("front-" + val);
                } else {
                    safeDeque.addLast("back-" + val);
                }
            });
        }
        
        executor.shutdown();
        executor.awaitTermination(5, TimeUnit.SECONDS);
        
        System.out.println("Final size: " + safeDeque.size());  // 1000
    }
}

Key pitfalls to avoid:

  1. Null values in ArrayDeque: It throws NullPointerException. If you need nulls, use LinkedList.

  2. Forgetting bounded behavior: When using bounded deques, always use offer variants and check return values.

  3. Iterator invalidation: Modifying a deque while iterating causes ConcurrentModificationException. Use iterator’s remove() method instead.

  4. Choosing LinkedList by default: I see this constantly. ArrayDeque should be your default. Only reach for LinkedList when you have a specific reason.

  5. Ignoring the stack/queue methods: Deque implements both Stack and Queue interfaces. Use push/pop for stack behavior, add/remove for queue behavior. This makes your intent clear.

Deques are one of those data structures that seem simple but unlock elegant solutions to complex problems. Master the sliding window pattern, understand when to use throwing vs. non-throwing methods, and default to ArrayDeque. That’s 90% of what you need to know.

Liked this? There's more.

Every week: one practical technique, explained simply, with code you can use immediately.