Sparround

Queue, Deque and Circular Queue

A queue is a collection working on the FIFO (First In, First Out) principle: the first in is the first out. The mental model is a checkout line.

The core operations, all of which should be O(1):

  • enqueue(x) — add an element at the back
  • dequeue() — remove and return the element at the front
  • peek()/front() — look at the front element
  • isEmpty(), size

The only difference from a stack is which end you remove from: a stack adds and removes at the same end (LIFO), a queue at opposite ends (FIFO). That small difference completely changes the character of an algorithm — swap the stack for a queue in the same traversal code and DFS becomes BFS.

Why a plain array is a bad queue. The most common mistake is the array.push() + array.shift() pair.

push is amortized O(1), but shift is `O(n)`: removing the first element forces every remaining element one slot left, because the array is contiguous. Pushing n elements through such a queue costs O(n²) — at 100,000 elements that is a plainly visible slowdown.

There are two correct solutions:

  • A linked list — remove at the head, append at the tail; both O(1), no shifting
  • A circular buffer — a fixed-size array with head and tail indices; instead of shifting on removal, head simply advances and wraps around at the end of the array ((head + 1) % capacity). Freed slots get reused and cache locality stays as good as an array's

Modern libraries' ArrayDeque is built exactly on a circular buffer — which is why it is the best default for a queue in practice.

StructureAdd/remove at the frontAdd/remove at the backIndex accessTypical use
StackO(1) / O(1) (same end)NoUndo, DFS, brackets
Queue (FIFO)— / O(1)O(1) / —NoBFS, task queues
DequeO(1) / O(1)O(1) / O(1)Usually yes (`ArrayDeque`)Sliding window, undo+redo, work stealing
Array (with push/shift)O(n) / O(n)amortized O(1) / O(1)O(1)**Unsuitable** as a queue
Circular bufferO(1) / O(1)O(1) / O(1)O(1) (with an offset)Fixed-size buffers, audio, logs
Priority queue (heap)Removal O(log n)Insertion O(log n)NoScheduling, Dijkstra

A deque (double-ended queue) supports O(1) insertion and removal at both ends. One deque can serve as both a stack and a queue, which is why modern libraries offer ArrayDeque instead of a separate Stack class.

A circular queue is the fixed-capacity variant: when the buffer fills, either the new element is rejected or the oldest is overwritten (a ring buffer). That behaviour is a deliberate choice — it guarantees bounded memory.

Real-world uses:

  • BFS — level-by-level traversal of graphs and trees, finding shortest paths. You cannot write BFS without a queue.
  • Task queues / job queues — background work, message brokers (RabbitMQ and Kafka are essentially distributed queues), printer spools
  • Rate limiting — in the sliding-window algorithm, timestamps of requests in the last n seconds live in a deque: new requests are appended at the back and expired ones removed from the front
  • Producer-consumer — a buffer between threads
  • Sliding window maximum — an O(n) solution with a monotonic deque
  • Audio/video buffers, sensor data, circular logs — fixed-size ring buffers

Interview tip. The most common question: "What's the difference between BFS and DFS?" The best answer starts from the structure: BFS uses a queue, DFS uses a stack — the code is nearly identical, only the end you take elements from changes. Then give the consequences: BFS goes level by level and finds the shortest path in an unweighted graph; DFS follows one branch to the end and can be more memory-efficient.

Second classic: "How would you implement an efficient queue in JavaScript?" The expected answer: push + shift is `O(n)`, so I'd use either an array with two indices (advance a head index and compact periodically), a linked list, or a circular buffer. Knowing that shift is O(n) is the actual checkpoint of this question.

The most common mistakes: using shift() as a dequeue and then claiming O(1); being unable to distinguish "full" from "empty" in a circular buffer (head == tail holds in both — the fix is either a size counter or leaving one slot unused); and marking a node "visited" in BFS when dequeuing rather than when enqueuing, which lets the same node enter the queue many times.

📚 Sources and documentation