17

Queues & Deques

First in, first out — your printer's job queue, and the engine behind BFS, ring buffers and sliding windows.

beginner~60 min6 simulations10 problems

▶ guided lesson⚡ 1-minute summary↻ 15 recall cards

before this: Arrays & Strings · Linked Lists · Stacks · Big-O

01 · why it exists

Why this exists, and the weight it carries

since1909–1917

Queue (FIFO)

Agner Krarup Erlang, Copenhagen Telephone Company

Nobody invented "first come, first served" — waiting lines are older than computers, and the FIFO queue in code has no single inventor. What has a clear start is the maths of queues. In 1909 Erlang, an engineer at the Copenhagen Telephone Company, showed that calls made at random follow a Poisson distribution. His 1917 paper gave formulas for lost calls and waiting time, and telephone companies in many countries adopted them. That field, queueing theory, is still how engineers size server pools, call centres and network buffers.

the problem it solvedHow many lines a telephone exchange needs so that calls rarely wait or get lost — sizing any system that serves requests in arrival order.

its jobs today

  • Operating systems keep ready threads and pending disk or network requests in queues; a print spooler sends jobs to the printer in the order they arrived.
  • Web servers and message brokers (Kafka, RabbitMQ, Amazon SQS) hold a burst of requests in line, so the backend works through it instead of falling over.
  • BFS explores a graph or a game map level by level with a queue — the first path that reaches a cell is a shortest one.

the weight it carries

Illustrative: a Tatkal-style burst of 1 lakh (100,000) booking requests reaches a server that handles 5,000 per second. With a FIFO queue the burst drains in 100,000 ÷ 5,000 = 20 seconds, in arrival order. With ArrayList.remove(0) as dequeue, the shifts alone add up to 99,999 + 99,998 + … + 1 ≈ 5·10⁹ element moves.

without it

Bursts of work would be dropped or served in random order — no fairness for print jobs or bookings — and BFS would stop finding shortest paths.

when · why · how

when
Things must be handled in the order they arrived: tasks, requests, messages, a BFS frontier, a sliding time window.
why
Both ends are O(1) — offer at the rear, poll at the front — so n items cost O(n) in total. A list that shifts on every removal turns the same work into O(n²).
how
Queue<Integer> q = new ArrayDeque<>(); then offer, poll, peek, isEmpty. Declare the interface type; pick ArrayDeque unless you need null elements or thread safety.

02 · plain english

The idea in 30 seconds

A queue lets you add at the rear and remove from the front: whoever arrived first leaves first (FIFO).

Both ends must be O(1). An array does that by letting head and tail walk forward and wrap around; a linked list does it with head and tail pointers.

A deque ("deck") opens both ends. Keep it sorted as you go and it becomes a monotonic deque — the trick behind sliding-window maximum.

picture this

Your printer's queue: jobs wait in the order you pressed Ctrl+P. The printer always takes the oldest job; a new job joins at the back. No job jumps the line — that is FIFO.

03 · remember

What you must know

  • FIFO: the element removed is always the one that has waited longest. A stack is LIFO; a queue is its opposite.
  • Circular array: tail = (tail + 1) % capacity and head = (head + 1) % capacity. Nothing ever shifts, so enqueue and dequeue are O(1).
  • Track size (or keep one slot empty) — otherwise head == tail can't tell an empty queue from a full one.
  • `ArrayDeque` is Java's default Queue and Stack: fast, cache-friendly, amortized O(1) at both ends. No null elements allowed.
  • `offer` / `poll` / `peek` return false/null on failure; `add` / `remove` / `element` throw. Pick one family and stay consistent.
  • Monotonic deque: pop from the back every element that can never be the answer again. Each index enters and leaves once → O(n) total.
  • Two stacks make a queue with amortized O(1) per operation: pour in into out only when out is empty.
  • Queues power BFS: processing nodes in arrival order visits them in order of distance — you'll use this in every graph chapter.

04 · see it

Play with it in 3D

Press Space to play, ←/→ to step, drag to move, ? for all shortcuts. Try your own input.

sim

Linked queue: enqueue & dequeue

Top: the queue as the user sees it. Bottom: the linked nodes in memory, with head and tail markers.

loading simulation…
sim

Circular buffer: watch head and tail wrap around

The array never shifts. Grey slots are free (or stale); the live part can wrap past the end back to index 0.

loading simulation…
sim

Queue using two stacks

Pushes go to `in`. When `out` runs dry, pour `in` into it — the reversal puts the oldest element on top.

loading simulation…
sim

Sliding window maximum with a monotonic deque

Values in the deque only decrease, so the front is the max. Smaller, older values are popped for good.

loading simulation…
sim

A time-window queue (Number of Recent Calls)

Times only grow, so old pings leave from the front and never come back.

loading simulation…
sim

Reveal Cards: simulate on positions, not cards

A queue of positions replays the reveal process; the next smallest card drops into each revealed position.

loading simulation…

05 · cost

Time & space, explained

Queue operations by implementation
Circular array (raw)Linked queue (raw)ArrayDequeArrayList + remove(0)why
enqueue (offer)O(1)O(1)O(1) amortizedO(1) amortizedWrite at tail / link after tail. ArrayDeque and ArrayList occasionally grow their array, which averages out to O(1).
dequeue (poll)O(1)O(1)O(1)O(n)Moving head is one step. ArrayList.remove(0) shifts every remaining element left.
peek frontO(1)O(1)O(1)O(1)The front is at a known index or node.
contains / searchO(n)O(n)O(n)O(n)A queue keeps arrival order, not sorted order — you must scan.
MemoryO(capacity)O(n) + a node per itemO(n)O(n)Linked nodes pay for an object header and a next reference per element; arrays are compact.
Building one structure from the other
push / enqueuepop / dequeueExtra spacewhy
Queue from two stacks (lazy transfer)O(1)O(1) amortized, O(n) worstO(n)Each element is moved from in to out at most once in its life, so n operations cost O(n) in total.
Queue from two stacks (transfer on every push)O(n)O(1)O(n)Every push empties and refills the main stack.
Stack from one queue (rotate on push)O(n)O(1)O(n)After offering x, the other n−1 elements are rotated behind it.
Stack from two queues (move on pop)O(1)O(n)O(n)Pop moves all but the newest element to the helper queue.
Sliding window maximum (n elements, window k)
TimeExtra spacewhy
Rescan every windowO(n·k)O(1)n − k + 1 windows, k elements each.
Max-heap with lazy deletionO(n log n)O(n)n heap inserts; stale tops are removed later, so the heap can hold up to n entries.
Monotonic dequeO(n)O(k)Each index is pushed once and popped at most once; the deque never holds more than k indices.

06 · java

Raw vs Collections Framework

A queue on a circular array: write it by hand vs use ArrayDeque

javaraw circular array (head, tail, size)java.util.*
class CircularQueue {    private final int[] data;    private int head = 0; // index of the front element    private int tail = 0; // index where the next element will go    private int size = 0; // how many elements are stored right now     CircularQueue(int capacity) {        data = new int[capacity];    }     /** Adds x at the rear. Returns false when the queue is full (like Queue.offer). */    boolean enqueue(int x) {        if (size == data.length) return false;        data[tail] = x;        tail = (tail + 1) % data.length; // step right, wrap to 0 at the end        size++;        return true;    }     /** Removes and returns the front. Throws when empty (like Queue.remove). */    int dequeue() {        if (size == 0) throw new NoSuchElementException("queue is empty");        int x = data[head];        head = (head + 1) % data.length; // step right, wrap to 0 at the end        size--;        return x;    }     int peek() {        if (size == 0) throw new NoSuchElementException("queue is empty");        return data[head];    }     boolean isEmpty() {        return size == 0;    }     int size() {        return size;    }}
  • % data.length is the whole trick: index capacity − 1 steps to index 0 instead of falling off the array.
  • size removes the classic ambiguity: when head == tail the queue is either empty (size == 0) or full (size == capacity).
  • ArrayDeque is exactly this circular buffer, plus automatic growth when it fills up — that's why its offer is amortized O(1).
  • Declare the variable as the interface (Queue<Integer> / Deque<Integer>) so you can swap implementations without touching the rest of the code.

A linked queue (and why LinkedList is rarely the best Queue)

javaraw linked queuejava.util.*
class LinkedQueue {    private static class Node {        final int val;        Node next;         Node(int val) {            this.val = val;        }    }     private Node head; // front: we remove here    private Node tail; // rear: we add here    private int size;     void enqueue(int x) {        Node node = new Node(x);        if (tail == null) head = node; // empty queue: the new node is also the front        else tail.next = node; // link it after the old rear        tail = node;        size++;    }     int dequeue() {        if (head == null) throw new NoSuchElementException("queue is empty");        int x = head.val;        head = head.next;        if (head == null) tail = null; // queue became empty: forget the rear too        size--;        return x;    }     int peek() {        if (head == null) throw new NoSuchElementException("queue is empty");        return head.val;    }     boolean isEmpty() {        return size == 0;    }}
  • Remove at head, add at tail — both O(1) with no array to grow.
  • When the last node is dequeued, set tail = null too. Forgetting this is the #1 linked-queue bug.
  • java.util.LinkedList implements Queue and Deque, but each element costs a node object. The ArrayDeque Javadoc itself says it is likely faster than LinkedList when used as a queue.
  • Use LinkedList only if you need null elements or you also need it as a List (index access).

Deque: both ends (and Java's real Stack)

javaDeque<Integer> dq = new ArrayDeque<>()java.util.*
class BothEnds {    static String demo() {        Deque<Integer> dq = new ArrayDeque<>();        dq.offerLast(2); // [2]        dq.offerFirst(1); // [1, 2]        dq.offerLast(3); // [1, 2, 3]        int front = dq.pollFirst(); // 1 → [2, 3]        int back = dq.pollLast(); // 3 → [2]        dq.push(9); // stack-style push = offerFirst → [9, 2]        int top = dq.peek(); // 9 — peek() = peekFirst()        return front + " " + back + " " + top + " " + dq; // "1 3 9 [9, 2]"    }}
  • offerFirst/offerLast, pollFirst/pollLast, peekFirst/peekLast — every end operation is O(1).
  • push/pop/peek on a Deque work on the front — so an ArrayDeque is also the recommended stack (prefer it over the legacy Stack class).
  • Iterating an ArrayDeque (or printing it) goes front → back.

BFS teaser: the queue's most famous job

javabreadth-first search distancesjava.util.*
class Bfs {    /** Fewest edges from start to every node of an unweighted graph (-1 = unreachable). */    static int[] distances(List<List<Integer>> graph, int start) {        int[] dist = new int[graph.size()];        Arrays.fill(dist, -1);        Deque<Integer> queue = new ArrayDeque<>();        dist[start] = 0;        queue.offer(start);        while (!queue.isEmpty()) {            int node = queue.poll(); // oldest discovered = closest to start            for (int next : graph.get(node)) {                if (dist[next] == -1) { // first visit is the shortest                    dist[next] = dist[node] + 1;                    queue.offer(next);                }            }        }        return dist;    }}
  • Nodes leave the queue in the order they were discovered, which is the order of their distance from the start.
  • So the first time BFS reaches a node is along a shortest path (in an unweighted graph). The graphs chapter builds on this.
Java API you will actually use
calldoescost
Queue<Integer> q = new ArrayDeque<>()Default FIFO queue (resizable circular array, no nulls)—
q.offer(x) / q.add(x)Add at the rear; offer returns false if a bounded queue is full, add throws IllegalStateExceptionO(1) amortized
q.poll() / q.remove()Remove the front; poll returns null when empty, remove throws NoSuchElementExceptionO(1)
q.peek() / q.element()Read the front without removing; peek returns null, element throwsO(1)
dq.offerFirst(x) / dq.offerLast(x)Add at the front / back of a DequeO(1) amortized
dq.pollFirst() / dq.pollLast()Remove from the front / back (null if empty)O(1)
dq.peekFirst() / dq.peekLast()Read either end (null if empty)O(1)
dq.push(x) / dq.pop()Stack-style: addFirst / removeFirstO(1) amortized / O(1)
q.size() / q.isEmpty()Element count / emptinessO(1)
q.contains(x) / q.remove(x)Search or remove one element by valueO(n)
new ArrayBlockingQueue<>(cap)Thread-safe bounded queue (producer/consumer)O(1)

A plain FIFO queue (BFS, scheduling, buffering)

→ ArrayDeque via the Queue interface

Fastest general-purpose queue; O(1) at both ends, compact memory.

A stack

→ ArrayDeque via the Deque interface (push/pop/peek)

Stack is a synchronized legacy class built on Vector; the Javadoc recommends Deque.

Sliding-window max/min, or "next useful candidate" problems

→ ArrayDeque<Integer> of indices as a monotonic deque

Both-end O(1) access; storing indices lets you tell when the front has left the window.

Fixed capacity, no allocation after start-up

→ Raw circular array (int[] + head/size)

No boxing, no resizing — what ring buffers in drivers and audio code do.

Process by priority, not arrival

→ PriorityQueue

It implements Queue, but poll() returns the smallest element, not the oldest (see the heaps chapter).

Several threads share the queue

→ ArrayBlockingQueue / LinkedBlockingQueue / ConcurrentLinkedQueue

ArrayDeque and LinkedList are not thread-safe.

null elements or List-style index access

→ LinkedList

ArrayDeque rejects null; LinkedList allows it and is also a List.

07 · recognise

Spot the pattern

"Process in the order they arrive", "first come first served", task scheduling

Plain FIFO queue (ArrayDeque)

Shortest path / fewest steps in an unweighted graph or grid, level-by-level

BFS with a queue

"Within the last X seconds / last k items", stream of timestamps

Queue as a time window: offer new, poll expired from the front

Max or min of every window of size k

Monotonic deque of indices

Shortest/longest subarray with a sum condition and negative numbers

Prefix sums + monotonic deque (LC 862)

"Design a circular queue / ring buffer / fixed-size buffer"

Array + head + size, indices wrap with %

Rotation games: "move the top card to the bottom", "pass the ball k times", Josephus

Simulate with a queue: poll then offer

Need both ends (add/remove front and back)

Deque (ArrayDeque)

08 · traps & tricks

Mistakes to avoid, tricks to use

traps

  • Using head == tail alone to test empty/full in a circular array — they look identical. Keep a size counter.
  • Computing (head - 1) % n in Java: % keeps the sign, so -1 % 5 == -1. Use (head - 1 + n) % n.
  • Using ArrayList.remove(0) as dequeue — it's O(n), turning an O(n) algorithm into O(n²).
  • Calling remove() / element() on a possibly empty queue — they throw. poll() / peek() return null instead (and unboxing that null into an int throws a NullPointerException).
  • Adding null to an ArrayDeque — it throws NullPointerException, because null is its "empty" signal.
  • Storing values instead of indices in a sliding-window deque — you then can't tell when the front left the window.
  • Comparing Integer objects from a deque with == (e.g. dq.peekFirst() == dq.peekLast()): outside the cached range −128…127, equal values are usually different objects, so == says false. Unbox or use .equals.
  • Forgetting tail = null when a linked queue becomes empty.

tricks

  • Default to Deque<Integer> q = new ArrayDeque<>(); and use offer/poll/peek — it works for queues, stacks and deques.
  • For monotonic deques say the invariant aloud: "values in the deque decrease from front to back, and all indices are inside the window."
  • When an operation is sometimes O(n) (two-stack queue), say amortized O(1) and give the reason: each element is moved at most once.
  • Simulation problems (tickets, cards, Josephus) are allowed to start as a queue simulation — then look for a counting shortcut.
  • Edge cases to test: empty queue, capacity 1, wrap-around exactly at the last index, k = 1, k = n, all-equal values.

09 · why & how

Questions you might be too shy to ask

Why not just use an array and shift everything left on dequeue?

Shifting moves every remaining element, so dequeue becomes O(n). With n operations that's O(n²). A circular array moves head instead of the data — O(1).

How does % capacity make the array "circular"?

(i + 1) % capacity is i + 1 for every index except the last one, where it becomes 0. So walking right from the last slot lands on the first slot — the array behaves like a ring.

Why is the rear (head + size − 1) % k and the next free slot (head + size) % k?

Start at head and count size items forward, wrapping with % k. The last item is size − 1 steps after head; the free slot is one step after that. Example: k = 4, head = 3, size = 2 → items sit in slots 3 and 0. Rear = (3 + 2 − 1) % 4 = 0, next free = (3 + 2) % 4 = 1. Simple hai — just count on a ring.

ArrayDeque or LinkedList for a queue?

ArrayDeque. Both give O(1) offer/poll, but ArrayDeque stores elements in one contiguous array (cache-friendly, no per-node object). Pick LinkedList only if you need nulls or List methods.

When should I use offer/poll vs add/remove?

They do the same job; only failure differs. offer/poll/peek return false/null; add/remove/element throw. In interviews offer/poll/peek is the common style. Just remember that poll() on an empty queue returns null, and assigning that to an int throws.

Queue or deque — when do I actually need a deque?

A queue adds at the back and removes from the front. That's all. A deque can add and remove at both ends. You need it when one problem uses both ends: in Sliding Window Maximum, old indices leave from the front and smaller values are popped from the back. In Java, ArrayDeque is both — declare it as Queue<Integer> or Deque<Integer> depending on what you need.

Is PriorityQueue a queue?

It implements Queue, but it is not FIFO — poll() returns the smallest element (by its comparator), not the oldest. It's a heap; see the heaps chapter.

What does "amortized O(1)" really mean for the two-stack queue?

A single pop can cost O(n) (when it pours in into out). But every element is poured at most once, so any sequence of n operations does at most about 4n stack operations in total — O(1) on average per operation, guaranteed, not just "usually".

Why does the monotonic deque store indices, not values?

You need two facts: the value (to compare) and the position (to know if it left the window). An index gives you both: nums[i] and i.

Sliding Window Maximum: why evict the front when it is <= i − k?

The window ending at i holds indices i − k + 1 to i. So index i − k is the first one outside it. Example: k = 3, i = 5 → the window is 3, 4, 5, and index 2 = 5 − 3 must go. With < i − k, index 2 would stay one step too long and could report a stale max. Only one index leaves per step, so an if (not a while) is enough for the front.

Popping smaller values from the back — can't that throw away a future answer?

No. Say 3 is in the deque and 6 arrives later. Every future window that still holds 3 also holds 6, because 6 is newer and leaves the window later. So 3 can never beat 6 again — dropping it loses nothing. Equal values work the same way: the newer copy stays longer, so popping with <= is safe.

Isn't a while loop inside a for loop O(n²)?

Not here. Count pops instead of loop iterations: each index is pushed once and can be popped at most once. Total pops ≤ n, so the whole run is O(n).

Time Needed to Buy Tickets: is the plain queue simulation good enough?

Check the constraints first. Here n ≤ 100 and tickets ≤ 100, so the line sells at most 100 × 100 = 10⁴ tickets — the simulation passes easily. If tickets could be 10⁹, it would time out and you'd need the counting formula. In an interview, give the simulation first (correct, easy), then the formula if they ask for better.

In an interview, can I just use ArrayDeque, or must I build the queue myself?

Depends on what is being tested. If the queue is a tool (BFS, sliding window, recent calls), use ArrayDeque — nobody wants you to rebuild it. If the problem is the queue (Design Circular Queue, Design Circular Deque), write the raw array with head and size. Not sure? Just ask: "Can I use ArrayDeque, or should I implement the buffer myself?"

10 · practice

Interview problems

Every problem has brute → better → optimal solutions in Java, with complexity and a dry run.

  1. #232
    Implement Queue using Stacksmust-do
    EasyAppleMicrosoftBloombergAmazonMeta
  2. #225
    Implement Stack using Queues
    EasyBloombergMicrosoftAmazon
  3. #622
    Design Circular Queuemust-do
    MediumAppleAmazonMicrosoftMeta
  4. #933
    Number of Recent Calls
    EasyGoogleAmazon
  5. #362
    Design Hit CounterLeetCode Premium
    MediumAppleUberWalmartGoogleAmazon
  6. #239
    Sliding Window Maximummust-do
    HardOracleAmazonNVIDIAMicrosoftGoogle+1
  7. #950
  8. #2073
  9. #641
  10. #862
    Shortest Subarray with Sum at Least K
    HardGoldman SachsAmazonGoogle

Also practise this here

Each problem has one home page in the course; these use this chapter's idea too. The note says what to look for.

  1. #994
    Rotting Oranges
    Mediumin Graphs

    Multi-source BFS: every rotten orange starts in the queue, and each minute is one level of polls.

  2. #1091
    Shortest Path in Binary Matrix
    Mediumin Graphs

    BFS on a grid: the first time the queue reaches the corner is along a shortest path.

11 · revise

The whole chapter on one screen

10 cards, about a minute. Read them the night before an interview, or right after the chapter to lock it in.

  1. what it is

    A queue is FIFO: offer at the rear, poll from the front — whoever waited longest leaves first. A deque opens both ends.

  2. use it when

    "In arrival order", BFS / fewest steps, "in the last X seconds", rotation games, and the max or min of every window.

  3. template

    Deque<Integer> q = new ArrayDeque<>(); → offer, poll, peek, isEmpty. The same object is a stack with push/pop/peek.

  4. cost

    offer / poll / peek: O(1) (amortized when ArrayDeque grows). ArrayList.remove(0) is O(n), so n dequeues become O(n²).

  5. template

    Ring buffer: head + size. Next free = (head + size) % k, rear = (head + size − 1) % k, dequeue = head = (head + 1) % k.

  6. trap

    head == tail means both empty and full — keep a size. Stepping left? (head − 1 + k) % k, because Java's % can be negative.

  7. trap

    Monotonic deque: store indices, not values — otherwise you can't tell when the front has left the window.

  8. cost

    Monotonic deque and two-stack queue: each element goes in once and comes out at most once → O(n) total, amortized O(1) each.

  9. pro tip

    Time window with growing timestamps? Offer the new one, poll expired ones from the front; the answer is the size.

  10. trap

    poll() on an empty queue returns null — int x = q.poll(); then throws. And ArrayDeque refuses null elements.

12 · recall

Flip, remember, repeat

card 1 / 15
recall

Queue vs stack: which element comes out next?

tap or Space to flip ↻

Question 1: Queue vs stack: which element comes out next?

13 · check yourself

Quick quiz

  1. Circular queue with capacity 5, tail = 4. You enqueue one element. What is tail now?

  2. Why keep a `size` field in a circular array queue?

  3. What does `poll()` return on an empty `ArrayDeque`?

  4. Queue from two stacks: what's the cost of n operations in total?

  5. Sliding window max deque (front → back) holds values [8, 5, 3]. Next value is 6. What does the deque hold after pushing it?

  6. Which is the best default Queue implementation in Java?

  7. Why does a monotonic deque run in O(n) despite a while loop inside a for loop?

  8. HitCounter (window = last 300 s): hit(100), then getHits(400). What does it return?