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
headandtailindices; instead of shifting on removal,headsimply 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.
| Structure | Add/remove at the front | Add/remove at the back | Index access | Typical use |
|---|---|---|---|---|
| Stack | — | O(1) / O(1) (same end) | No | Undo, DFS, brackets |
| Queue (FIFO) | — / O(1) | O(1) / — | No | BFS, task queues |
| Deque | O(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 buffer | O(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) | No | Scheduling, 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
nseconds 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
- Kotlin ArrayDequeofficialkotlinlang.org
- Dart Queueofficialapi.dart.dev