S026 · Stacks & Queues — LIFO/FIFO in Practice
The two most important restricted data structures — and how they secretly power your function calls, browser history, printer queues, BFS, and every parser you've ever used.
🎯 Use stack (LIFO) and queue (FIFO) fluently in Python, own the monotonic-stack pattern for O(n) next-greater problems, and recognise them running underneath function calls, BFS, undo, and every real event loop.
Why this session exists
Stacks and queues are what happens when you take an ordinary sequence and restrict how you're allowed to touch it. That single restriction unlocks the algorithms that run every function call (call stack), every browser history (undo stack), every printer job (FIFO queue), every level-order graph traversal (BFS queue), and every JSON / expression parser (matching-brackets stack). This session teaches both the abstract API and the two production-favourite variants: monotonic stack (O(n) next-greater-element) and deque (O(1) both-ends queue).
- State the difference between LIFO and FIFO in one sentence each.
- Choose the right Python container: list for stack, collections.deque for queue.
- Solve valid-parentheses, min-stack, and next-greater-element from memory.
- Implement BFS on a graph with a deque — and know why NOT to use recursion for it.
- Recognise where stacks and queues appear inside the runtime you already use (call stack, event loop, print spooler).
Prerequisites
- S025 · Linked Lists — the underlying implementation of a fast queue.
- S023 · Arrays & Strings — the two-pointer skills we'll build on.
(a) Intuition · 5 min
A stack of plates in a cafeteria: you can only take the top plate and add to the top. Last plate in is the first plate out — LIFO.
A queue at the DMV: whoever arrived first gets served first, everyone else lines up behind them — FIFO. Cutting the line breaks the invariant, and someone will yell at you.
A stack has two operations: push and pop, both O(1). Everything happens at ‘the top’. A queue has enqueue at the back and dequeue at the front, both O(1) — as long as your implementation supports both ends cheaply (list.pop(0) is O(n)! Use deque).
Once you spot the LIFO / FIFO pattern in a problem, the solution becomes almost mechanical: whatever you saw most recently must be handled first (stack) or whatever you saw first must be handled first (queue). Half of algorithm design is naming the right pattern.
The four operations you actually need to memorise
- Stack: push, pop, peek, is_empty. All O(1). In Python: list.append + list.pop.
- Queue: enqueue, dequeue, peek, is_empty. All O(1) — but ONLY if you use collections.deque. list.pop(0) is O(n) and will silently kill your perf.
- Monotonic stack: a stack where elements are always kept in increasing or decreasing order — pop while the new element breaks the invariant. Turns O(n²) into O(n) for next-greater / next-smaller.
- Priority queue: technically not a queue at all — it's a heap (S029). Named ‘queue’ because it exposes dequeue-highest-priority.
Timeline — the stack rules everything you touch
- 1946Turing describes the call stackAlan Turing's ACE report proposes ‘nested subroutines’ backed by a pushdown stack — the mechanism that would define every language runtime for 80 years.
- 1957FORTRAN uses fixed-frame callingNo recursion — one static frame per subroutine. Programmers cursed and demanded stacks.
- 1961Dijkstra's shunting-yardTwo stacks convert infix arithmetic to postfix. Every calculator, every SQL parser, every regex engine still uses variants.
- 1972Unix pipes = queues between processesThe | operator wires stdout of one process to stdin of the next — a bounded FIFO in kernel memory.
- 1998Web browser history stackNetscape's Back button implements an undo stack. Every SPA still models routing as a stack.
- 2013React uses a stack for reconciliationThe Fiber architecture rewrites the reconciler as an explicit work stack so it can be paused/resumed — recursion couldn't.
(b) Visual walkthrough · 15 min
Stack (LIFO) vs Queue (FIFO)
The monotonic-stack pattern — from O(n²) to O(n)
For each element x, we want to know ‘what is the next element to the right that's greater than x?’
This is the invariant: the top of the stack always holds the smallest ‘still unanswered’ value seen so far.
The current x is the answer for the popped index. Record it.
It's now waiting for its own answer.
Each index is pushed and popped at most once. Total O(n) — even though it looks like a nested loop.
Where stacks show up in the runtime you already use
Every language
- Each call pushes a stack frame (locals + return address)
- Return pops it
- Overflow = StackOverflowError / segfault
- This is why deep recursion crashes
SQL, regex, JSON
- Two-stack shunting-yard
- One for operators, one for operands
- Precedence resolution via pop-while
- Same pattern in every parser generator
History API
- Push on navigate
- Pop on Back
- Also the model for editor undo/redo
- ‘SPA routers’ are a wrapper over this
Modern React
- Reconciliation becomes an explicit work stack
- Pausable / resumable / cancellable
- Recursion can't do that
Where queues show up in the systems you already use
Queues in the wild
"A stack and a queue are different data structures. I pick one, and I've made a data-structure decision."
They are the same container with a different removal policy. LIFO vs FIFO is a scheduling decision, not a storage decision — and it changes the order your problem is explored in, which changes what answers you can compute at all.
Because they are taught as two chapters with two APIs, so they feel like two things. The moment that framing collapses is when you see BFS and DFS: identical code, identical graph, one line different — pop() vs popleft() — and you get either shortest paths or you don't. That is not two data structures. That is one container and two policies.
Same traversal, one character of difference, completely different semantics:
from collections import deque
g = {'A': ['B', 'C'], 'B': ['D'], 'C': ['D'], 'D': []}
def walk(start, use_stack):
frontier, seen, order = deque([start]), {start}, []
while frontier:
n = frontier.pop() if use_stack else frontier.popleft()
order.append(n)
for m in g[n]:
if m not in seen:
seen.add(m); frontier.append(m)
return order
print(walk('A', True)) # DFS order
print(walk('A', False)) # BFS order - and only this one finds shortest pathsWhy is list.pop(0) O(n) while deque.popleft() is O(1)? Both remove one element. The difference is not an optimisation someone forgot.
- 1A Python list is a contiguous array of pointers, and its contract is that
lst[i]is O(1).forced by · O(1) indexing requires the address of element i to bebase + i·itemsize— pure arithmetic, no search - 2That formula only works if element 0 is always at
base, with no gaps.forced by · any hole or moving origin would break the arithmetic, which is the entire basis of the O(1) guarantee - 3So removing element 0 cannot simply leave a hole or advance the base pointer — every remaining element must move down one slot.forced by · the invariant "index i lives at base + i·itemsize" must hold after the operation too
- 4That is an O(n) memmove per pop. Draining a list of n items from the front is therefore O(n²).forced by · n pops × O(n) shift each
- 5A deque gives up contiguity: it is a doubly linked list of fixed-size blocks, with head and tail pointers into the end blocks.forced by · if you stop promising O(1) random access, you are free to move the logical origin instead of moving the data
Therefore deque.popleft() is O(1) — it advances a pointer inside a block — and the price is that dq[i] is no longer O(1); it must find the block first.
And note what this predicts: a BFS written with a plain list and pop(0) is quietly O(V²) instead of O(V+E). It will pass every small test and fall over on real data. This is one of the most common silent performance bugs in Python graph code, and it comes from exactly this derivation.
A stack is a spring-loaded plate dispenser: the last plate in is the first out. It naturally models nesting — you must finish the inner thing before returning to the outer thing. Function calls, brackets, undo history, HTML tags: all nesting, all stacks.
A queue is a ticket line: first in, first served. It naturally models fairness and distance — everyone at distance k is served before anyone at distance k+1. That is why a queue, and only a queue, gives you shortest paths in an unweighted graph.
- Stack = nesting and backtracking. If the problem says "matching", "innermost", or "undo", it is a stack.
- Queue = levels and fairness. If the problem says "shortest", "fewest steps", or "in order of arrival", it is a queue.
- Recursion is a stack you did not write. Converting recursion to iteration is literally making that stack explicit.
- A stack has unbounded depth risk (stack overflow); a queue has unbounded width risk (memory blowup on a wide frontier). Neither is safe by default.
Fire this model the moment you see: bracket or tag matching · undo/redo · a call trace · expression parsing · "fewest number of steps" · a task backlog or work queue · Kafka consumer offsets · Airflow's ready-task pool · any producer/consumer buffer.
You are processing a backlog of work items that themselves spawn more work. Drain it LIFO (stack) or FIFO (queue)?
Default to FIFO for anything a human or an SLA is waiting on, because bounded latency is a property you cannot retrofit. Use LIFO when the workload is a deep tree and memory is what will kill you first — the classic case is converting a recursive traversal to an iterative one specifically to escape the call-stack limit.
The senior move is to notice that this choice is usually invisible in the code: it is one method call. Which means it is also invisible in review. Write down which policy you chose and why, next to the container, because the next person will not be able to infer it.
(c) Hands-on · 25 min
Save as stacks_queues.py. Zero deps beyond stdlib.
"""stacks_queues.py — the interview canon for stacks + queues.
Run: python stacks_queues.py
"""
from __future__ import annotations
from collections import deque
# ------------------------------------------------------------------
# 1) Valid Parentheses — the canonical stack problem
# ------------------------------------------------------------------
def valid_parentheses(s: str) -> bool:
pairs = {")": "(", "]": "[", "}": "{"}
stack: list[str] = []
for ch in s:
if ch in "([{":
stack.append(ch)
elif ch in ")]}":
if not stack or stack.pop() != pairs[ch]:
return False
return not stack
# ------------------------------------------------------------------
# 2) MinStack — get_min in O(1) with an auxiliary stack
# ------------------------------------------------------------------
class MinStack:
"""Standard stack + get_min in O(1). Trick: keep a parallel stack of running minima."""
def __init__(self) -> None:
self._data: list[int] = []
self._mins: list[int] = []
def push(self, x: int) -> None:
self._data.append(x)
self._mins.append(x if not self._mins else min(x, self._mins[-1]))
def pop(self) -> int:
self._mins.pop()
return self._data.pop()
def top(self) -> int:
return self._data[-1]
def get_min(self) -> int:
return self._mins[-1]
# ------------------------------------------------------------------
# 3) Next Greater Element — the monotonic-stack pattern
# ------------------------------------------------------------------
def next_greater(nums: list[int]) -> list[int]:
"""For each element, the next-to-the-right greater value, or -1 if none. O(n)."""
n = len(nums)
ans = [-1] * n
stack: list[int] = [] # stack of INDICES with strictly decreasing values
for i, x in enumerate(nums):
while stack and nums[stack[-1]] < x:
ans[stack.pop()] = x
stack.append(i)
return ans
# ------------------------------------------------------------------
# 4) Daily Temperatures — same monotonic-stack idea, different question
# ------------------------------------------------------------------
def daily_temperatures(temps: list[int]) -> list[int]:
"""For each day, days until a warmer temperature (0 if never). O(n)."""
n = len(temps)
ans = [0] * n
stack: list[int] = []
for i, t in enumerate(temps):
while stack and temps[stack[-1]] < t:
j = stack.pop()
ans[j] = i - j
stack.append(i)
return ans
# ------------------------------------------------------------------
# 5) Implement a Queue with deque
# ------------------------------------------------------------------
class Queue:
def __init__(self) -> None:
self._d: deque = deque()
def enqueue(self, x) -> None:
self._d.append(x)
def dequeue(self):
return self._d.popleft() # O(1) — the whole point of deque
def peek(self):
return self._d[0]
def __len__(self) -> int:
return len(self._d)
# ------------------------------------------------------------------
# 6) BFS on a graph — a queue in its natural habitat
# ------------------------------------------------------------------
def bfs_shortest_path(graph: dict[int, list[int]], start: int, goal: int) -> list[int] | None:
"""Return the shortest path (as a list of nodes) from start to goal, else None. O(V + E)."""
if start == goal:
return [start]
seen = {start}
parent: dict[int, int] = {start: -1}
q: deque = deque([start])
while q:
node = q.popleft()
for nbr in graph.get(node, []):
if nbr in seen:
continue
seen.add(nbr)
parent[nbr] = node
if nbr == goal:
# Reconstruct path by walking parents backwards
path: list[int] = [nbr]
while parent[path[-1]] != -1:
path.append(parent[path[-1]])
path.reverse()
return path
q.append(nbr)
return None
# ------------------------------------------------------------------
# 7) Perf demo — list.pop(0) vs deque.popleft
# ------------------------------------------------------------------
def perf_demo(n: int = 200_000) -> None:
import time
lst = list(range(n))
t = time.perf_counter()
while lst:
lst.pop(0)
print(f"list.pop(0) n={n:>7} : {time.perf_counter()-t:.3f}s (O(n^2))")
dq = deque(range(n))
t = time.perf_counter()
while dq:
dq.popleft()
print(f"deque.popleft n={n:>7} : {time.perf_counter()-t:.3f}s (O(n))")
if __name__ == "__main__":
print("valid '(()[]{})':", valid_parentheses("(()[]{})"))
print("valid '(]': ", valid_parentheses("(]"))
ms = MinStack()
for v in [3, 5, 2, 6, 1, 4]:
ms.push(v)
print("MinStack get_min after pushes:", ms.get_min()) # 1
ms.pop(); ms.pop() # pop 4, 1
print("MinStack get_min after pops:", ms.get_min()) # 2
print("next_greater [2,1,2,4,3]:", next_greater([2, 1, 2, 4, 3]))
print("daily_temperatures [73,74,75,71,69,72,76,73]:", daily_temperatures([73, 74, 75, 71, 69, 72, 76, 73]))
graph = {1: [2, 3], 2: [4], 3: [4, 5], 4: [6], 5: [6], 6: []}
print("BFS 1 -> 6:", bfs_shortest_path(graph, 1, 6))
perf_demo()Anatomy of the script
What each block teaches
class QueueFromStacks:
def __init__(self):
self.inbox = []
self.outbox = []
def enqueue(self, x):
self.inbox.append(x)
def dequeue(self):
if not self.outbox:
while self.inbox:
self.outbox.append(self.inbox.pop()) # reverse into outbox
return self.outbox.pop()
q = QueueFromStacks()
for x in [1, 2, 3, 4]:
q.enqueue(x)
print(q.dequeue(), q.dequeue(), q.dequeue(), q.dequeue()) # 1 2 3 4This is a classic interview follow-up. The elegance is in the amortised analysis: any single dequeue could be O(n), but over any sequence of n operations the total work is O(n) — average O(1) per op.
(d) Production reality · 15 min
LinkedIn had 30+ backend services and every one had a bespoke way of shipping ‘events’ (profile updates, connection accepts, etc.) to the analytics team. Point-to-point integrations exploded to N² pipes; every schema change broke something.
queue = list(); queue.pop(0). Works fine on 1,000 URLs in dev. In staging with 500,000 URLs, it takes 8 HOURS to crawl what should take 30 minutes.queue = collections.deque(); queue.popleft(). Time drops to 30 minutes. Zero other code changes.deque. Full stop.awaits 100 database calls in sequence. Every request pushes 100 microtasks onto the event loop queue. Under load, the queue backs up. p99 latency balloons past the health-check timeout. Pods start OOM-killing.Promise.all to batch the calls in parallel (or push the work to a Kafka queue for a worker to handle). Every runtime has a task queue with finite drain rate — treat it like a resource.Where this shows up in the rest of the plan
(e) Recall + stretch · 10 min
Explain-out-loud test
Teach these three in one minute each, no notes:
- What's the difference between a stack and a queue, and give one everyday example of each.
- Why is
list.pop(0)a performance bug and what should you use instead? - What's the monotonic-stack pattern in one breath — and what problem does it solve?
What comes next
Hub: The 6-Month Learning Plan
Part of a 130-session evergreen learning series. Session structure: intuition → visual → hands-on → production war stories → recall. Duration: 90 minutes.