AmouAI Hub/Courses/Data Structures & Algorithms/Chapter 6
Stacks
A pile of plates: the last one on is the first one off. That single restriction is exactly what you need to match brackets, undo an action, and understand how functions call each other.
By the end of this chapter you can
- Implement a stack on top of a list and justify why
append/popare the right end - Explain why restricting access to one end makes a structure more useful, not less
- Write a bracket matcher and say why counting brackets cannot work
- Trace the call stack of a recursive function and connect it to
RecursionError - Recognise a monotonic-stack problem and explain why the nested loop is still O(n)
1One end, and only one end
A stack is a list with most of its abilities deliberately removed. That is the entire design.
A stack of plates in a cafeteria. You put a clean plate on top; you take a plate from the top. Nobody slides a plate into the middle of the pile, and nobody takes the bottom one out.
It would be possible to reach into the middle. The point is that not being able to is what makes the pile useful: you always know exactly which plate you are getting.
A stack supports exactly two operations, both on the same end:
- push(x) — put x on the top
- pop() — remove and return the top
Usually plus peek() (look at the top without removing) and is_empty(). That is the whole interface. Last in, first out — LIFO.
# In Python you do not need a class. A list IS a stack:
stack = []
stack.append("A") # push — O(1) amortised
stack.append("B") # push
top = stack[-1] # peek — O(1)
x = stack.pop() # pop — O(1), returns C
empty = not stack # is_empty
# Use the END of the list, never the front. append/pop are O(1);
# insert(0, x)/pop(0) are O(n) and would make every operation linear.Because you can only touch the top, both operations are O(1) with no searching
and no shifting — and, more importantly, the structure now means something. A stack
is a record of what is still unfinished, most recent first.
Every use in this chapter is really that same sentence in a different costume.
2Matching brackets
The first problem where a stack is not merely convenient but necessary. Counting cannot solve it; remembering order can.
Is {[a+(b*c)]-d} correctly bracketed? A first instinct is to count: as many
( as ), and so on. That fails immediately:
"[(])" # one of each kind of bracket — counts balance perfectly
# and it is obviously wrongThe problem is that brackets have order: the most recently opened bracket must be the next one closed. “Most recent, first out” is the definition of a stack, so a stack is not one possible tool here — it is the shape of the problem itself.
def is_balanced(s):
pairs = {")": "(", "]": "[", "}": "{"}
stack = []
for ch in s:
if ch in "([{":
stack.append(ch) # we now owe a matching close
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 a failure
# O(n) time, O(n) space in the worst case ( H ).
#
# Two failure modes, and both must be checked:
# 1. a closer that does not match the top → I
# 2. openers left on the stack at the end → J Every compiler, every JSON parser, every syntax highlighter that greys out an unclosed string, every editor that flashes the matching brace — all of them run some version of this loop.
The general principle: a stack is how you parse anything nested. Arithmetic expressions, HTML tags, nested function calls, XML. Nesting is LIFO.
3The call stack
You have been using a stack since your first program. It is the mechanism that makes function calls work at all.
When a function is called, the machine has to remember where to come back to and what the local variables were. It pushes a stack frame holding exactly that. When the function returns, the frame is popped and execution resumes where the frame said.
Why a stack and not something else? Because calls nest perfectly: the function that started most recently is always the one that finishes first. That is LIFO, and nothing else would work.
def a():
print("a starts")
b() # push a frame for b; a's frame WAITS below it
print("a ends") # ← the return address stored in b's frame
def b():
print("b starts")
c()
print("b ends")
def c():
print("c starts")
print("c ends")
a()
# Stack over time (top on the right):
# [a] → [a, b] → [a, b, c] → [a, b] → [a] → []
#
# Output: a starts, b starts, c starts, c ends, b ends, a ends.
# The G come out in exactly reverse order. That is the stack talking.Python limits the call stack to about 1,000 frames. Recurse deeper and you get
RecursionError instead of the memory corruption that a real stack overflow causes in
C.
When you hit it, the useful question is not “how do I raise the limit?” but “is my base case wrong, or is my recursion too deep for recursion?” A missing base case is a bug. Genuine depth means rewriting the recursion as a loop with an explicit stack — which is exactly what the next section is about.
Turning recursion into iteration
Any recursive function can be rewritten with an explicit stack, because that is all recursion is. This matters when the depth would blow the call stack:
# Recursive — elegant, O(depth) call-stack frames
def sum_nested(x):
if isinstance(x, list):
return sum(sum_nested(item) for item in x)
return x
# Iterative with an explicit stack — same result, no frames
def sum_nested_iter(x):
total = 0
stack = [x]
while stack:
item = stack.pop()
if isinstance(item, list):
stack.extend(item) # push all children
else:
total += item
return total
# The list `stack` is doing precisely the job the machine's call stack
# was doing. The only difference is that it lives on the heap, where
# there is no 1,000-frame limit.4The monotonic stack
The technique that makes stacks more than a data-structures-course exercise. It turns a family of O(n²) problems into O(n).
The problem: for each element of an array, find the next element to its right that
is larger. Brute force checks every pair: O(n²).
The insight: keep a stack of elements still waiting for their answer, and maintain it in decreasing order. When a new value arrives, it is the answer for everything on the stack smaller than it — so pop them all and record it.
def next_greater(xs):
result = [-1] * len(xs)
stack = [] # holds INDICES, in decreasing value order
for i, x in enumerate(xs):
while stack and xs[stack[-1]] < x:
result[stack.pop()] = x # x is the answer for that waiting index
stack.append(i)
return result # anything left has no greater element
next_greater([2, 1, 2, 4, 3, 5]) # [4, 2, 4, 5, 5, -1]There is a while inside a for, which looks quadratic. It is not.
Each index is pushed exactly once and popped at most once. So across the whole run, the inner loop executes at most n times in total — not n times per outer iteration. Total work is at most 2n.
This is the same amortised argument as the dynamic array in Chapter 3 and the sliding window in Chapter 4. Recognising it is worth more than memorising any single algorithm: a nested loop whose counters only ever move forward is linear.
What the pattern looks like in the wild
Once you know the shape, you see it everywhere. All of these are the same algorithm with a different comparison:
| Problem | Stack holds | Pop when |
|---|---|---|
| Next greater element | Indices, values decreasing | A larger value arrives |
| Next smaller element | Indices, values increasing | A smaller value arrives |
| Daily temperatures (days until warmer) | Indices, temps decreasing | A warmer day arrives |
| Largest rectangle in a histogram | Indices, heights increasing | A shorter bar arrives |
| Stock span | Indices, prices decreasing | A higher price arrives |
The wording is almost always “for each element, find the nearest element to its left/right that is bigger/smaller.” If you find yourself about to write two nested loops over the same array comparing pairs, stop and ask whether a monotonic stack applies.
5Undo, and other stack-shaped problems
A last sweep of places a stack is the right answer, to build the recognition reflex.
Undo and redo
Two stacks. Every action pushes onto the undo stack. Undo pops from it and pushes onto the redo stack. A new action clears the redo stack — which is exactly the behaviour you have always experienced in an editor and probably never wondered about.
class Editor:
def __init__(self):
self.text = ""
self.undo_stack = []
self.redo_stack = []
def type(self, s):
self.undo_stack.append(self.text)
self.redo_stack.clear() # a new action invalidates the redo history
self.text += s
def undo(self):
if self.undo_stack:
self.redo_stack.append(self.text)
self.text = self.undo_stack.pop()
def redo(self):
if self.redo_stack:
self.undo_stack.append(self.text)
self.text = self.redo_stack.pop()Evaluating expressions
Reverse Polish notation — 3 4 + 2 * — evaluates with one stack and no
parentheses at all. Push numbers; on an operator, pop two, combine, push the result. Compilers
convert infix to postfix precisely so this becomes possible.
Depth-first search
Chapter 20 explores graphs two ways. The only difference between breadth-first and depth-first search is whether the frontier is a queue or a stack. Swap one for the other and the behaviour flips completely — same code, different container.
Backtracking
Chapter 27's “choose, explore, un-choose” loop is a stack of decisions. Un-choosing is a pop.
Reach for a stack whenever the problem involves nesting, reversal, or “the most recent unfinished thing.”
If the answer to “which one do I deal with next?” is the newest, you want a stack. If it is the oldest, you want a queue — and that is the next chapter.
What to carry forward
- A stack allows access at one end only. The restriction is the feature: both operations are O(1) and the structure means 'what is unfinished, newest first'.
- Nesting is LIFO. Counting brackets cannot work because order matters; a stack remembers order.
- The call stack is a real stack of frames. RecursionError is Python refusing to overflow it, and the fix is usually an explicit stack, not a higher limit.
- A monotonic stack turns 'nearest larger/smaller element' from O(n²) into O(n) — each index is pushed once and popped once.
- In Python, use
list.appendandlist.pop. Neverinsert(0)/pop(0), which are O(n). - Newest first → stack. Oldest first → queue.
>_Playground
A stack class, a bracket matcher and a monotonic stack, all runnable. Try feeding the bracket matcher strings that should fail.
✓Exercises
Checked automatically the moment you submit. Work top to bottom — each one assumes the last. Your answers are saved in this browser.
Chapter 7 — Queues, Deques and Circular Buffers
The queue at a coffee shop: fair, boring, and the backbone of everything from print spoolers to breadth-first search.