Skip to main content

Stacks & Queues

Overviewโ€‹

Stacks and queues are not new storage โ€” they are restrictions on storage. Both hold a sequence, and both deliberately refuse to let you reach into the middle of it. That refusal is the feature: an interface with two operations has far fewer ways to be used incorrectly, and the restriction is exactly what makes the operations O(1).

Core Conceptsโ€‹

StackQueueDeque
DisciplineLIFO โ€” last in, first outFIFO โ€” first in, first outBoth ends
Addpush (to the top)enqueue (to the back)push_front / push_back
Removepop (from the top)dequeue (from the front)pop_front / pop_back
Inspectpeek / topfrontfront / back
ModelsNesting, backtracking, undoFairness, buffering, arrival orderBoth, plus sliding windows

All operations are O(1). search is not part of either interface; if you need it, you have chosen the wrong structure.

A stack drawn as a vertical column of elements with push and pop arrows both acting on the topmost element
Push and pop both act on the same end. Nothing below the top is reachable without removing what sits above it. Wikimedia Commons, Public domain

Architecture / Mechanismโ€‹

Implementationโ€‹

A stack is a dynamic array with two of its operations hidden โ€” appending and removing at the end are already O(1) amortized:

stack = []
stack.append(x) # push
top = stack[-1] # peek
x = stack.pop() # pop

A queue is the case where the array representation goes wrong. Removing from the front of an array is O(n), so the naive version is quadratic:

queue = []
queue.append(x) # O(1)
x = queue.pop(0) # O(n) โ€” every remaining element shifts down

The fix is a circular buffer: keep head and tail indices into a fixed array and wrap them modulo capacity, so neither end ever moves data. That is what real deque implementations do (Python's collections.deque uses a doubly linked list of fixed-size blocks, which achieves the same O(1) ends while allowing unbounded growth).

from collections import deque

q = deque()
q.append(x) # enqueue at the back โ€” O(1)
x = q.popleft() # dequeue from the front โ€” O(1)
Use the right type, or the complexity silently changes

list.pop(0) and list.insert(0, x) are O(n) in Python; deque.popleft() and deque.appendleft() are O(1). The same trap exists as ArrayList versus ArrayDeque in Java, and std::vector versus std::deque in C++. Nothing warns you โ€” the loop simply becomes quadratic.

Where stacks are the machine, not a choiceโ€‹

The call stack is a stack because function calls nest: a function returns to its most recent caller, which is precisely LIFO. Every recursive algorithm therefore uses a stack whether or not it names one, and any recursion can be rewritten iteratively by managing that stack yourself โ€” which is how you avoid stack-overflow on deep inputs.

# Recursive depth-first traversal โ€” the call stack does the bookkeeping
def dfs(node):
if node is None:
return
visit(node)
dfs(node.left)
dfs(node.right)

# The same traversal with an explicit stack โ€” bounded by heap, not stack size
def dfs_iterative(root):
stack = [root]
while stack:
node = stack.pop()
if node is None:
continue
visit(node)
stack.append(node.right) # pushed first, so popped last
stack.append(node.left)

Practical Usageโ€‹

ProblemStructureWhy
Matching brackets, parsing expressionsStackNesting is LIFO by definition
Undo/redoTwo stacksThe most recent action is the first to reverse
Depth-first searchStackExplore deepest-first
Breadth-first searchQueueExplore nearest-first
Task/job scheduling, request bufferingQueuePreserves arrival order โ€” fairness
Producer/consumer between threadsConcurrent queueThe handoff point, with backpressure
Sliding-window maximumDequePush at the back, evict stale entries from the front

A worked example โ€” bracket matching, which is the canonical use and about as short as an algorithm gets:

def balanced(s):
pairs = {")": "(", "]": "[", "}": "{"}
stack = []
for ch in s:
if ch in "([{":
stack.append(ch)
elif ch in pairs:
if not stack or stack.pop() != pairs[ch]:
return False # wrong closer, or nothing open
return not stack # anything left open is unbalanced

Edge Cases & Pitfallsโ€‹

  • Popping an empty stack is the most common bug in this code. Decide deliberately whether it raises, returns a sentinel, or is a precondition the caller must check โ€” and be consistent.
  • Unbounded queues remove backpressure. A queue that grows without limit converts an overload into memory exhaustion instead of a visible slowdown. Bound it and choose what happens when full; see the thread pool discussion for the same hazard in another setting.
  • std::stack and std::queue are adaptors, not containers โ€” they wrap deque by default. This matters when you want a different underlying container for cache or memory reasons.
  • Recursion depth is a real limit. Python defaults to ~1000 frames; a deep tree or a long linked list will exhaust it. The iterative rewrite above is the fix, not a larger limit.

Comparisonsโ€‹

Array-backedLinked-list-backed
Stack push/popO(1) amortized, contiguousO(1) always, one allocation each
Queue operationsO(1) with a circular bufferO(1) with head and tail pointers
MemoryCompact, may over-allocateOne or two pointers per element
Worst-case latencyOccasional O(n) resizeNo resize spike

Array-backed is the right default; linked-list-backed matters when a single O(n) resize pause is unacceptable.

Referencesโ€‹

  • Cormen, Leiserson, Rivest & Stein, Introduction to Algorithms, ยง10.1 โ€” stacks and queues, including the circular-buffer queue.
  • CPython collections.deque implementation โ€” the block-based doubly linked list described above.

Books & Videosโ€‹

  • Sedgewick & Wayne, Algorithms, 4th ed., ยง1.3 โ€” "Bags, Queues, and Stacks", with both implementations developed side by side.