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:

  1. Linear queue — the naive baseline (and its flaw).
  2. Circular queue — fixes the flaw with modular arithmetic.
  3. Deque — both ends are fair game.
  4. 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) % capacity

That’s the whole idea. Two pointers chasing each other around a ring.

Try it: Circular Queue
Watch how front and rear wrap around the ring with modular arithmetic.
[0][1][2][3][4][5]size0/6

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:

TrickCostVerdict
Keep an explicit count variableOne extra intSimple, 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).

OperationWhat 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 problemsSliding Window Maximum keeps 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:

OperationTime
Enqueue (push)O(log n)
Dequeue (pop)O(log n)
Peek topO(1)
Build from n elementsO(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

VariantEnqueueDequeueOrderUse it when…
LinearO(1)O(1)*FIFOAlmost never — see Circular.
CircularO(1)O(1)FIFOFixed-size buffer, lots of enqueue/dequeue churn.
DequeO(1)O(1)Both endsYou want stack+queue, or sliding-window tricks.
PriorityO(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.