DSA · Linear structures
The queue that never moves an element
Serve people in the order they arrived. Build that on a plain array and every dequeue drags the whole queue one slot left. One modulo operator removes the dragging, and opening the two doors a queue keeps shut turns it into a deque that answers sliding window maximum in a single pass.
Wrap front and rear around a 5-slot ring →01 The idea
Serve in the order people arrived
Stand in the queue at a railway ticket counter. You join at the back, the clerk serves the front, and nobody in the middle is ever touched. That is the whole contract of a queue: two ends, one direction of travel, no reordering. A print spooler works this way, so does the ready queue of processes waiting for the CPU, so does the frontier of a breadth-first search.
The interesting part is not the idea, it is the storage. A queue grows at one end and shrinks at the other, so the region it occupies inside an array keeps drifting to the right. Do nothing about that drift and you either run out of array while the front sits empty, or you pay to slide everything back. The fix is to stop thinking of the array as a line and start thinking of it as a ring, where slot 4 is followed by slot 0.
02 Worked example
Thirteen operations on a 5-slot ring
Capacity 5. The stream of values arriving is 9 4 6 2 8 3 5. The queue starts with front = 0, rear = −1 because nothing has been written yet, and size = 0. Every row below is one operation, except the first, which folds the five opening enqueues into a single line — thirteen operations in nine rows. The rose box is the slot that operation touched, and the note on the right gives the index arithmetic. The 3 that gets refused is retried later, once two dequeues have freed room for it. Watch rear walk off the end on the second enq 3 row — the one that succeeds — and front do the same on the last dequeue.
Both indices walked off the end and came back to slot 0, and not one value was copied from one slot to another. All thirteen operations did constant work — including the sixth, which was refused in O(1) the moment size was seen to equal the capacity. That is the insight the code below encodes: instead of moving the data to fit a fixed window, move the window around the data.
03 The code
Shift the data, or move the indices
Both versions store the queue in one array of size cap and expose the same two operations. The naive one keeps the front pinned at index 0, which means every dequeue has to slide the survivors down. The circular one lets the front drift and brings it back with a modulo. Compare the two dequeue bodies: one contains a loop, the other does not, and that single difference is the whole lesson.
Naive array queue — dequeue shifts
n = 0 # how many are storedenqueue(x): if n == cap: return FULL a[n] = x; n = n + 1dequeue(): if n == 0: return EMPTY x = a[0] for i in 0 .. n-2: a[i] = a[i+1] # slide n = n - 1; return x
Circular queue — indices wrap
front = 0; rear = -1; size = 0enqueue(x): if size == cap: return FULL rear = (rear + 1) % cap # 4 -> 0 a[rear] = x; size = size + 1dequeue(): if size == 0: return EMPTY x = a[front] front = (front + 1) % cap # 4 -> 0 size = size - 1; return x
Why the shift is the whole problem. In the trace, the five successful dequeues happened while the queue held 5, 4, 5, 4 and 3 elements, so the naive version would have copied 4 + 3 + 4 + 3 + 2 = 16 values to achieve exactly what the circular version did with five additions. One dequeue is O(n), so draining a queue of n elements is O(n²). On a million-element buffer a single dequeue copies a million values.
Why keep a size counter. Without it, full and empty are indistinguishable. In both states rear ends up sitting one step behind front, and the indices alone cannot tell you which of the two you are in. A counter answers isEmpty and isFull in O(1). The standard alternative is to keep one slot permanently unused so the two states can never collide — which means a capacity-5 array stores 4 elements.
Why a linked queue needs neither. Hold a head and a tail node pointer: enqueue links a new node after tail, dequeue unlinks head. Both are O(1) with no capacity and no modulo, and it grows instead of refusing work. You pay one pointer per element and lose the contiguous memory, so the fixed ring wins whenever you know the maximum size in advance.
05 Cheat sheet
Costs worth memorising
| Operation | Cost | Note |
|---|---|---|
| enqueue, circular queue | O(1) | One modulo, one write, one increment. No element moves. |
| dequeue, circular queue | O(1) | front steps forward mod cap; the old value stays in its slot until overwritten. |
| dequeue, naive array queue | O(n) | Slides every survivor left. The lesson script pays 16 copies for 5 dequeues, and draining n elements is O(n²). |
| peek front or rear | O(1) | One array read. Not the same as dequeue — it does not move front. |
| push_front / pop_back, deque | O(1) | (front − 1 + cap) % cap and (rear − 1 + cap) % cap. The + cap keeps the result non-negative. |
| search for a value | O(n) | A queue exposes two ends only. If you need to look inside, you picked the wrong structure. |
| space, array ring | O(cap) | Allocated up front, whether or not it is full. One allocation, contiguous, cache-friendly. |
| space, linked queue | O(n) | One node and one pointer per element, allocated on demand. No capacity limit. |
| sliding window maximum, deque | O(n) | Each index is pushed once and removed once, so the total deque work is linear in n and does not depend on k. |
| sliding window maximum, brute force | O(n·k) | Recomputes the maximum of every window from scratch. At n = 1,000,000 and k = 1,000 that is about a billion comparisons. |
06 Where & why
Queue, deque, or something else
A queue is not a fast structure, it is a fair one. Nothing about it beats a plain array on raw speed; what it buys you is the guarantee that whatever arrived first is served first, in constant time, with no scanning and no reordering. Choose it when arrival order is part of the correctness of your program, not when you merely want a list.
Reach for it when…
Use something else when…
| Situation | Better choice | Why |
|---|---|---|
| You need the largest or smallest item next, not the oldest | Priority queue / binary heap | O(log n) insert and extract keeps order by value. Scanning a queue for its maximum is O(n) every time. |
| You need the newest item first | Stack | Same O(1) costs, opposite discipline. DFS, undo history and expression parsing all want LIFO. |
| You need indexing, or search by value | Array or hash table | A queue exposes only its two ends, so reaching an arbitrary element costs O(n). |
| The maximum size is unknown and can spike | Linked queue or a growable deque | A fixed ring refuses the enqueue when it is full. A linked queue grows instead of dropping work, at one pointer per element. |
07 Interview questions
What interviewers actually ask
Eleven questions in the order an interview escalates: what it is, what it costs, why the naive version is slow, and then the deque follow-up that separates candidates who have only read about it.
What is a queue, and how is it different from a stack?
What are the queue operations, and what does each one cost?
Why is dequeue O(n) if you just store the queue in a plain array?
How does a circular queue fix that?
In a circular queue, how do you tell full apart from empty?
Array-based queue or linked-list queue — which would you pick?
What is a deque, and what does it give you that a queue does not?
How would you implement a queue using two stacks?
Explain sliding window maximum with a deque, and why it is O(n).
A queue and a priority queue are both called queues. Are they the same thing?
Where do queues actually show up in real systems?
08 Practice problems
Six problems, in order of bite
Every one is solvable with what is above — the two ends, the modulo step, the size counter and the deque rule from the console — plus, in one case, a plain tally of how often each character has been seen.