◈ python mapVol 4 · Ch 52/62
Volume 4 Python, from the metal up · chapter 52

52The queue — first in, first out

In Chapter 51 we built the stack — last in, first out. It always hands you back the newest thing you gave it. Here we build its mirror image, and it turns out to be everywhere. A queue is how your operating system shares the CPU. It is how a search sweeps a maze level by level, and how every keystroke you type waits its turn. The rule is dead simple: First In, First Out. Add at the back, remove from the front, and nobody jumps the line. Here's the plan. We'll write the obvious Python queue out of a plain list, then watch it quietly betray us on big inputs. After that we fix it two ways. First a ring buffer, which bends a fixed array into a circle. Then collections.deque, the tool you'll actually reach for. And the whole way through we keep asking the one question that decides everything: when you pull the item off the front, what does the machine actually have to move? By the end you'll have a queue that adds and removes in true O(1). You'll know exactly why the natural version secretly crawls at O(n). And you'll have seen — in memory — the one trick that fixes it.

★ YOU ALREADY RUN THIS · the-till-linenobody ever taught you the rule — but you would riot if it broke
Friday evening, one till open, and you push your trolley in behind the man with the crate of beer. You do not pick a spot: you join at the back, because the back is the only door in. Then somebody drifts up beside the belt with two items and starts easing in ahead of you, and something in your chest goes tight. Nobody taught you that rule. You felt it break. And look at who is being served — the woman at the front is there for exactly one reason: she has been standing there longest. Everyone behind her holds position. Nobody shuffles up. The line moves by one person leaving the front and one joining the back, and nothing else in it ever moves.
you join at the back — the only door inq.append(x) · enqueue, and the tail is the only place it happens
the till serves whoever stands at the frontq.popleft() · dequeue — always the oldest survivor
the queue-jumper makes your chest go tightFIFO is an invariant, not a preference — break it and it stopped being a queue
nobody shuffles up when the front leavesthe items never move — only the two ends do. That is the whole O(1)
the cafeteria tray stack hands you the newest tray (ch 51)same two moves, opposite end — a stack serves the newest, a queue the oldest
pin it: the front of a queue is never a decision — it is simply whoever has waited longest, and every line of this chapter exists to keep that true.
iolinked · chapter 52 — the checkpoints6 steps
$ sections covered in The queue — first in, first out
01One rule, and it means order
02Why a plain list makes a terrible queue
03The ring buffer — two indices chasing around a fixed array
04collections.deque — the real fix, and its memory
05How the machine feels it, and why search runs on queues
06When to reach for a queue

01One rule, and it means order

Let's start with the shape of the thing. A queue is a linear collection with two doors, one at each end, and a law about which door does what. The back door is enqueue: a new item goes in there, and only there. The front door is dequeue: an item comes out there, and only there. Watch what that one law forces — because you always remove the oldest surviving item, the one that has waited longest, the queue preserves arrival order perfectly. First in, first out. A stack is LIFO; a queue is FIFO; both are nothing more than a rule about which end you touch.

Let's make that concrete before we go further. Say three web requests hit your server in order: R1, then R2, then R3. Each one enqueues at the back as it arrives, so the line reads R1, R2, R3 from front to back. Now the server is ready to work. It dequeues from the front and gets R1, the one that has waited longest. Dequeue again and it gets R2, then R3. The order out is exactly the order in. Nobody who arrived later was served first. That is the whole promise of a queue, and every use in this chapter is just this small trace played out at scale.

And that rule is not a toy. FIFO is fairness: the request that arrived first gets served first, so no task starves while newer ones cut the line. It is also buffering, and the picture there is a fast producer feeding a slow consumer — your keyboard feeding a busy program, or a network card feeding your app. A queue holds that backlog in the exact order it arrived, and the consumer drains it at its own pace without losing or reordering a thing. Keep those two words, fairness and buffering, in mind, because every real use at the end of this chapter is one of them.

Now the part this volume cares about most: what does a queue actually look like in RAM? The honest first picture is a row of slots — a contiguous block, exactly the array of 8-byte references from Volume 1, plus two markers. One marker, front, points at the oldest live item, the next one to leave. The other, back, points at the next free slot, where the next arrival will land. To enqueue, we write at back and nudge back forward. To dequeue, we read at front and nudge front forward. Nothing else moves.

Put real addresses on that picture and it stops being abstract. Say the block starts at address 1000 and each reference is 8 bytes, so slot 0 lives at 1000, slot 1 at 1008, slot 2 at 1016, and on up. If front holds the index 2 and back holds 5, the live items sit in slots 2, 3, and 4 — three of them — at addresses 1016, 1024, and 1032. To dequeue, we read slot 2 and bump front to 3. The value at 1016 never budged — we just stopped pointing at it. That is the whole trick in one line: the data stays put and two small integers do all the walking.

a queue in memory — a contiguous array of 8-byte references (Vol 1) 0x1a00 0x1a08 0x1a10 0x1a18 0x1a20 0x1a28 0x1a30 0x1a38 · · A B C D · · 01 23 45 67 front dequeue reads here (A leaves first) back enqueue writes here, then back → 7
The queue, from memory up — four live items (A oldest, D newest) sit in a contiguous run of reference slots. Two integers do all the work: front marks the next to leave, back the next free spot. Enqueue and dequeue each touch one slot and move one marker.

If both operations touch a single slot and bump a single index, both should be O(1). So why does the most natural Python queue crawl to a halt on big inputs? →

02Why a plain list makes a terrible queue

Reach for a queue in Python and the obvious move is a list: append to add at the back, pop(0) to remove from the front. It looks right. It even works — for a while. Then it dies, and the reason is buried in how a list lives in memory. Recall from Volume 1: a Python list is a contiguous array of references with a fixed front pinned at index 0. There is no movable front marker. So when you delete index 0, the list cannot just "advance a pointer" — it must keep index 0 meaning index 0. Every one of the remaining n−1 references has to slide down one slot to close the hole.

list.pop(0) — pull the front, then shift EVERYTHING down one slot before A B C D E F remove ✕ after B C D E F · n−1 moves = O(n) per dequeue
The hidden cost — a list has no movable front, so removing index 0 slides all n−1 survivors down one slot (a single memmove under the hood). Cheap for six items; ruinous for a million.

"Slides down one slot" is a bulk memory copy — internally, a single memmove of n−1 pointers. For six items it is invisible. But for a million-item queue, every single dequeue copies almost eight megabytes of references before it can return one value. That is O(n) per removal, which makes draining the whole queue O(n²). We do not have to trust the reasoning, because we can time it. Here each trial dequeues one item from a size-n queue and re-enqueues it, so the size stays fixed and we isolate the cost of one front removal:

trap.pypython
import timeit
from collections import deque

for n in (10_000, 100_000, 1_000_000):
    tl = timeit.timeit("l.append(l.pop(0))",       # dequeue front, re-enqueue
                       setup=f"l=list(range({n}))", number=5000) / 5000
    td = timeit.timeit("d.append(d.popleft())",    # same, but a deque
                       setup=f"from collections import deque; d=deque(range({n}))",
                       number=5000) / 5000
    print(f"n={n:>9}   list {tl*1e9:8.0f} ns   deque {td*1e9:4.0f} ns")

# n=    10000   list    14790 ns   deque   27 ns
# n=   100000   list   151925 ns   deque   28 ns
# n=  1000000   list   526401 ns   deque   24 ns

Read the list column top to bottom. Ten thousand items: ~15 µs per dequeue. Ten times the items: ~152 µs. That is ten times the cost, a dead-straight line, exactly what O(n) predicts. The deque column beside it never twitches: ~25 nanoseconds whether it holds ten thousand items or a million. (The numbers come from this machine and will vary. The shapes — one climbing, one flat — are the law.) Push it further and it stops being a benchmark and becomes pain. Draining a 100,000-item queue front-to-back took the list 7.5 seconds. The deque did the same job in 5.4 milliseconds, about 1,400× faster, and the gap widens with every extra item.

Those numbers check against each other. Ten times the items gave ten times the per-dequeue cost, 15 µs climbing to 152 µs, which is the straight line O(n) demands — a size-10n queue shifts ten times as many pointers per removal. The full-drain gap is starker still, because there both costs compound: draining 100,000 items is O(n²) work for the list against O(n) for the deque. 7.5 seconds versus 5.4 milliseconds is roughly a 1,400× spread, and every tenfold jump in n widens it by another factor of ten. The deque is not a little faster here. It is a different curve.

↺ The thing people get backwards
list.pop(0) is not "a queue with a small constant" — it is the wrong complexity class. The instinct is that removing one item is cheap because you only wanted one item. But a list stores its front by position, and position 0 must stay position 0, so removing it forces every survivor to move. The cost is not in the item you took; it is in all the items you kept. A queue needs a front that can move instead — and a list's front cannot.

Myth

"A Python list is a perfectly good queue — append and pop(0) do exactly what I need."

Reality

append is fine — amortized O(1), as Volume 3 showed. But pop(0) is O(n): it memmoves the whole tail. You get correct output in quadratic time, and that is the worst kind of bug, because the small tests all pass.

Here is why that bug hides so well. Draining a queue of n items with pop(0) does about n²/2 pointer shifts in total. Run your unit test with a thousand items and that is roughly 500,000 shifts, over in well under a millisecond, so nobody notices. Then you ship it, and production hands you a million items instead — a thousand times more data. But because the work grows with the square, a thousand times the data means a million times more shifts. The test that passed in a blink now grinds for seconds. The code never changed. Only the size did, and O(n²) was waiting the whole time.

The list's sin was a front that couldn't move. What if we let it move — and let both ends wander freely around a fixed block of slots? →

03The ring buffer — two indices chasing around a fixed array

Here is the human insight that fixes everything, and it is beautifully cheap. The whole problem was that dequeuing tried to keep the front pinned at index 0, so don't pin it. Keep the same contiguous array, but let front and back be movable integer indices. Dequeue? Just advance front by one, and the old slot is now dead space we simply don't care about. Enqueue? Write at back and advance back by one. No element ever moves. Both operations are a write and an add, which is genuine O(1).

Walk the indices once and you will feel the problem coming. Take a fixed array of capacity 4, with front and back both starting at 0. Enqueue three items and back climbs 0 → 1 → 2 → 3. Now dequeue two and front climbs 0 → 1 → 2. The live queue is just the one item in slot 2, yet back is already sitting at the last slot. Enqueue once more: that fills slot 3, and back tries to step to slot 4, which does not exist. Slots 0 and 1 sit empty and wasted behind us. The indices only crawl forward, so they run out of room while the array is nearly empty. That is the exact wall the next idea climbs over.

But if both indices only ever crawl forward, they march off the end of a fixed array. The fix is the trick that names the structure: wrap around. When an index reaches the last slot, its next step goes back to slot 0. We compute that with a single modulo, (i + 1) % capacity. The array is no longer a line. It is a ring. The live queue is the arc of slots from front up to back. That arc drifts around the circle as items come and go, forever reusing the slots that dequeues freed behind it. This is a ring buffer, also called a circular buffer, and it is one of the most-used structures in all of systems programming.

Watch the wrap happen with real numbers. Take a ring of capacity 8, so the valid slots are 0 through 7. If back is sitting at slot 7 and a new item arrives, we compute (7 + 1) % 8, which is 8 % 8 = 0 — back lands on slot 0, the slot some earlier dequeue freed. One item later it steps to (0 + 1) % 8 = 1, then 2, and on around the circle. The modulo is the whole ring in one operator: it turns a straight line of indices into a loop that never runs out, as long as the live count stays under the capacity.

InteractiveEnqueue and dequeue — watch front & back chase around the ring
ring buffer capacity 8 0 elements moved
front 0 · back 0
Every op is one write and one index = (index + 1) % 8. Nothing ever slides — the "moved" counter stays at zero no matter how long you play.
· · C D E · · · front back (i + 1) % capacity — the ends are joined
A line bent into a ring — the live queue is the shaded arc from front to back. When either index steps off the end it wraps to slot 0. Freed slots behind front get reused, so a fixed array serves an endless stream — no growth, no shifting.
Wait —
if dequeued slots get quietly reused, and front can equal back in two different situations, how does the ring ever know whether it is empty or completely full?
The deeper cut — how do you tell "full" from "empty"?

There is a classic ambiguity in a ring buffer. When front == back, is the ring empty or completely full? Both states can produce equal indices. Real implementations pick one of three fixes. They keep an explicit count of live items, which is what the widget above does, and it is the simplest and clearest option. Or they waste one slot, so a full ring has back one short of front. Or they use free-running indices that only get masked when you read them. Here is the point worth carrying away. A ring buffer trades unbounded growth for a fixed capacity, and that boundedness is a feature. It is how audio drivers, network stacks, and log pipelines guarantee they will never blow up memory under load. When the ring fills, they apply backpressure, which makes the producer wait, or they drop the oldest item. That is a deliberate choice, not a crash.

Make that bound concrete. A sound card wants a steady stream of samples at 48,000 per second, and it reads them from a ring buffer the driver keeps refilling. Give that ring 4,096 slots and it holds about 85 milliseconds of audio — 4,096 ÷ 48,000 is 0.085 seconds. That fixed 85 ms is the driver's safety margin. If your program falls behind and the ring drains empty, you hear a click. If it races ahead and the ring fills, the driver makes it wait — that is backpressure in the flesh. The buffer is deliberately small and bounded, and that boundedness is exactly what keeps the audio smooth and the memory flat.

A ring buffer is perfect when you know the capacity up front. But Python's queue of choice grows without bound and still gives O(1) at both ends. How? →

04collections.deque — the real fix, and its memory

You will rarely hand-roll a ring buffer in Python. You will reach for collections.deque (pronounced "deck," from double-ended queue), the standard library's answer to exactly this problem. It gives you O(1) append and pop at the back and O(1) appendleft and popleft at the front — no shifting, ever. For a FIFO queue you use just two of its four doors: append to enqueue, popleft to dequeue.

deque.pypython
from collections import deque

q = deque()          # an empty FIFO
q.append("A")        # enqueue at the back
q.append("B")
q.append("C")
first = q.popleft()  # dequeue from the front  ->  "A"  (first in, first out)
print(first, list(q))
# A ['B', 'C']

Line by line: deque() builds an empty double-ended queue. Three appends enqueue A, B, C at the back, in order. Then popleft() removes from the front and returns "A", the oldest item, exactly FIFO, and what remains is B then C. These are the same five lines a list would use, minus the O(n) landmine.

THE STDLIB TOOLBELT · collections.deque as a queueone import, two methods — and the O(n) landmine is gone
from collections import deque q = deque() # an empty FIFO -- that is the whole constructor q = deque(iterable) # or start it full, front-to-back q = deque(iterable, maxlen=n) # bounded: the far end falls off, silently q.append(item) # ENQUEUE at the back -- O(1) item = q.popleft() # DEQUEUE from the front -- O(1) nxt = q[0] # peek the front, leaving it in place if not q: … # ALWAYS ask this before you popleft while q: … # the drain -- and the outer loop of every BFS
append(x)Enqueue. One reference written into the end block's next free cell, one cursor step. The million items already inside are never read — that is where the O(1) comes from.
popleft()Dequeue. Reads the front cell, steps the cursor inward, returns the oldest item. This is the method a list simply does not have, and the reason this box exists.
q[0] · q[-1]Peek the front, peek the back. Both O(1) — measured at 17.9 ns on a 100,000-item deque. Every index between them walks the block chain: the next chapter's honest caveat.
maxlen=nA bounded queue. Once it holds n items, every append drops one off the far end to make room. No exception, no warning, nothing returned — see the second tripwire before you trust it.
queue.QueueThe threaded cousin (Chapter 27). Same FIFO, but get() blocks until an item arrives instead of raising, and put() can block when full. Reach for it when producer and consumer are separate threads; a deque is the tool when one thread owns the queue — and the third tripwire draws that line exactly.
while q:The drain loop. An empty deque is falsy, so this reads as “while anyone is still waiting” — and it is literally the outer loop of the breadth-first search in section 05.
INPUTfrom collections import deque

orders = deque()                       # the whole constructor

orders.append("#1041 latte")           # enqueue at the BACK
orders.append("#1042 flat white")
orders.append("#1043 cortado")
print("waiting:", len(orders), list(orders))

while orders:                          # the emptiness guard, same shape as a stack's
    ticket = orders.popleft()          # dequeue from the FRONT - oldest first
    print("  serving", ticket, "| still waiting:", list(orders))

print("empty?", not orders)
try:
    orders.popleft()
except IndexError as e:
    print("popleft on empty:", type(e).__name__ + ":", e)

recent = deque(maxlen=3)               # a bounded queue SILENTLY drops
for t in ["#1041", "#1042", "#1043", "#1044"]:
    recent.append(t)
    print("  maxlen=3 ->", list(recent))
OUTPUTwaiting: 3 ['#1041 latte', '#1042 flat white', '#1043 cortado']
  serving #1041 latte | still waiting: ['#1042 flat white', '#1043 cortado']
  serving #1042 flat white | still waiting: ['#1043 cortado']
  serving #1043 cortado | still waiting: []
empty? True
popleft on empty: IndexError: pop from an empty deque
  maxlen=3 -> ['#1041']
  maxlen=3 -> ['#1041', '#1042']
  maxlen=3 -> ['#1041', '#1042', '#1043']
  maxlen=3 -> ['#1042', '#1043', '#1044']
TRIPWIRES
  • list.pop(0) is not a slow dequeue — it is the wrong complexity class. Draining 100,000 items on this machine, just now: 4875.1 ms for while q: q.pop(0) against 9.7 ms for while q: q.popleft()501×. Chapter 48 clocked the same contrast at 3005.3 ms versus 10.3 ms; the ratio moves with the machine and the day, the two curves never do.
  • maxlen drops silently, and from the end you did not push. On deque([1,2,3], maxlen=3): append(4) gives [2, 3, 4] — dropped from the left; appendleft(0) then gives [0, 2, 3] — dropped from the right. Nothing is raised and nothing is returned, so the discarded item is gone for good. Use it when forgetting is the feature, never as a size check.
  • An empty deque raises: IndexError: pop from an empty deque (a list says pop from empty list — different words, same lesson). And if q: q.popleft() is safe in one thread only. A deque's individual appends and pops are atomic, but the check and the pop are two separate steps, so another thread can empty the deque in between. That gap is precisely why queue.Queue exists: q.get(timeout=0.25) raised _queue.Empty after 0.25 s of waiting, where the deque raised instantly.

One caution before we look inside. A deque has four doors, and only one pair gives you a queue. append then popleft is FIFO: in at the back, out at the front, oldest first. Flip to append then pop — both at the same end — and you have rebuilt the stack from Chapter 51, newest first. Same object, opposite discipline. So the FIFO promise does not live in the deque itself — it lives in which two methods you choose to call. Pick the pair that matches the order you need, and never mix them without meaning to.

What does a deque look like in memory, and why does it stay O(1) at both ends? Not one contiguous array — that would re-create the list's problem at one end or the other. Instead a deque is a doubly linked list of fixed-size blocks. Each block is a small contiguous array (64 reference slots in CPython), and the blocks are chained with next/prev pointers. The deque keeps direct pointers to the first and last block, plus the write position inside each. Adding at either end writes into the end block; when an end block fills, a new block is linked on. No element is ever copied to make room — growth just links another block.

Put a number on those blocks. A CPython deque block holds 64 references, so a deque of 1,000 items needs at least 1,000 ÷ 64 ≈ 15.6 blocks — round up to 16 — chained together. Fifteen of them are full, holding 960 items, and the sixteenth carries the last 40. Each block adds one next and one prev pointer, so the linking overhead is just 16 × 2 = 32 extra pointers for the entire 1,000-item deque. That is why the per-block bookkeeping is a rounding error beside the references themselves: a handful of pointers guarding thousands of slots.

collections.deque — a doubly linked chain of fixed blocks (64 slots each) deque left / right ptrs ·ABC block 0 · 64 slots DE·· block 1 · 64 slots next / prev front → popleft back → append
Why both ends are O(1) — the deque holds direct pointers to its end blocks. popleft reads the front block; append writes the back block; a full end block just links a fresh one. Elements never move — so cost never depends on how many are already inside. (The 64-slot block design is the next chapter's main event.)

That design has a memory price worth naming out loud. An empty list weighs 56 bytes, while an empty deque weighs 760 bytes, because it pre-allocates its first block of slots up front. So for tiny collections a deque is heavier. But per element the two converge on the same figure, about 8 bytes each, and that is because both, at bottom, store 8-byte references (Vol 1) rather than the objects themselves:

deque.pypython
import sys
from collections import deque
print(sys.getsizeof([]),        sys.getsizeof(deque()))       # empty:  56   760
for n in (1000, 10000):
    print(n, sys.getsizeof(list(range(n))), sys.getsizeof(deque(range(n))))
# 1000    8056    8680
# 10000  80056   83128

At 10,000 items the list holds 80,056 bytes and the deque 83,128. The deque's small surplus is the per-block bookkeeping: the next/prev pointers and the partly-filled end blocks. Both are ~8 bytes times n plus a fixed header, each a container of references, precisely the Volume 1 model. So the deque buys you O(1) at both ends for a modest, near-constant space premium, and that is a very good trade.

You can check that 8-bytes-plus-header claim by hand. A reference is 8 bytes, and the list holds 10,000 of them, so the slots alone are 10,000 × 8 = 80,000 bytes. Add the list's fixed 56-byte header and you get 80,056 — exactly the number the tool reported. Nothing mysterious is hiding in there. The list is a header stapled to a flat run of references, just as Volume 1 drew it. The deque's 83,128 is the same story with a little more bookkeeping: the same 80,000 bytes of references, plus the next/prev pointers and the slack in its partly-filled end blocks.

InteractiveSlide n — one queue's cost climbs, the other's stays flat
time for ONE dequeue from a queue of n items list.pop(0) O(n) — shifts every survivor down one slot deque.popleft() O(1) — advance a pointer, never move an element
n
Grow the queue and the red bar marches with n while the green bar refuses to move. That flat green line is the whole reason deque exists.

You now have a real O(1) FIFO. The last question is the one that makes it worth knowing: where does the machine — and the wider world — actually run on queues? →

05How the machine feels it, and why search runs on queues

Big-O says the ring buffer and a naive pointer-per-node linked queue are both O(1) per operation. The metal disagrees about which is fast. A ring buffer is one contiguous array, so consecutive slots sit in consecutive cache lines. Walking the queue streams through the CPU cache (Volume 3) at full speed. A linked queue puts one heap node per item, scattered anywhere in memory. So every dequeue chases a pointer to an address the cache never predicted. That is a stall of hundreds of cycles, again and again. The deque splits the difference on purpose. 64 items per block means you get long contiguous runs, which are cache-friendly, with only an occasional pointer hop between blocks. That block size is not arbitrary. It is tuned so the pointer-chase is rare relative to the streaming. This is the whole reason a heap lives in a flat array too, a lesson the heap chapter makes central.

BFS (Vol 3 ch 13): the queue IS the frontier — explore level by level s a b c d frontier queue (FIFO): front a b · back dequeue s → discover a, b → enqueue them → dequeue a → discover c … FIFO order guarantees nearer nodes are always visited first. Swap the queue for a stack, and the very same loop becomes DFS.
The famous one — breadth-first search is just "process the frontier in FIFO order." Because the queue serves oldest-first, BFS fans out in rings of equal distance from the start — which is exactly why it finds shortest paths in an unweighted graph.

That is the deepest "wow" in the chapter: BFS and a queue are the same idea. The queue holds the frontier, the nodes seen but not yet explored. Dequeue the oldest, enqueue its undiscovered neighbours at the back, and repeat. Because a queue is FIFO, everything one step from the start is processed before anything two steps away. So BFS sweeps outward in perfect concentric rings and lands on the shortest path first. Now change one line to dequeue from a stack instead, and the identical loop becomes depth-first search. The container chooses the algorithm. That is the 1% move in miniature: the data structure is the strategy.

Picture it on a tiny grid to see the rings. Start at cell S and enqueue it. Dequeue S, then enqueue its four neighbours — the cells exactly one step away. Because they all sit at the back, the queue empties every distance-1 cell before it touches a distance-2 cell. When you finally dequeue a distance-1 cell and enqueue its undiscovered neighbours, those are distance-2, and they line up behind everything closer. So the search always finishes a whole ring before it starts the next. The first time you reach your target, you reached it in the fewest steps possible — that is why BFS finds the shortest path, and it is the FIFO rule doing every bit of the work.

Count the rings and the shortest-path claim becomes something you can check. Ring 0 is just S itself, the start. Ring 1 is its 4 neighbours, one step out. Ring 2 is everything one step beyond those, two steps from S, and so the pattern goes. Because the queue drains ring 0 completely, then ring 1, then ring 2, the moment a target pops out of the queue its ring number is its distance from S in steps. If your target sits in ring 3, BFS reached it in 3 steps and could not have done it in fewer — every earlier ring held the cells that were closer, and all of them came out first.

InteractiveSlide the search deeper — BFS’s queue explodes, DFS’s stack barely grows
peak memory a search must hold · tree branches b = 4 ways · explored to depth d BFS · queue holds the frontier O(bᵈ) — one whole level, each b× wider than the last · DFS · stack holds one path O(b·d) — at most b siblings per level on the path · Same loop, same goal found — but the queue keeps the entire outer ring; the stack keeps only the trail. Swap the queue for a stack and BFS becomes DFS — the container also chooses the memory bill.
depth 6
Breadth-first search must remember every node on the current frontier at once — and each level is b times wider than the last, so the queue climbs to tens of millions by depth 12. Depth-first search never holds more than one root-to-leaf path. Same search, same shortest path found — wildly different memory.

06When to reach for a queue

Which item comes out next? = which end do you touch? oldest first — FIFO QUEUE deque.append / .popleft fairness · buffering · BFS · schedulers newest first STACK list.append / .pop undo · DFS · ch 03 both ends DEQUE appendleft / popleft / … sliding windows · ch 05
Pick it on sight — the whole choice collapses to one question: which end comes out next? Oldest-first is a queue; newest-first is a stack; if you need both, it is a deque. Nothing here should ever be a list's pop(0).

Good at data structures is mostly one skill: matching the access pattern to the container on sight. The queue's tell is unmistakable — whenever items must be handled in arrival order, and you always take the oldest first. Fairness (serve who came first) or buffering (a fast source, a slow drain, order preserved between them). The instant you catch yourself writing list.pop(0) in a loop, stop: you have found a queue wearing a list's clothes, and it is quietly O(n²). Type from collections import deque instead.

NOW BUILD A QUEUE OUT OF TWO STACKSboth of them serve the newest — stand them nose to nose and they serve the oldest
The drill. Forget deque exists. You are allowed only the two stack moves from Chapter 51 — append and pop, the far end of a list — and you must build a working FIFO out of them. No pop(0), no insert(0, x), no shifting.

The one idea that unlocks it: keep two stacks. Arrivals land on _in. Departures leave from _out. When _out runs dry, pour _in into it one item at a time — and notice what pouring a stack into a stack does to the order.

Then answer the real question. That pour is O(n), so how can this be a constant-time queue? The trick is when you are allowed to pour. Write the rule down before you code it: pour only when _out is empty, never on a whim.

The check that settles it: do not time it — count it. Instrument the pour, push 100,000 items through, drain them all, and print the total number of items moved. If your rule is right, that number lands on something exact and beautiful. Predict it before you run it.
show the solution
class TwoStackQueue:
    """A FIFO queue built from two LIFO stacks - and nothing else."""

    def __init__(self):
        self._in = []            # arrivals land here, newest on top
        self._out = []           # departures leave from here, OLDEST on top

    def enqueue(self, x):
        self._in.append(x)       # always O(1)

    def _flip(self):
        # Pour _in into _out. Popping a stack reverses it, so the oldest
        # arrival - buried at the bottom of _in - ends up on TOP of _out.
        while self._in:
            self._out.append(self._in.pop())

    def dequeue(self):
        if not self._out:        # only refill when _out has run dry
            if not self._in:
                raise IndexError("dequeue from empty queue")
            self._flip()
        return self._out.pop()   # O(1) on every call that finds _out stocked

    def __len__(self):
        return len(self._in) + len(self._out)


q = TwoStackQueue()
for t in ["A", "B", "C"]:
    q.enqueue(t)
print("in:", q._in, "out:", q._out)          # nothing has flipped yet
print("dequeue ->", q.dequeue())             # this one pays for the flip
print("in:", q._in, "out:", q._out)
print("dequeue ->", q.dequeue())             # free
q.enqueue("D")                               # arrives on the IN side
print("in:", q._in, "out:", q._out)
print("dequeue ->", q.dequeue())             # still free - out was stocked
print("dequeue ->", q.dequeue())             # out empty -> flip D across
print("len:", len(q))
try:
    q.dequeue()
except IndexError as e:
    print("empty:", type(e).__name__ + ":", e)

# in: ['A', 'B', 'C'] out: []
# dequeue -> A
# in: [] out: ['C', 'B']
# dequeue -> B
# in: ['D'] out: ['C']
# dequeue -> C
# dequeue -> D
# len: 0
# empty: IndexError: dequeue from empty queue

# THE AMORTIZED PROOF: count the moves, not the clock.
moves = 0
class Counted(TwoStackQueue):
    def _flip(self):
        global moves
        while self._in:
            self._out.append(self._in.pop()); moves += 1

c = Counted()
for i in range(100_000):
    c.enqueue(i)
for i in range(100_000):
    c.dequeue()
print("100,000 items ->", moves, "flip moves total =", moves / 100_000, "per item")

# 100,000 items -> 100000 flip moves total = 1.0 per item

# WHY THE FLIP IS RARE - and why "1.0" is the whole answer.
# Follow one item all the way through. It is pushed onto _in exactly once.
# It is moved across to _out exactly once. It is popped off _out exactly
# once. Three touches, fixed, forever - no matter how many other items are
# in the queue when its turn comes. That is why the counter printed exactly
# 100,000 moves for 100,000 items: 1.0 each, not 1.4, not n.
#
# The pour LOOKS expensive because one unlucky dequeue moves everything
# waiting. But that dequeue is buying the free ride for every dequeue behind
# it - _out is now stocked, and the next k calls just pop. Charge the cost
# to the items rather than to the calls and it flattens to a constant: this
# is amortized O(1), the exact bookkeeping the list's overallocation used.
#
# Break the rule and watch it die. Drop the `if not self._out:` guard so
# every dequeue re-pours the whole queue, and count again at n = 10,000:
#
#   guarded        10,000 moves  (      1.0 per item)
#   unguarded 100,000,000 moves  ( 10,000.0 per item)
#
# That is n moves against n**2 - measured, not estimated - and you have
# rebuilt list.pop(0) out of two stacks. The two stacks were never the
# trick. The GUARD was.
#
# Worth knowing where you have seen this: two stacks make a queue, and the
# same trick in reverse (two queues make a stack) is the classic pair. The
# structure was never in the container. It was always in the discipline.
Where you meet this — every day, mostly invisibly
Your OS scheduler holds ready processes in run queues and serves them fairly. Every print job waits in a print queue. BFS (maze solvers, "degrees of separation," network broadcast, the shortest path your GPS considers on an unweighted grid) runs on a queue. Task/message queues — Celery, RabbitMQ, Amazon SQS, Kafka — are literally this structure at datacenter scale: producers enqueue jobs, worker fleets dequeue them in order. Keyboard and network input buffers queue your keystrokes and arriving packets so nothing is lost or reordered while the program is busy. Streaming audio and video buffer incoming frames in a ring so playback stays smooth when the network stutters. You used several of these to load this page.
The rule of thumb
Need oldest-first (FIFO)? → collections.deque, using append and popleft. Need newest-first (LIFO)? → a stack (a plain list — the previous chapter). Need both ends, or a fixed-capacity buffer? → still a deque, or a ring buffer. Never a list's pop(0) for a queue.

You have the FIFO half of the story. But notice the widget above did something a pure queue never needs: it added and removed at both ends. That double-ended powerappendleft as easily as append — is not a queue at all. It is the deque in full, and its 64-slot block design is a small marvel of engineering worth a chapter of its own.

Next (chapter 53): the deque — one structure that is a stack and a queue at once. We open up those linked blocks of 64, see why that exact number, and watch appendleft and popleft stay O(1) at the end nobody else can touch. →

PROGRAMS run them line by line — the same ideas, written the many ways Python offers
Chapter 52, in working code

A queue is the most patient structure there is — whoever waited longest gets served first — and once you see two little indices (or one clever deque) make that both fair and fast, you'll spot queues humming underneath every scheduler, print spooler, and search you ever meet.

The FIFO idea — first in, first out
A queue only ever adds at the back and removes from the front. The order out matches the order in — always.
Roll your own — the ring buffer
A fixed array, two indices, and modulo arithmetic. The front is allowed to MOVE, so nothing ever shifts.
The everyday queue — collections.deque
In real code you reach for deque, not a hand-rolled ring: O(1) at both ends, and it grows without bound.
What a queue is really for — breadth-first search
Serve the oldest first and a search fans out level by level. Swap the queue for a stack and the SAME code goes deep instead.
end of chapter 52 · six sections
iolinked
designed & developed by Ajai Raj
Python, from the metal up · a product of iolinked