Search Tech Journey

Find topics, journeys and posts

6-month learning plan26 / 130
back to blog
pythonbeginner 50m read

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.

🧩DSAM03 · Data Structures & Algorithms· Session 026 of 130 90 min

🎯 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).

You will be able to
  • 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

Stack of plates vs line at the DMV
🌍 Real world

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.

💻 Code world

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 + queue API — that's it
  • 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

  1. 1946
    Turing describes the call stack
    Alan Turing's ACE report proposes ‘nested subroutines’ backed by a pushdown stack — the mechanism that would define every language runtime for 80 years.
  2. 1957
    FORTRAN uses fixed-frame calling
    No recursion — one static frame per subroutine. Programmers cursed and demanded stacks.
  3. 1961
    Dijkstra's shunting-yard
    Two stacks convert infix arithmetic to postfix. Every calculator, every SQL parser, every regex engine still uses variants.
  4. 1972
    Unix pipes = queues between processes
    The | operator wires stdout of one process to stdin of the next — a bounded FIFO in kernel memory.
  5. 1998
    Web browser history stack
    Netscape's Back button implements an undo stack. Every SPA still models routing as a stack.
  6. 2013
    React uses a stack for reconciliation
    The 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)

11
Iterate left → right

For each element x, we want to know ‘what is the next element to the right that's greater than x?’

22
Keep a stack of INDICES with strictly decreasing values

This is the invariant: the top of the stack always holds the smallest ‘still unanswered’ value seen so far.

33
While top of stack < x, pop it

The current x is the answer for the popped index. Record it.

44
Push current index

It's now waiting for its own answer.

55
Analysis

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

Function call stack

Every language

  • Each call pushes a stack frame (locals + return address)
  • Return pops it
  • Overflow = StackOverflowError / segfault
  • This is why deep recursion crashes
Expression parser

SQL, regex, JSON

  • Two-stack shunting-yard
  • One for operators, one for operands
  • Precedence resolution via pop-while
  • Same pattern in every parser generator
Browser Back button

History API

  • Push on navigate
  • Pop on Back
  • Also the model for editor undo/redo
  • ‘SPA routers’ are a wrapper over this
React Fiber reconciler

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

BFS traversal
Standard breadth-first search on any graph or tree. Dequeue a node, enqueue its unvisited neighbours. See S028 and S030.
algorithm
Print / job spoolers
First document submitted is first printed. CUPS, Kubernetes job queues, Celery workers.
system
Message queues (Kafka, SQS, RabbitMQ)
Distributed FIFO. Producers enqueue, consumers dequeue. The pattern from S055–S060.
system
Event loops
Node.js, browser JavaScript, Python asyncio all wrap a FIFO task queue. setTimeout(fn, 0) means ‘enqueue fn at the back’.
runtime
Rate limiting (leaky bucket)
Requests enter a fixed-size queue at the back, are processed off the front at a steady rate. Excess is dropped.
system

Common misconception
✗ What most people think

"A stack and a queue are different data structures. I pick one, and I've made a data-structure decision."

✓ What is actually true

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.

Why the myth is so sticky

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.

Prove it to yourself

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 paths
From first principles
Start with the question

Why is list.pop(0) O(n) while deque.popleft() is O(1)? Both remove one element. The difference is not an optimisation someone forgot.

  1. 1
    A 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 be base + i·itemsize — pure arithmetic, no search
  2. 2
    That 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
  3. 3
    So 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
  4. 4
    That 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
  5. 5
    A 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

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.

Mental modelPlate stack vs. ticket line

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.
🔔 Fires when you see

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.

The tradeoff

You are processing a backlog of work items that themselves spawn more work. Drain it LIFO (stack) or FIFO (queue)?

LIFO — stack
+ you gain memory stays proportional to the depth of the work tree, not its width; the most recently produced item is hottest in cache; finishes whole sub-branches before starting new ones, so partial results are complete subtrees
− you pay starvation — an item pushed early can wait arbitrarily long while newer work keeps arriving; latency for the oldest item is unbounded
pick when work is a deep tree, memory is the binding constraint, and per-item latency does not matter — crawlers doing depth-limited exploration, dependency resolution, cycle detection
FIFO — queue
+ you gain bounded, fair latency: the oldest item is always next, so no starvation; explores in level order, which is what "shortest" and "freshest first" both require
− you pay memory scales with the width of the frontier, which on a branching workload explodes exponentially; and cache locality is poor because the next item was produced long ago
pick when you need latency SLAs or shortest-path semantics — any user-facing job queue, any BFS, any streaming consumer
Priority queue
+ you gain you choose the ordering explicitly rather than inheriting it from the container; can encode deadline, cost, or business priority
− you pay O(log n) per op instead of O(1), and you now own a starvation problem you must actively solve with ageing
pick when items genuinely differ in urgency and you can name the priority function — otherwise you have added complexity to reimplement FIFO badly
What a senior engineer actually does

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

valid_parentheses
The canonical ‘matching brackets’ stack pattern. Push openers, pop on closers, check the pair. Also the shape of every syntax parser.
stack
MinStack
The auxiliary-stack trick: pushing (value, running_min) pairs (or a parallel stack) makes get_min O(1) without scanning.
stack
next_greater
Monotonic stack template. Stack holds INDICES with strictly decreasing values. When a bigger value arrives, it answers all the smaller pending indices at once.
monotonic
daily_temperatures
Same pattern as next_greater but the answer is a DISTANCE, not a value. Notice how ans[j] = i - j uses stored indices.
monotonic
Queue class
Thin wrapper around deque. Never use list for a queue — see the perf_demo.
queue
bfs_shortest_path
BFS is a queue in action. First-time-you-see-a-node is via the shortest number of edges → the queue pops the closest nodes first.
queue
perf_demo
Live demonstration that list.pop(0) is O(n) and deque.popleft is O(1). Prints a 100–1000× ratio on 200k elements.
perf
Try itImplement a queue using two stacks and analyse the amortised cost
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 4

This 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.

💡 Hint · One stack for ‘input’, one for ‘output’. On dequeue, if output is empty, pop everything from input into output (reversing the order). Each element is moved at most twice → amortised O(1) per dequeue.

(d) Production reality · 15 min

War story LinkedIn / Kafka origin story · 2011· 2011500 billion messages/day
🔥 What broke

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.

🧯 The fix
Jay Kreps, Neha Narkhede, and Jun Rao designed Kafka: a distributed durable FIFO queue. Producers append to topic partitions; consumers pull at their own pace. One central bus replaced N² integrations.
🎓 Lesson to steal
Message queues aren't just data-structure academic trivia. They're the backbone of every high-throughput system you'll ever build. Kafka today handles 500B+ msgs/day at LinkedIn alone.
Post-mortem
War story Every Python codebase · every yearsilent O(n²) tax
🔥 What broke
Team builds a URL crawler with 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.
🧯 The fix
Swap queue = collections.deque(); queue.popleft(). Time drops to 30 minutes. Zero other code changes.
🎓 Lesson to steal
‘list is a queue’ is the single most common Python performance bug on real teams. If you're popping from the front, use deque. Full stop.
War story Node.js event loop · every misuselatency bomb
🔥 What broke
A backend engineer writes a hot endpoint that 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.
🧯 The fix
Use 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.
🎓 Lesson to steal
The event loop IS a FIFO queue. Backpressure and queue depth are things you must monitor in production, not just theory-book concepts.

Where this shows up in the rest of the plan

Stacks + queues appear everywhere
S027 · Recursion
The call stack IS a stack. Iterative → recursive is literally ‘use the language's stack instead of your own’.
S028 · Trees & BSTs
DFS = stack, BFS = queue. Same code shape, opposite container.
S029 · Heaps & PQs
‘Priority queue’ is technically a heap, not a queue — but people call it that.
S030 · Graphs
Every graph traversal is one of stack (DFS), queue (BFS), or priority queue (Dijkstra).
S055 · Kafka / SQS
Distributed FIFO queues as the backbone of async architectures.
S073 · Event Loops & Concurrency
asyncio, Node.js, browser event loops — all task queues.

(e) Recall + stretch · 10 min

Recall — click to reveal · click to reveal
★ = stretch question

Explain-out-loud test

Teach these three in one minute each, no notes:

  1. What's the difference between a stack and a queue, and give one everyday example of each.
  2. Why is list.pop(0) a performance bug and what should you use instead?
  3. 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.