Queues and Deques

Picture a print server handling jobs from a hundred office workstations. Jobs must print in the order they arrive. The first person to hit "print" should get their document first. This holds no matter how many others queue up afterward. This is the essence of fair scheduling. It is exactly the contract a queue guarantees: first in, first out (FIFO). Every operating system scheduler leans on this same simple idea. So does every network router buffering packets, every breadth-first search, and every message broker like Kafka or RabbitMQ. A deque (double-ended queue, pronounced "deck") generalizes this further. It lets you insert and remove from both ends in constant time. This flexibility makes deques the workhorse behind sliding-window algorithms and undo/redo systems. It also powers work-stealing schedulers, and even the humble `deque` container in the C++ and Python standard libraries. You must understand how to implement both structures efficiently. This means never secretly paying O(n) somewhere. That skill is a rite of passage for anyone building systems that manage ordered work. The naive approach to a queue uses a plain array and shifts elements after every removal. That costs O(n) per dequeue. That's the trap this article dismantles. The fix is the circular buffer: a fixed-size array where indices wrap around using modular arithmetic. This lets both ends of the queue advance without ever shifting data. We'll also cover the linked-list-based queue and deque. These trade array-resizing amortization for pointer overhead. They still keep the same O(1) worst-case guarantee per operation. By the end, you will be able to implement a queue and a deque from scratch. You'll use both array-backed circular buffers and doubly linked lists. You will reason precisely about why each operation is O(1). You'll trace through a worked example with wraparound. You will also recognize the classic bugs that trip up even experienced programmers. These include off-by-one errors in circular index math. They also include ambiguity between "full" and "empty."

Key takeaways
  • A queue supports FIFO access: `enqueue` adds to the back, `dequeue` removes from the front, both in O(1).
  • A deque supports O(1) insertion and removal at both ends: `pushFront`, `pushBack`, `popFront`, `popBack`.
  • Naive array queues cost O(n) per dequeue due to shifting; circular buffers fix this with modular index arithmetic.
  • Circular buffers must distinguish "full" from "empty" using a size counter, a sentinel slot, or a full/empty flag.
  • A doubly linked list gives O(1) worst-case operations at both ends without resizing, at the cost of pointer overhead.
  • Queues underlie BFS, task scheduling, producer-consumer pipelines, and streaming/sliding-window algorithms.
The core insight

A queue or deque achieves O(1) operations by tracking movable front/back pointers instead of physically shifting elements — circular arithmetic or linked pointers make "the front" a moving target rather than a fixed array slot.

The idea

A queue is an abstract data type maintaining a sequence with two restricted access points: elements are added only at the rear (`enqueue`) and removed only from the front (`dequeue`). This enforces First-In-First-Out order — the element that has waited longest is served next. Contrast this with a stack, which is Last-In-First-Out (LIFO), removing the most recently added element. A deque relaxes the queue's restriction: both ends allow insertion and removal. Formally, a deque supports `pushFront(x)`, `pushBack(x)`, `popFront()`, and `popBack()`, all in O(1) time. A deque is strictly more general than a queue (use only `pushBack`/`popFront`) and a stack (use only `pushBack`/`popBack`), which is why standard libraries often implement `deque` as the single underlying container and derive `queue` and `stack` as restricted interfaces over it. The central engineering challenge is avoiding the O(n) cost of shifting elements in a plain array. Two efficient strategies dominate in practice. The first is the circular buffer (also called a ring buffer): a fixed-capacity array where a `front` index and a `back` index each advance using arithmetic modulo the capacity, "wrapping around" to index 0 after reaching the end. The second is a doubly linked list, where `head` and `tail` pointers let you splice nodes in or out at either end in O(1) with no arithmetic at all, at the cost of per-element pointer storage and worse cache locality. Both strategies also generalize naturally to a dynamic-array-backed deque, which resizes (doubling/halving) exactly as a dynamic array does, giving amortized O(1) operations even without a fixed capacity.

How it works

Circular buffer queue

The circular buffer stores elements in a fixed array `A[0..N-1]`. Two indices, `front` and `back`, track the logical ends of the queue, and a `count` tracks how many elements are currently stored (this sidesteps the full-vs-empty ambiguity described below). `enqueue` writes at `back` and then advances `back = (back + 1) mod N`; `dequeue` reads at `front` and then advances `front = (front + 1) mod N`. ``` structure CircularQueue: array A[0..N-1] integer front = 0 // index of the front element integer back = 0 // index where the next element will be written integer count = 0 // number of elements currently stored function isEmpty(): return count == 0 function isFull(): return count == N function enqueue(x): if isFull(): error "queue overflow" // or: resize A to 2N and re-index A[back] = x back = (back + 1) mod N count = count + 1 function dequeue(): if isEmpty(): error "queue underflow" x = A[front] front = (front + 1) mod N count = count - 1 return x function peekFront(): if isEmpty(): error "empty queue" return A[front] ```

Circular buffer deque

Extending this to a deque adds symmetric operations at the front. The trick is that `pushFront` must move backward from `front`, which in modular arithmetic means adding `N - 1` (equivalently subtracting 1 and wrapping) rather than subtracting 1 directly, since most languages' `mod` on negative numbers behaves inconsistently. ``` structure CircularDeque: array A[0..N-1] integer front = 0 integer back = 0 integer count = 0 function pushBack(x): if count == N: resize() A[back] = x back = (back + 1) mod N count = count + 1 function pushFront(x): if count == N: resize() front = (front - 1 + N) mod N // step backward, safely A[front] = x count = count + 1 function popBack(): if count == 0: error "empty" back = (back - 1 + N) mod N x = A[back] count = count - 1 return x function popFront(): if count == 0: error "empty" x = A[front] front = (front + 1) mod N count = count - 1 return x ``` When capacity is exhausted, `resize()` allocates a new array of size `2N`, copies elements starting at `front` in logical order into positions `0..count-1` of the new array, and resets `front = 0`, `back = count`. This is the same doubling trick used for dynamic arrays, and it delivers amortized O(1) `pushBack`/`pushFront` even though a resize itself costs O(n).

Linked-list deque

The alternative representation uses a doubly linked list with `head` and `tail` pointers. Every node stores a value plus `prev` and `next` pointers. ``` structure Node: value prev, next structure LinkedDeque: Node head = null Node tail = null integer count = 0 function pushFront(x): n = new Node(value=x, prev=null, next=head) if head != null: head.prev = n else: tail = n // list was empty head = n count = count + 1 function pushBack(x): n = new Node(value=x, prev=tail, next=null) if tail != null: tail.next = n else: head = n // list was empty tail = n count = count + 1 function popFront(): if head == null: error "empty" x = head.value head = head.next if head != null: head.prev = null else: tail = null // list became empty count = count - 1 return x function popBack(): if tail == null: error "empty" x = tail.value tail = tail.prev if tail != null: tail.next = null else: head = null // list became empty count = count - 1 return x ``` This version has no amortization: every operation is O(1) in the worst case, not just on average, since there is never a resize. The price is a pointer-per-element memory overhead and weaker cache locality compared to the array version.

Figure
Circular buffer with wraparound: front and back indices chase each other around a fixed-size array
Capacity N = 8, count = 5 D E · · · A B C 0 1 2 3 4 5 6 7 front=5 back=2 logical order: A,B,C,D,E (wraps past index 7 → 0) dequeue() removes A, front becomes 6 — no data ever shifts, only indices move

Complexity & correctness

Time complexity. For the circular-buffer implementation: `enqueue`/`dequeue` (queue) and `pushFront`/`pushBack`/`popFront`/`popBack` (deque) are all O(1) worst case when capacity is fixed and not exceeded, because each operation performs a constant number of array accesses and one modular addition. When the buffer must grow, resizing costs O(n) for that one call but happens so rarely (doubling means the buffer must fill up again before the next resize) that the amortized cost per operation remains O(1) — the same aggregate/accounting argument used for dynamic array growth. Concretely, after a resize to capacity 2N2N, at least NN more insertions must occur before the next resize, so the total resizing work across mm operations is O(m)O(m), giving O(1)O(1) amortized per operation. For the linked-list implementation, every operation touches only `head`, `tail`, and at most one node's neighbors — O(1) worst case, with no amortization needed since there is no resizing at all.

Space complexity. The circular buffer uses O(N) space where N is the allocated capacity, which may exceed the actual element count by up to a factor of 2 right after a resize (this is the standard dynamic-array space/time tradeoff). The linked list uses O(n) space exactly proportional to the number of elements, but each element carries the overhead of two extra pointers, which typically costs more per element in practice despite the tighter asymptotic bound.

Correctness — circular buffer. The key invariant is: > At all times, the `count` elements logically in the queue occupy exactly the array slots > A[front],A[(front+1)  N],…,A[(front+count−1)  N]A[\text{front}], A[(\text{front}+1) \bmod N], \dots, A[(\text{front}+\text{count}-1) \bmod N], > in that order from front to back. Proof sketch by induction on the operation sequence. Initially `count = 0`, and the invariant holds vacuously. Assume it holds before an operation.

  • `enqueue(x)` writes `x` at index `back`. Since `back = (front + count) mod N` is maintained as an invariant consequence (verify: initially `front = back = 0 = (0+0) mod N`), writing there and incrementing `count` extends the occupied range by exactly one slot at the correct position, and `back` advances to `(front + count) mod N` for the new count — invariant preserved.
  • `dequeue()` reads `A[front]`, which by the invariant is the oldest remaining element (FIFO correctness), then advances `front` and decrements `count`, shrinking the occupied range from the left — invariant preserved.

The `isFull`/`isEmpty` checks guard against `count` leaving the valid range `[0, N]`, which is exactly what prevents overwriting live data (silent corruption) or reading stale data. This is why tracking `count` explicitly is essential: distinguishing "empty" (front == back, count = 0) from "full" (front == back, count = N) is impossible from `front` and `back` alone, since both conditions produce the identical index pattern. Some implementations instead always leave one slot empty (never let the buffer fill completely) so that `front == back` unambiguously means "empty" — this trades one slot of capacity for removing the need for a `count` field.

Correctness — linked list. The invariant is that the list, followed from `head` via `next` pointers, produces the elements in front-to-back order, and following from `tail` via `prev` pointers produces the reverse. Each push/pop operation splices in or unlinks exactly one node while restoring both pointer directions and the `head`/`tail` sentinels for the empty-list edge case — the case analysis for "list becomes empty" or "list was empty" in the pseudocode above handles the only subtlety.

Tradeoffs. Circular buffers are cache-friendly and memory-compact but need a resize strategy (or a hard capacity cap, undesirable for many applications). Linked lists have no resizing and no wasted capacity, but suffer from pointer-chasing cache misses and roughly 2–3× per-element memory overhead. In practice, production systems (Python's `collections.deque`, Java's `ArrayDeque`) use a variant of the circular buffer — often as a linked list of fixed-size arrays ("unrolled" or "chunked" deque) — to get array-like cache locality with linked-list-like resizing behavior.

Worked example

Trace a circular buffer of capacity N=4N = 4 processing this sequence of operations: ``` enqueue(10), enqueue(20), enqueue(30), dequeue(), enqueue(40), enqueue(50), dequeue(), dequeue() ```

StepOperationArray (index: value)frontbackcountReturned
0init[_,_,_,_]000—
1enqueue(10)[10,_,_,_]011—
2enqueue(20)[10,20,_,_]022—
3enqueue(30)[10,20,30,_]033—
4dequeue()[10,20,30,_]13210
5enqueue(40)[10,20,30,40]103—
6enqueue(50)wraps: [50,20,30,40]114—
7dequeue()[50,20,30,40]21320
8dequeue()[50,20,30,40]31230

Step 6 is the interesting one: `back` was 0 before the call, so `enqueue(50)` writes `A[0] = 50` (overwriting the now-logically-removed slot vacated when 10 was dequeued) and then `back = (0+1) mod 4 = 1`. Note `back` now numerically equals `front`, but `count = 4 = N` correctly signals "full," not "empty" — exactly the ambiguity the `count` field resolves. At step 7, `dequeue()` correctly returns 20 (not 50), since `front = 1` points at the true oldest surviving element; the value 50 sits at index 0 but is logically the newest element, third in line behind 20 and 30. This confirms the circular index arithmetic tracks logical order correctly even after wraparound, which is the crux of the structure's correctness.

Figure
State after step 6: buffer is full and wrapped, front and back coincide numerically but count=4 signals full
After enqueue(50): array = [50, 20, 30, 40] 50 20 30 40 idx 0 idx 1 idx 2 idx 3 front=1 (oldest: 20) back=1 (next write) front == back numerically, but count=4=N means FULL, not empty — count disambiguates

Common pitfalls & traps

PitfallWhy it happensFix
Confusing full and emptyBoth states can produce `front == back` if you only track two indicesTrack an explicit `count`, or always reserve one unused slot
Negative index from `front - 1`Many languages' `%` returns negative results for negative operands (e.g., `-1 % 4 == -1` in C/Java)Compute `(front - 1 + N) mod N`, not `(front - 1) mod N`
Resizing without re-normalizing indicesCopying the raw array (not in logical order) after resize scrambles element orderCopy starting at `front`, wrapping, into fresh indices `0..count-1`; then reset `front=0, back=count`
Forgetting to update `tail`/`head` on last-element removalPopping the only remaining node leaves a dangling `tail` or `head` pointing at a freed nodeExplicitly check "did the list become empty?" and null out the other pointer too
Treating queue and stack interchangeablyUsing `pushBack`/`popBack` when FIFO was intended silently reverses orderBe explicit about which end each operation touches; test order preservation, not just presence
Amortized ≠ worst-caseAssuming every single `enqueue` is O(1) in a hard real-time systemIf a hard per-call bound is required, use the linked-list deque, not the resizing array version

Pioneers & history

The queue as an abstract concept predates computing by centuries — it formalizes the everyday notion of "waiting in line," and the mathematical theory of queues (queueing theory) was developed by Agner Krarup Erlang in the early 1900s to analyze telephone traffic. In computer science, queues and deques entered the field as fundamental data structures alongside stacks during the formalization of data structures in the 1950s–60s, becoming a mainstay of algorithm textbooks once breadth-first search, scheduling algorithms, and buffer management made their utility concrete. The circular buffer specifically emerged from hardware and operating-systems practice, where fixed-size memory regions for I/O buffering needed a way to reuse space without shifting bytes — a pattern still visible today in network interface card ring buffers and audio/video streaming pipelines.

Origins · 1 minAgner Krarup Erlang and how this was discovered
AK
Agner Krarup Erlang
1878–1929 · Danish mathematician and engineer who founded queueing theory while working for the Copenhagen Telephone Company

Erlang modeled how telephone calls queue for a limited number of circuits, deriving formulas (the Erlang B and C formulas) still used today to size call centers, network buffers, and — by direct descent — the capacity planning behind modern queue-based systems like message brokers and load balancers.

History
- 1909: Erlang publishes his first paper on telephone traffic and queueing.
- 1950s: Stacks and queues are formalized as core data structures alongside the rise of high-level programming languages.
- 1968: Donald Knuth's The Art of Computer Programming, Vol. 1 gives a rigorous treatment of queues, deques, and their array/linked implementations.
- 1970s–80s: Circular buffers become standard in operating-system I/O subsystems and hardware device drivers.
- 2000s–present: Chunked/unrolled deques (Python's `collections.deque`, Java's `ArrayDeque`) combine array locality with linked-list resizing in production runtime libraries.

Where this matters

Operating system schedulers use queues (often multiple priority queues) to decide which process or thread runs next, ensuring fairness among waiting tasks. Network devices use circular buffers extensively: a network interface card's receive ring buffer stores incoming packets in a fixed-size array that the kernel drains continuously, exactly the enqueue/dequeue pattern described here, chosen specifically because it avoids the cost of shifting packet data. Message-queueing middleware — RabbitMQ, Apache Kafka, Amazon SQS — implements the FIFO queue contract at a distributed-systems scale, using techniques descended from the same core index-management ideas, layered with persistence and replication. Breadth-first search and its many derivatives (level-order tree traversal, shortest paths in unweighted graphs, and the frontier expansion at the heart of many search algorithms that eventually generalize toward structures like B-Trees for on-disk level-by-level access) rely on a plain FIFO queue to process nodes in order of distance from a source. Deques specifically power sliding-window algorithms (finding the maximum in every window of size kk in O(n) total time via a monotonic deque), undo/redo stacks that also support "redo-of-redo" from either end, work-stealing schedulers where idle threads steal tasks from the opposite end of a busy thread's deque to minimize contention, and the double-ended `deque` containers ubiquitous in Python, C++, and Java standard libraries.

Ask the tutor: How would you adapt a circular-buffer deque to support efficient random access (`get(i)`) in addition to O(1) push/pop at both ends?

Why not just use a regular array and shift elements on every dequeue?

Shifting every remaining element left by one position after each dequeue costs O(n) time, since in the worst case (a queue holding n elements) you touch all n of them. If you dequeue n times in a row, the total cost becomes O(n²), which is unacceptably slow for anything beyond small, infrequent queues. The circular buffer solves this by never physically moving data — it only ever moves the `front` and `back` indices, each of which is a single constant-time update using modular arithmetic. This is the entire motivation for preferring circular buffers (or linked lists) over naive shifting arrays whenever queue operations are frequent or the queue is large.

Is a deque strictly more powerful than both a stack and a queue?

Yes, structurally: a deque supports insertion and removal at both ends, so restricting yourself to only `pushBack`/`popBack` gives you a stack (LIFO), and restricting to `pushBack`/`popFront` gives you a queue (FIFO). This is why many standard libraries — Python's `collections.deque`, Java's `ArrayDeque` — implement a single efficient deque and let `Stack` and `Queue` be thin, restricted interfaces or aliases over it. The one caveat is that a deque implementation carries slightly more bookkeeping (two active ends instead of one), so a purpose-built stack or queue can occasionally be marginally simpler or faster in constant-factor terms, though the asymptotic complexity is identical.

How does a monotonic deque solve the sliding-window maximum problem?

A monotonic deque stores indices of array elements in decreasing order of their values, from front to back. As the window slides right, you first pop from the back any indices whose values are smaller than the new element (they can never be the maximum again, since the new element is both larger and more recent), then push the new index at the back. You also pop from the front any index that has fallen outside the current window. The front of the deque is then always the index of the current window's maximum. Each index is pushed and popped at most once, giving O(n) total time for all n windows — an application only possible because both push and pop are O(1) at both ends.

Why do languages like Python implement deque as a "chunked" structure rather than a single array or plain linked list?

A plain circular array requires occasional O(n) full-array copies during resize, and a plain linked list wastes memory and cache performance on per-node pointers. A chunked (or "unrolled") deque stores a doubly linked list of fixed-size array blocks (e.g., 64 elements each). Pushing or popping within a block is array-fast and cache-friendly; only when a block fills up or empties does the structure allocate or free a whole block, which is a rare, cheap, O(1)-amortized event rather than a full-array copy. This hybrid captures most of the cache benefits of arrays and most of the resizing benefits of linked lists.

Can a deque be implemented with a single dynamic array without ever wrapping indices?

Not efficiently for both ends simultaneously. If you only grow at the back (like a standard dynamic array), `pushFront` still requires shifting every existing element right by one, costing O(n). Some implementations sidestep this by leaving "slack" space at both ends of the underlying array and shifting the whole block only when slack runs out on one side — but this is effectively a manual, less elegant reinvention of the circular buffer's wraparound trick, since it still needs modular-style bookkeeping to reuse freed slack space efficiently over many operations.

How does a queue relate to breadth-first search?

Breadth-first search explores a graph level by level: all nodes at distance kk from the source are visited before any node at distance k+1k+1. A FIFO queue enforces exactly this order — when a node is dequeued and its unvisited neighbors are enqueued, those neighbors are guaranteed to be at distance one greater than the dequeued node, and because they entered the queue after all other distance-kk nodes, they will only be processed after those nodes are exhausted. Using a stack instead (LIFO) would produce a depth-first traversal instead, visiting one branch deeply before backtracking, so the queue's specific ordering guarantee is what makes BFS's level-by-level behavior possible.

What happens if you call dequeue or popFront on an empty queue/deque?

This is an underflow error, and a correct implementation must check for it explicitly rather than silently returning garbage or crashing with an obscure error (like dereferencing a null `head` pointer or reading an uninitialized array slot). Most implementations either raise an explicit exception/error (as in the pseudocode here), return a sentinel value paired with a boolean success flag, or offer a separate `isEmpty()` check that callers are expected to use before calling `dequeue`. Silently proceeding without this check is a common source of hard-to-diagnose bugs, especially in producer-consumer systems where the queue can legitimately become empty during normal operation.

Before this

Where this leads