Skip to content

Queues and deques

coreCommon atGoogleAmazonCloudflareDatabricksAnthropicRead firstArraysStacksPractise in the Sandbox →

A queue adds at the back and removes from the front (FIFO), both in O(1). A deque (double-ended queue) does O(1) at both ends. The classic implementation is a ring buffer: a fixed array plus a head index and a size, with positions wrapping around % capacity, so nothing ever shifts.

A queue is a line at a coffee shop. New people join at the back; the barista serves the front. Whoever has waited longest goes next. That fairness is why queues run schedulers, print spoolers, message brokers and network buffers.

It’s also why breadth-first search uses one: if you process places in the order you discovered them, you explore everything 1 step away before anything 2 steps away, so the first time you reach a node is along a shortest path.

The implementation trap: an array is great at its end, terrible at its front. Removing a[0] slides everything left (O(n)). Two fixes:

  • Don’t move the data, move the index. Keep head pointing at the oldest item. Dequeue is head += 1.
  • Reuse the freed space. When the back reaches the end of the array, wrap around to slot 0, which the front has already vacated. That’s a ring buffer (also called a circular buffer).

The ring and the flat row are the same memory. Enqueue until the tail wraps past the end, dequeue a few, then fill it completely. Notice that head and the next-write slot coincide both when it’s empty and when it’s full: that’s why you track size.

Ring buffer: a queue that never shifts
sandbox
012113H1245Tsize 2/6H = head (oldest)T = next write
the same memory, as a flat array
H
·
·
·
11
12
·
012345
T
head3
size2
tail(3+2)%6 = 5
queue[11, 12]
live itemsjust writtenjust readblocked: full
log (newest first)
Nothing yet. Try an operation.
The problem says… Why a queue fits
“fewest steps / moves / hops”, “level by level” BFS: a FIFO queue visits nodes in order of distance. See BFS.
“process in arrival order”, “tasks wait their turn” FIFO is the fairness rule. See scheduling.
“in the last N seconds / last N events” A queue of timestamps: append new ones at the back, drop expired ones from the front.
“fixed-size buffer”, “keep only the latest N” Ring buffer: bounded memory, O(1) per operation.
“add or remove at both ends”, “sliding window max/min” A deque. See monotonic queue.
producer/consumer, “workers pull jobs” A (blocking, thread-safe) queue between them. See concurrency.
  1. Allocate an array of capacity slots. Keep head (index of the oldest item) and size.
  2. The next write goes at (head + size) % capacity. Writing increments size.
  3. A read takes slots[head], then head = (head + 1) % capacity and size -= 1.
  4. size == 0 means empty; size == capacity means full. Decide what “full” does: reject the write, grow (copy into a bigger array, unrolling the wrap), block the producer, or overwrite the oldest item.
  1. Enqueue the start and mark it seen with distance 0.
  2. Dequeue a node; for each neighbour not seen yet, record distance + 1, mark it, enqueue it.
  3. Repeat until the queue is empty. Nodes leave the queue in non-decreasing distance order.
distances.py
from collections import deque
def distances(graph: dict[int, list[int]], source: int) -> dict[int, int]:
"""Fewest edges from `source` to every reachable node of an unweighted graph."""
dist = {source: 0}
q = deque([source]) # 1. FIFO: nodes come out in order of distance
while q:
node = q.popleft() # 2. O(1) at the front (list.pop(0) would be O(n))
for nxt in graph.get(node, []):
if nxt not in dist: # 3. mark when enqueuing, so nothing is queued twice
dist[nxt] = dist[node] + 1
q.append(nxt)
return dist
if __name__ == "__main__":
g = {0: [1, 2], 1: [3], 2: [3, 4], 3: [5], 4: [5], 5: []}
assert distances(g, 0) == {0: 0, 1: 1, 2: 1, 3: 2, 4: 2, 5: 3}
assert distances(g, 5) == {5: 0}
assert distances({}, 7) == {7: 0}
print("ok")

Moving items from the front to the back is the queue version of “go around the circle”.

elimination_order.py
from collections import deque
def elimination_order(n: int, k: int) -> list[int]:
"""People 1..n stand in a circle; every k-th is removed. Return the removal order."""
circle = deque(range(1, n + 1))
order: list[int] = []
while circle:
circle.rotate(-(k - 1)) # 1. move k-1 people from the front to the back, O(k)
order.append(circle.popleft()) # 2. the k-th is now at the front: remove it
return order
if __name__ == "__main__":
assert elimination_order(7, 3) == [3, 6, 2, 7, 5, 1, 4]
assert elimination_order(5, 1) == [1, 2, 3, 4, 5]
assert elimination_order(1, 4) == [1]
assert elimination_order(0, 2) == []
print("ok")

To answer “how many events in the last W seconds?”, keep event timestamps in a queue in arrival order. On each new event or query at time now, pop from the front while the front is too old (outside (now − W, now]), then the queue length is the answer. Each timestamp is enqueued once and dequeued once, so the cost is amortized O(1) per event, the same argument as a sliding window. If events can arrive out of order or memory must be bounded regardless of rate, the plain queue stops being enough; that’s where bucketed counters (one slot per second, in a ring) come in. See rate limiting.

Python TypeScript C#
queue / deque collections.deque none built in: array + head index, or write a ring buffer Queue<T> (FIFO only)
enqueue q.append(x) q.push(x) q.Enqueue(x)
dequeue q.popleft() q[head++] (avoid q.shift(), O(n)) q.Dequeue() / TryDequeue(out x)
peek front q[0] q[head] q.Peek() / TryPeek(out x)
both ends appendleft, pop, rotate(k) ring buffer no built-in deque: LinkedList<T> (AddFirst, RemoveLast…) or a ring buffer
bounded deque(maxlen=n) drops from the other end when full write it write it, or Channel.CreateBounded<T>
thread-safe queue.Queue (blocking) n/a (single thread + event loop) ConcurrentQueue<T>, Channel<T>, BlockingCollection<T>

Inside, .NET’s Queue<T> is a ring buffer over an array that doubles when full; CPython’s deque is a doubly linked list of fixed-size blocks. Both make front operations O(1).

Operation deque / ring buffer array with pop(0) / shift() linked list
enqueue (back) O(1) (amortized if it grows) O(1) amortized O(1)
dequeue (front) O(1) O(n) O(1)
peek O(1) O(1) O(1)
index q[i] O(1) ring buffer; Python deque[i] is O(n) toward the middle O(1) O(n)
memory contiguous, cache friendly contiguous one node + pointers per item
Draining n items: front-removal from an array vs a real queue
drag the slider
list.pop(0) / arr.shift()
523,776
deque / Queue<T> / head index
1,024
Removing from the front of an array slides every remaining element left: n(n−1)/2 moves in total, O(n²). A deque, a ring buffer or a moving head index does one step per dequeue: O(n). At n = 220 that's about 550 billion moves versus 1 million. (V8 sometimes avoids the copy for small arrays, but you can't count on it.)
  • Bounded / overwriting buffer. Fixed capacity; a write when full either fails or replaces the oldest item. Common for logs, metrics, audio and network buffers.
  • Blocking queue. put waits when full, get waits when empty. The textbook producer/consumer coordination.
  • Priority queue. Not FIFO at all: removes the smallest key first. That’s a heap.
  • Multi-source BFS. Start with several nodes in the queue at distance 0. Everything spreads from all of them at once.
  • 0-1 BFS. Edges of weight 0 or 1: push weight-0 neighbours to the front of a deque and weight-1 to the back. Shortest paths in O(V + E).
  • Two-stack queue. Push onto an “in” stack, pop from an “out” stack, refilling it only when empty. Amortized O(1).
  • list.pop(0) / arr.shift() in a loop. Quietly O(n²). In Python use deque; in TS use a head index.
  • Marking BFS nodes when dequeued instead of when enqueued. The same node gets queued many times; on dense graphs that’s a blow-up.
  • Off-by-one wraparound. Use % capacity everywhere, and remember that in JS/C#, % of a negative number is negative (-1 % 5 === -1). For “one step back”, use (i - 1 + capacity) % capacity.
  • Empty vs full ambiguity with only head and tail. Track size.
  • Time windows with an inclusive/exclusive mix-up. “Last 60 seconds” at time 100: is an event at 40 in or out? Ask, then write the comparison once.
  • Ask what happens when the buffer is full (reject, block, grow, overwrite?) and when it’s empty (return null, throw, block?). Those are API decisions, and interviewers want you to raise them.
  • Say why FIFO: “BFS needs nodes in discovery order so the first visit is the shortest.”
  • Call out the language gap: “TypeScript has no deque; I’ll use a head index so dequeue stays O(1).”
  • Follow-ups: “make it thread-safe” (a lock around head/size, or a lock-free single-producer/single-consumer ring), “make it unbounded” (grow and copy, unrolling the wrap), “events arrive out of order”, “memory must stay constant”.
Check yourself
pick one
A ring buffer has capacity 8, head = 6 and size = 5. At which index does the next enqueue write?
Check yourself
pick one
A ring buffer stores only head and tail (next write). head == tail. What can you conclude?
Check yourself
pick one
Your Python BFS uses a list and q.pop(0). On a grid of 1,000 × 1,000 cells, roughly how much work goes into the pops alone?

Classic textbook exercises, not gym questions:

  • Level-order traversal of a binary tree, one list per level.
  • Minimum knight moves between two squares on a chessboard (BFS over positions).
  • Implement a queue with two stacks; then a stack with one queue.
  • Open the lock: fewest single-wheel turns to reach a 4-digit code while avoiding forbidden codes (BFS over states).