Stacks and Queues, Shown Doing the Jobs They Exist For
Stacks and queues are the two structures most often taught purely as definitions. Last-in-first-out; first-in-first-out; here is a picture of some plates. It is accurate and it explains nothing, because knowing the rule tells you nothing about why anybody wanted it.
Both rules are worth having for the same reason: each makes exactly one question cheap. A stack makes what was the most recent thing? instant. A queue makes what has been waiting longest? instant. The way to see why that matters is to watch each one do a job.
A stack, matching brackets
Every closing bracket has exactly one candidate partner: the most recent opener that is still unclosed. That is a stack’s only trick, which makes this the job stacks were born for.
A stack answers exactly one question quickly: what was the most recent thing? That is the question bracket matching asks on every closing bracket, because the only opener a closer could possibly belong to is the most recent unclosed one.
Scanning ([)] pushes ( then [, and then meets
) at position 2 — where the top of the stack is
[, not (. Counting brackets would accept this string, since it has
two openers and two closers; a stack rejects it, because it remembers the order. Scanning
(() gets all the way to the end and fails differently: reached the end with 1 bracket still open.
Step through the scan. Openers are pushed; each closer must match whatever is on top of the stack. Try all 4 inputs — one is balanced, one crosses its pairs, one never closes.
- Characters read
- 0
- Brackets still open
- 0
- Top of stack
- —
The same scan, as text
Input
Stack
Position
Step through each input. The highlighted stack entry is the top — the only entry a closing bracket ever looks at.
Run ([)] and watch where it fails. This is the case worth understanding, because a simpler
approach accepts it: counting. Two openers, two closers, balanced — and wrong. Counting knows
how many brackets there are and nothing about which is inside which. The stack fails at position 2,
and it fails for a reason it can state: the ) arrived while [ was still open.
Then run ((). It fails differently — nothing mismatched at all, the scan simply ran out of input
with one bracket still on the stack. Both are unbalanced strings; only a structure that remembers
order can tell you which kind of unbalanced.
A queue whose values never move
Here is the problem the array-versus-linked-list comparison leaves open. Removing from the front of an array shifts everything behind it forward, which is linear work on every single removal — and taking from the front is the definition of a queue. So how is a fast array-backed queue possible?
By not moving the data. Keep two indices instead, and let them wrap around the end.
Taking an item from the front of an ordinary array means shifting everything behind it
forward one slot. A ring buffer avoids that by never moving the values at all: it keeps two
indices, head for where the next item comes out and tail for where
the next one goes in, and lets both wrap around the end of a fixed array.
After filling 5 slots of a 6-slot buffer and taking 3 items out, head sits at index 3 and tail at 5. Two more additions push tail to index 1 — it has wrapped past the end and back to the start, writing into slots the earlier items have already vacated. Every enqueue and every dequeue is a constant amount of work no matter how full the buffer is, because nothing is ever shifted.
Add and remove items and watch head and tail move. The values themselves never shift position — that is the entire trick.
- Items held
- 0
- head index
- 0
- tail index
- —
The same buffer, as text
In order
Raw slots
Position
Add several items, take a few, then keep adding. Watch tail roll past the end back to index 0 while the values sit exactly where they were written.
Add four, remove three, then add more, and tail wraps to 0 and starts writing into slots the
earlier items have vacated. The array is being used as a circle. Nothing is ever shifted, so both
operations are constant work regardless of how many items are held — which is exactly what the
plain-array version failed to deliver.
This is not a toy. Ring buffers are how audio pipelines, network stacks, keyboard input and log collectors move data between a producer and a consumer, precisely because the cost per item does not grow with the backlog.
Both, as programs
PAIRS = {")": "(", "]": "[", "}": "{"}
def check(text):
"""Returns None if balanced, or (position, reason) for the first thing that is wrong."""
stack = []
for i, ch in enumerate(text):
if ch in "([{":
stack.append((ch, i))
elif ch in PAIRS:
if not stack:
return i, f'"{ch}" closes nothing'
top, at = stack.pop()
if top != PAIRS[ch]:
return i, f'"{ch}" does not match "{top}" opened at {at}'
if stack:
return len(text), f"{len(stack)} bracket(s) never closed"
return None
for text in ["([]{})", "([)]", "(()", "sum(a[i], b[j])"]:
problem = check(text)
if problem is None:
print(f"{text!r:20} balanced")
else:
at, why = problem
print(f"{text!r:20} FAILS at {at}: {why}")
print()
SIZE = 4
slots, head, tail, count, value = [None] * SIZE, 0, 0, 0, 1
print(f"{'operation':<10} {'head':>4} {'tail':>4} {'held':>4} slots")
for op in ["add", "add", "add", "take", "take", "add", "add", "take", "add"]:
if op == "add" and count < SIZE:
slots[tail] = value
tail = (tail + 1) % SIZE
count += 1
value += 1
elif op == "take" and count > 0:
slots[head] = None
head = (head + 1) % SIZE
count -= 1
shown = " ".join("." if s is None else str(s) for s in slots)
print(f"{op:<10} {head:>4} {tail:>4} {count:>4} {shown}")'([]{})' balanced
'([)]' FAILS at 2: ")" does not match "[" opened at 1
'(()' FAILS at 3: 1 bracket(s) never closed
'sum(a[i], b[j])' balanced
operation head tail held slots
add 0 1 1 1 . . .
add 0 2 2 1 2 . .
add 0 3 3 1 2 3 .
take 1 3 2 . 2 3 .
take 2 3 1 . . 3 .
add 2 0 2 . . 3 4
add 2 1 3 5 . 3 4
take 3 1 2 5 . . 4
add 3 2 3 5 6 . 4 Read the slots column down the page. By the seventh row it says 5 . 3 4 — value 5 is sitting at
index 0 while 3 and 4 are still at indices 2 and 3, exactly where they were written. The queue’s
order is 3, 4, 5, and the array’s order is nothing of the sort. That gap between the logical
sequence and the physical layout is the whole idea, and it is why the operation costs the same
whether the buffer holds two items or two thousand.
The connection worth carrying away
Neither structure stores anything an array could not. What each provides is a restriction, and the restriction is what makes one question cheap. That is a pattern rather than a pair of facts:
- The bracket matcher needs the most recent unclosed opener — a stack, and the answer is on top.
- A traversal that must reach nearer nodes first needs the longest-waiting node — a queue, and the answer is at the head. That is exactly why breadth-first search finds shortest paths and depth-first does not.
Choosing a structure is choosing which question you want to be free.
What this page simplifies
- The ring buffer never grows. When it fills, this one refuses. Real implementations either block the producer, drop the oldest item, or allocate a larger buffer and copy across.
- One consumer, one producer, one thread. Ring buffers are popular precisely because they can be made safe for concurrent use, and that is a substantially harder problem than the indices shown here.
- Bracket matching only checks brackets. A real parser also has to know that brackets inside a string literal or a comment are not brackets at all.
- Nesting depth is unbounded here. A stack that grows without limit is a denial-of-service surface in a parser reading untrusted input, so real ones cap it.