Queue Variants
The plain FIFO queue is the starting point. Real codebases use a handful of variants that bend the rules in useful ways. The four worth knowing:
- Linear queue — the naive baseline (and its flaw).
- Circular queue — fixes the flaw with modular arithmetic.
- Deque — both ends are fair game.
- Priority queue — order by importance, not arrival time.
1. Linear queue — and why nobody uses it
The straightforward version: two indices, enqueue at rear, dequeue at front. We covered this in Basic Operations.
The problem: once front moves forward, the slots behind it are dead weight. After a few cycles, your queue reports “Overflow” while half the array is empty.
capacity 5, after enqueue×5 + dequeue×3:
[_, _, _, x, x] front=3, rear=5
^ rear at the end
We can't enqueue another element — even though slots 0, 1, 2 are free.Fixes:
- Shift everything left on every dequeue — correct, but O(n) per dequeue. Brutal.
- Use a circular array — keep the same memory, just wrap.
The second one is what everyone actually does.
2. Circular queue
The fix is one operator: %. When rear reaches the end of the array, it loops back to slot 0 — and since front has already moved past those slots, they’re free to reuse.
enqueue: rear = (rear + 1) % capacity
dequeue: front = (front + 1) % capacityThat’s the whole idea. Two pointers chasing each other around a ring.
Enqueue past the end and watch rear wrap to slot 0 — as long as front has moved on, those slots are yours again.
”Empty vs full” — the classic bug
In a circular buffer, both empty and full can look like front == rear. There are two standard fixes:
| Trick | Cost | Verdict |
|---|---|---|
Keep an explicit count variable | One extra int | Simple, recommended. |
| Leave one slot permanently unused | ”Full” = (rear + 1) % cap == front. Saves a field but loses a slot. | Memory-tight cases only. |
We used the count approach in the previous page. It’s a tiny price for never having to think about the bug.
Circular buffers show up far outside DSA class — they’re the standard structure for audio buffers, network packet rings, kernel ring buffers (dmesg), and lock-free producer-consumer queues. The pattern is everywhere.
3. Deque (Double-Ended Queue)
A deque (“deck”) relaxes the queue rule: you can add or remove from either end in O(1).
| Operation | What it does |
|---|---|
pushFront(x) | Add x to the front |
pushBack(x) | Add x to the back |
popFront() | Remove and return the front |
popBack() | Remove and return the back |
peekFront() | Look at the front |
peekBack() | Look at the back |
A deque is a strict generalization — it can act as a stack or a queue. That’s why most modern standard libraries skip a dedicated queue type and just give you a deque (Python’s collections.deque, Java’s ArrayDeque, C++‘s std::deque).
Usage
When deques really shine
- Sliding-window problems —
Sliding Window Maximumkeeps a deque of indices in monotonic order, popping from both ends. - Undo + redo with bounded history — push new actions on the back, drop old ones from the front when the buffer fills.
- Work-stealing schedulers — each thread takes work from one end of its deque; thieves take from the other.
A deque is more powerful than a queue or stack, but that flexibility costs nothing. Defaulting to ArrayDeque / collections.deque is almost always the right call in interview code.
4. Priority Queue
A priority queue doesn’t care about insertion order. It hands you the highest-priority element next — regardless of when it arrived.
enqueue(task with priority 3)
enqueue(task with priority 1) ← higher priority (lower number)
enqueue(task with priority 5)
dequeue → task with priority 1 ← jumps the queue
dequeue → task with priority 3
dequeue → task with priority 5(Whether “higher priority” means lower or higher numbers is just convention. Min-heaps are most common in DSA problems.)
The implementation: a heap
You could build a priority queue with a sorted list — but enqueue would be O(n). You could use an unsorted list — but dequeue would be O(n).
The standard answer is a binary heap, which gives:
| Operation | Time |
|---|---|
| Enqueue (push) | O(log n) |
| Dequeue (pop) | O(log n) |
| Peek top | O(1) |
| Build from n elements | O(n) |
We’ll build heaps from scratch on Day 7. For now, here’s the practical usage:
Where priority queues live
- Dijkstra’s shortest path — always expand the closest unvisited node next.
- A* search — same idea, with a heuristic.
- Huffman coding — repeatedly merge the two smallest frequencies.
- Job scheduling — run the most urgent task next.
- Top-K problems — “find the K largest elements” → keep a min-heap of size K.
- Event simulation — process events in chronological order across millions of events.
Variant cheat sheet
| Variant | Enqueue | Dequeue | Order | Use it when… |
|---|---|---|---|---|
| Linear | O(1) | O(1)* | FIFO | Almost never — see Circular. |
| Circular | O(1) | O(1) | FIFO | Fixed-size buffer, lots of enqueue/dequeue churn. |
| Deque | O(1) | O(1) | Both ends | You want stack+queue, or sliding-window tricks. |
| Priority | O(log n) | O(log n) | By priority | ”Next most important” beats “next oldest”. |
* O(1) in time per operation, but burns array slots until you wrap to circular.
You now have the queue family in your head. Next up: where they’re actually useful.