Chapter 7
12 min read
Section 37 of 261

The Stack Abstraction

Stacks

The LIFO Idea

Imagine a tray dispenser at a cafeteria, the kind with a spring-loaded base. Trays go in at the top; the top tray comes out first. Or stack a deck of plates: you can only put a plate on the top of the pile, and you can only take a plate off the top. The plate at the bottom of the pile—the one you put down first—is the last one you'll get back.

That single rule—last in, first out, abbreviated LIFO—is the whole story of a stack. Every operation, every algorithm, every weird kernel data structure built on top of a stack derives from this one constraint. It sounds restrictive, and it is. The restriction is exactly what makes stacks powerful: it forces a particular discipline of access that mirrors how humans (and compilers, and CPUs) handle nested things.

Why this matters: When you nest something inside something else—a function call inside another, a subexpression inside a bigger expression, a folder inside a folder, a parenthesis inside another parenthesis—you need to finish the inner thing before you can finish the outer thing. That discipline is exactly what a stack enforces. If you can recognize nesting, you have spotted a stack.

Stack as an Abstract Data Type

A stack is an abstract data type (ADT): we specify it by what it does, not by how it's built. Three core operations and two auxiliary ones:

OperationSignatureEffect
push(x)stack × element → stackPlace x on top. Stack now has one more element.
pop()stack → element × stackRemove and return the top element. Fail if empty.
peek() (a.k.a. top())stack → elementReturn the top element WITHOUT removing it. Fail if empty.
is_empty()stack → boolTrue iff the stack contains no elements.
size()stack → intNumber of elements currently held.

Notice what is not on this list: there is no stack[k], no find(x), no insert_at(i, x). A stack does not let you reach inside. The only element addressable from the outside is the top. If you want anything below the top, you must pop everything above it first.

ADT vs implementation

The stack ADT says nothing about arrays or linked lists or pointers. Section 7.2 will show two completely different implementations—one with an array, one with a linked list—that satisfy this contract identically. From the outside, you cannot tell them apart by behavior; only by their performance constants and memory profile.

The Algebraic Axioms of a Stack

We can pin down the meaning of “LIFO” with four equations. Let ss be any stack and xx any element. Then:

  1. pop(push(s,x))=(x,s)\text{pop}(\text{push}(s, x)) = (x, s) — pushing then popping returns exactly xx and leaves the original ss behind.
  2. peek(push(s,x))=x\text{peek}(\text{push}(s, x)) = x — the top of a freshly pushed stack is the thing we just pushed.
  3. is_empty(empty)=true\text{is\_empty}(\text{empty}) = \text{true} — the empty stack reports itself as empty.
  4. is_empty(push(s,x))=false\text{is\_empty}(\text{push}(s, x)) = \text{false} — any push makes the stack non-empty.

These four lines fully specify a stack. Given any sequence of pushes and pops, you can derive its final state and the value returned by every pop using only these rewrite rules. That's why it's called an algebraic specification: the equations behave like algebra, and any implementation that respects them is a correct stack—regardless of whether it's built from arrays, linked nodes, or carrier pigeons.

Why care about the axioms?

When you write a stack-based algorithm, you implicitly use these equations. The classic correctness argument for parentheses matching, expression evaluation, and DFS termination all boil down to: “the stack's state after step kk equals what the axioms predict, so invariant II still holds.” Knowing the axioms by heart turns proofs into exercises.

Cost of Every Operation

A well-implemented stack hits the same complexity table regardless of whether the underlying buffer is a dynamic array or a singly linked list. The only operation in the public surface area is “touch the top,” and the top is always one pointer/index away.

OperationTime (worst)Time (amortized)Space addedNotes
pushO(1)O(1)+1 elementArray variant pays O(n) once when buffer doubles
popO(1)O(1)−1 elementNo shifting; the top vanishes by index decrement
peekO(1)O(1)0Pure read
is_emptyO(1)O(1)0Compare size to zero
sizeO(1)O(1)0Stored as a counter — never recomputed
Every operation a client of a stack can ask for is constant time. That uniformity is the structural reason stacks are popular as building blocks: when you compose them into bigger algorithms (DFS, expression evaluation, undo/redo, function calls), the inner stack work disappears into the noise.

Interactive: Stack Visualizer

Below is a live stack. Cells stack vertically—the bottom cell was pushed first, the top cell was pushed last. Use push, pop, and peek to feel the rhythm. Watch what happens when you try to pop an empty stack, or push onto a full one.

size = 2 / 8
base of stack7[0]3[1]top
> stack initialized: [7, 3] <- top

Cells stack vertically. push drops a green cell onto the top with a small bounce; pop lifts the top cell off in red; peek highlights the top in blue without removing it. Notice all three are O(1) — we only ever touch the top, never anything below.

Three things to notice as you play:

  • The top is always at the same end. No matter how many elements are on the stack, every operation happens at the very top. We never reach down.
  • Order is total. If you push A, B, C, you will pop C, B, A. There is no “skip the middle” primitive—and that constraint is exactly what makes the structure useful for nested problems.
  • Pop on empty fails. A stack with no elements has no top. Some libraries return a sentinel; others raise. Both choices are defensible, but every robust stack does something predictable in this case.

A Stack in Python

With the contract clear, the implementation is short. Python's built-in list is already a perfect underlying buffer—append and pop at the tail are amortized O(1). We wrap a list in a class not because we need to, but because the wrapper enforces the LIFO discipline and gives our stack a name in the type system.

Stack — minimal Python implementation
🐍python
1Class declaration

We wrap a Python list and expose only the four stack operations. This is encapsulation — clients of Stack cannot reach in and do `_data[3]` because the leading underscore signals 'private'. The discipline matters: the moment you allow random access, you no longer have a stack.

2Constructor

__init__ runs once per Stack(). No arguments — every stack starts empty. This is one of the algebraic axioms in code form: empty() is the canonical 'starting' stack.

3The underlying buffer

We use a Python list as the storage. Python lists are dynamic arrays: append/pop at the END are amortized O(1) because the list reserves spare capacity. This is exactly the property push/pop need.

EXAMPLE
self._data = []   # contiguous heap buffer with spare slots
5push signature

push takes one element and returns nothing. The hint `-> None` documents that — push mutates the stack but produces no value. It's a *command*, not a *query*.

6Append at the tail

list.append is the canonical O(1) tail-add in Python. The element lands at index len(self._data); that index is also the new top. Crucially we do NOT use insert(0, x) — that would shift every existing element and make push O(n).

LOOP TRACE · 4 iterations
before push(7)
_data = []
after push(7)
_data = [7]
after push(3)
_data = [7, 3]
after push(9)
_data = [7, 3, 9]
8pop signature

pop is a *command-query*: it both removes the top AND returns it. That double duty is fine for stacks because the LIFO contract makes it unambiguous — you always pop what you just peeked.

9Empty check first

Before touching the buffer, we check emptiness. This is a precondition: pop is only defined on non-empty stacks. Skipping the check would still fail (Python's list.pop raises IndexError too), but we want a *meaningful* error message at the right abstraction level.

10Raise on contract violation

We raise IndexError, matching list.pop()'s behavior, so existing exception handlers keep working. Some libraries prefer returning a sentinel like None — that pushes the burden of checking onto every caller and tends to hide bugs.

11Pop the tail

list.pop() with no argument removes and returns the LAST element. Like append, it's O(1) — Python just decrements the length and hands back the slot's value. That symmetry between append and pop is what makes a list a perfect stack buffer.

EXAMPLE
_data = [7, 3, 9]   ->   pop returns 9, _data becomes [7, 3]
13peek signature

peek (also called top in some libraries) is a pure *query*: returns the top, mutates nothing. No `-> None` here — peek must return the element.

16Read the last slot

Index -1 is Python's idiom for 'last element'. O(1) — Python lists know their length, so negative indexing is an arithmetic adjustment, not a scan. We do NOT call self._data.pop() and re-push it; that would be both slower AND wrong on edge cases (e.g. concurrent code).

EXAMPLE
_data = [7, 3, 9]   ->   peek returns 9, _data unchanged
18is_empty as a query

Returns a boolean. Conventionally we test `if not stack.is_empty()` rather than `if len(stack) > 0` — it reads more like prose and protects us if we later swap the underlying buffer for something whose length is expensive.

21__len__ dunder method

Implementing __len__ lets clients write `len(stack)` and `if stack:` (because empty is falsy in Python when len == 0). Free polymorphism with the rest of the language — this is why Python idioms feel uniform.

25Construct an empty stack

s now points at a fresh Stack with _data = []. No work has happened yet beyond allocating the wrapper object and one empty list — a few hundred bytes total.

26First push

After s.push(7), the buffer is [7]. The top is 7. The bottom is also 7 (the same element, because there's only one). is_empty() now returns False.

28After three pushes

Now _data = [7, 3, 9]. The order in memory matches the order of insertion, but the LIFO contract means we will retrieve them in REVERSE order: 9, then 3, then 7. That reversal is the entire useful property of a stack.

LOOP TRACE · 3 iterations
after s.push(7)
_data = [7]
top = 7
after s.push(3)
_data = [7, 3]
top = 3
after s.push(9)
_data = [7, 3, 9]
top = 9
29peek prints 9

peek returns the most-recently-pushed element. Critically, _data is unchanged after this line — print only reads; the next pop still has 9 to return.

30pop prints 9 and removes it

pop returns the same 9, but this time also removes it. After this line, _data = [7, 3] and the new top is 3. This pair (peek -> pop) is the cleanest demonstration of the algebraic axiom peek(push(s, x)) = x.

31len prints 2

len(s) calls our __len__, which returns len(self._data) = 2. We pushed 3 and popped 1, so 2 elements remain. The arithmetic is trivial — but it confirms the size invariant: size after n pushes and m pops (m <= n) equals n - m.

12 lines without explanation
1class Stack:
2    def __init__(self):
3        self._data: list = []
4
5    def push(self, x) -> None:
6        self._data.append(x)
7
8    def pop(self):
9        if not self._data:
10            raise IndexError("pop from empty stack")
11        return self._data.pop()
12
13    def peek(self):
14        if not self._data:
15            raise IndexError("peek on empty stack")
16        return self._data[-1]
17
18    def is_empty(self) -> bool:
19        return len(self._data) == 0
20
21    def __len__(self) -> int:
22        return len(self._data)
23
24# Usage
25s = Stack()
26s.push(7)
27s.push(3)
28s.push(9)
29print(s.peek())   # 9
30print(s.pop())    # 9
31print(len(s))     # 2

Could you just use a list directly?

Yes. In small scripts, stack = [] with stack.append(x) and stack.pop() is perfectly idiomatic Python. The Stack class is worth the seven extra lines when (a) you want a self-documenting type signature in a larger codebase, or (b) you might later swap the implementation (e.g. for a thread-safe one) without touching call sites.

The Same Stack in TypeScript

Translating the same ADT into TypeScript shows where the language differences matter. The algorithm is identical; the type system does more work for us at compile time.

Stack<T> — generic TypeScript implementation
📘typescript
1Generic class declaration

`<T>` makes Stack parametric in the element type. Stack<number>, Stack<string>, Stack<TreeNode> all share the same code but get full type-checking. Python's version is dynamically typed; here the compiler enforces the contract.

2Private buffer typed as T[]

`private` is enforced at compile time — clients cannot read or write data from outside the class. T[] is exactly an array of the chosen element type. JavaScript arrays are also dynamic, like Python lists, so push/pop at the tail are O(1) amortized.

4push method

Takes one T, returns void. Same shape as Python — a command, not a query. The compiler will reject `s.push('hello')` on a Stack<number> at compile time, before the bug reaches production.

5Delegate to Array.prototype.push

JS's built-in array push appends and returns the new length. We discard the length and let the type-system prove void.

8pop returns T (not T | undefined)

We promise the caller they get a T back. To honor that, we throw on empty rather than returning undefined. This is the same API choice as Python — fail loudly on contract violations.

9Array.pop returns T | undefined

JS's pop is forgiving — it returns undefined on an empty array. TypeScript faithfully types that as `T | undefined`. We narrow with the next line.

10Type-narrowing throw

After `if (x === undefined) throw`, the TS compiler knows x is T on the next line. This is *flow analysis* doing real work for us — no cast, no `!` non-null assertion needed.

14peek with explicit length check

Unlike Python's negative indexing, JS arr[-1] returns undefined silently — not the last element. We index by length-1 explicitly, after guarding against length 0.

19isEmpty

Same shape as Python. Boolean, no parameters, O(1).

22size as a getter

Using a getter lets clients write `s.size` (no parens) like a property. Symmetric with how arrays expose `.length`.

27Instantiate Stack<number>

`new Stack<number>()` allocates a fresh stack whose elements must be numbers. The angle-bracket annotation is the type argument; at runtime it has no effect, but the compiler uses it everywhere.

32Same observable behavior as Python

peek prints 9, pop prints 9 and removes it, size prints 2. Two languages, two type systems, identical algorithm. That's the value of an ADT — it's specified by behavior, not by code.

LOOP TRACE · 3 iterations
after pushes
data = [7, 3, 9]
after peek()
returned = 9
data = [7, 3, 9]
after pop()
returned = 9
data = [7, 3]
22 lines without explanation
1class Stack<T> {
2  private data: T[] = [];
3
4  push(x: T): void {
5    this.data.push(x);
6  }
7
8  pop(): T {
9    const x = this.data.pop();
10    if (x === undefined) throw new Error("pop from empty stack");
11    return x;
12  }
13
14  peek(): T {
15    if (this.data.length === 0) throw new Error("peek on empty stack");
16    return this.data[this.data.length - 1];
17  }
18
19  isEmpty(): boolean {
20    return this.data.length === 0;
21  }
22
23  get size(): number {
24    return this.data.length;
25  }
26}
27
28const s = new Stack<number>();
29s.push(7);
30s.push(3);
31s.push(9);
32console.log(s.peek());   // 9
33console.log(s.pop());    // 9
34console.log(s.size);     // 2

The same shape works in Java with ArrayDeque<T> (the standard recommends it over the legacy java.util.Stack), in Go with a slice and append/slice-trimming, and in Rust with Vec<T> (push/pop already part of the standard library). Stacks are universal.


First Application: Balanced Brackets

The single most common stack interview question, and not by accident—every compiler, JSON parser, XML parser, and IDE bracket-highlighter runs essentially this algorithm. Given a string like (a + [b * {c - d}]), decide whether every opening bracket has a matching closer in the right order.

The insight: the most-recently-opened bracket is the next one that needs to close. That phrase— “most recently opened”—is the LIFO contract in disguise.

Balanced brackets — 8 lines, O(n) time, O(n) space
🐍python
1Function signature

Takes a string of brackets (and optionally other characters we ignore) and returns True iff every opener has a matching closer in correct nesting order. This is the canonical first stack algorithm — it's how compilers, linters, and JSON parsers all sanity-check structure.

2Closer-to-opener map

When we see a closer, we need to know what opener it must match. A dictionary makes that lookup O(1). Building the table once, outside the loop, also documents the bracket grammar at a glance.

EXAMPLE
pairs[")"] == "("
pairs["]"] == "["
pairs["}"] == "{"
3Set of openers for fast membership

We could write `if ch in '([{'` but `ch in set('([{')` is O(1) instead of O(k). With only 3 openers, the difference is microscopic — but writing it correctly here builds the habit for larger alphabets.

4The stack

We use a plain Python list as our stack. The type hint `list` documents intent — we'll only call append (push) and pop on it, never index it from the front.

6Walk the string left-to-right

One pass over the input. The whole algorithm is O(n) in the length of expr because each character causes at most one push and one pop, both O(1).

7Is it an opener?

Three branches: opener (push), closer (check + pop), or other (ignore). Stacks are about classifying the current input and reacting to the *most recent unmatched context* — exactly what `stack[-1]` represents.

8Push the opener

Record that we owe a matching closer. The order on the stack is critical — the deepest unmatched opener is at the top, exactly the one a closer must match next. LIFO is *not optional* here; a queue would match brackets in the wrong order.

LOOP TRACE · 4 iterations
expr = "([{}])"
stack = []
after "("
stack = ["("]
after "(["
stack = ["(", "["]
after "([{"
stack = ["(", "[", "{"]
9Is it a closer?

Membership in `pairs` (a dict) is also O(1). If ch is neither an opener nor a closer, both branches fall through and the character is silently ignored — useful for `is_balanced('foo([1+2]*3)')`.

10Two failure modes

Closer with empty stack means a closer arrived before any opener — `)abc`. Closer with mismatched top means `([)]` — '(' was opened, then '[', then ')' tried to close '[' instead of the '(' it implies. Both are immediate fail.

11Bail out fast

Returning False here is correct AND efficient — no need to scan the rest of the string once we know it's malformed. This is the practical reason linters get fast feedback to your editor.

12Match — discard the opener

We don't *need* to record that the brackets matched; we just need to forget about the opener. Pop discards the top, leaving the previous unmatched context (the one above us in the nesting) exposed.

LOOP TRACE · 3 iterations
after "([{}"
stack = ["(", "["]
after "([{}]"
stack = ["("]
after "([{}])"
stack = []
13Final check: empty stack means balanced

If the stack still contains openers, those openers were never closed — `(((`. `not stack` is the Pythonic 'is empty' test for lists. The whole algorithm is 8 lines and handles unbounded nesting depth correctly because the stack itself can grow.

1 lines without explanation
1def is_balanced(expr: str) -> bool:
2    pairs = {")": "(", "]": "[", "}": "{"}
3    openers = set("([{")
4    stack: list = []
5
6    for ch in expr:
7        if ch in openers:
8            stack.append(ch)                    # push opener
9        elif ch in pairs:
10            if not stack or stack[-1] != pairs[ch]:
11                return False                    # mismatch or empty
12            stack.pop()                         # match — remove opener
13    return not stack                            # all openers consumed?
InputWalkResult
"()"push "(" → match ")" pop → emptyTrue
"([{}])"push (, push [, push {, pop {, pop [, pop ( → emptyTrue
"(]"push "(" → ")" expected but got "]" → mismatchFalse
"(("push (, push ( → end with stack non-emptyFalse
")"closer with empty stackFalse
""no characters, empty stackTrue (vacuously)

Two consequences worth carrying forward:

  • The stack's height equals the current nesting depth. If you push an opener and pop on a matching closer, the stack's peak height during the walk is the maximum nesting depth of the input. Compilers report this as the “maximum bracket depth” statistic.
  • This generalizes. Replace “openers and closers” with “function entry and return” and you have the call stack. Replace it with “BEGIN and END tags” and you have an XML parser. The algorithm is the same; only the alphabet changes.

Recursion and stacks are the same idea, viewed from two angles. Every recursive function call in any modern programming language pushes a stack frame onto the call stack: the frame holds the function's parameters, locals, and the return address. When the function returns, its frame is popped. The chain of currently-executing functions, from main down to the deepest active call, is literally a stack.

That observation has a stronger form: any recursive algorithm can be rewritten iteratively using an explicit stack, and vice versa. The two are interchangeable in expressive power; they differ only in who manages the stack—the language runtime, or you.

Recursive callEquivalent stack op
Function entrypush frame (params, locals, return PC) onto call stack
Function returnpop frame; jump to saved return PC
Recursive self-callpush a NEW frame for the same function with smaller args
Hitting the base casepop without further pushing — start unwinding
Stack overflow exceptionliteral: the call stack ran out of room

Converting Recursion to Iteration

Pre-order tree traversal is the canonical example. The recursive form:

🐍python
1def preorder(node):
2    if node is None:
3        return
4    print(node.value)
5    preorder(node.left)
6    preorder(node.right)

becomes, with an explicit stack:

🐍python
1def preorder_iter(root):
2    if root is None:
3        return
4    stack = [root]
5    while stack:
6        node = stack.pop()
7        print(node.value)
8        # Push right FIRST so left is processed next (LIFO)
9        if node.right is not None: stack.append(node.right)
10        if node.left  is not None: stack.append(node.left)

When you would do the conversion by hand

Two practical reasons to write the stack manually instead of using recursion: (1) the recursion depth could exceed the OS-imposed call stack limit (typically 1 MB / a few thousand frames on Linux); (2) you want to inspect or persist the partial state, e.g. checkpoint a long-running search.

Where Stacks Live in Real Systems

Stacks are everywhere—often invisibly. Once you know to look, you stop seeing applications and start seeing stacks-in-disguise.

SystemWhere the stack livesWhat it stores
CPU (x86, ARM, RISC-V)Hardware stack pointer (RSP / SP) plus PUSH/POP instructionsFunction frames, return addresses, saved registers — millions of pushes per second
JVM, .NET CLROperand stack inside every method frameIntermediate values during bytecode execution: e.g. iadd pops two, pushes their sum
Web browser (V8, SpiderMonkey)JavaScript call stack — origin of 'Maximum call stack size exceeded'Active function calls; ~10–15K frames before overflow on most engines
PostScript / PDFThe entire language IS stack-basedNumbers, operators, commands; '3 4 add' pushes 3, pushes 4, then add pops both and pushes 7
Forth, FactorTwo stacks: data + return — exposed to the programmerUsed heavily in firmware, the Open Firmware boot ROM (Sun, OLPC), and FPGA toolchains
Compilers / interpretersRecursive-descent parser stack, type-checker scope stack, code-gen operand stackProduction parsers like Bison emit stack machines explicitly
OS context switchKernel saves user-process register state by pushing onto the kernel stackEach thread has its own kernel stack; switch = swap stack pointers
Editors & browsersUndo / redo as two stacks (do-stack + redo-stack)Each user action pushes; Ctrl-Z pops do-stack to redo-stack; Ctrl-Y pops the other way
Graph / tree algorithmsDFS recursion stack (or explicit when avoiding overflow)Pending-to-visit nodes; the topmost frontier of the search
RPN calculators (HP-12C, dc, Forth)Visible operand stackNumbers entered, partial results — '3 4 + 2 *' computes (3+4)*2 with two pushes and two pops
Backtracking solversSudoku, n-queens, regex engines (Perl, Python re)The current partial assignment; on dead end, pop and try the next branch
Tower of HanoiEach peg literally IS a stack of disksDisk-on-disk stacking is the physical analog

Two observations from this list. First, hardware-level stacks predate software-level ones: the CPU stack pointer is older than most programming languages, and the entire calling convention of every modern operating system is built on it. Second, “stack-based” languages like PostScript and Forth show that you can build an entire programming model around the structure—not just use it as an internal data structure.


When NOT to Use a Stack

The LIFO discipline is a feature when you need it and a straitjacket when you don't. Reach for something else when:

  • You need fairness or first-come-first-served ordering. Print queues, request queues, BFS—these are FIFO and want a queue (Chapter 8).
  • You need random access by index. Use an array. arr[k] is O(1)O(1); reaching the kk-th-from-top element of a stack is O(k)O(k) and destroys the stack to do it.
  • You need to retrieve the smallest/largest element. Use a heap / priority queue (Chapter 14). A stack returns the most-recently-pushed element regardless of value.
  • You need fast membership checks. x in stack is O(n)O(n). Use a set or hash table.
  • The access pattern is unconstrained. If you genuinely need to read or modify any element on demand, you're reaching for a list, deque, or array, and the stack discipline buys you nothing.
A common beginner mistake is to use a stack “just to track elements” and then iterate over it from the bottom, which violates the abstraction. The moment you find yourself wanting non-top access, switch types. The stack ADT is intentionally narrow; widen the type, don't widen the abstraction.

Pitfalls and Edge Cases

  • pop on empty. Pick a policy and document it: raise (Python list, Java Deque), return a sentinel like None / undefined (JS Array, Go slice trim), or return an Option/Maybe (Rust, Haskell). The wrong choice is silently doing nothing—that hides bugs.
  • Stack overflow from deep recursion. A program that recurses 100,000 levels deep on a tree will crash with stack overflow on Linux's default 8 MB stack (~250 KB/frame for Python). Three fixes, in order of preference: (1) iterate, (2) use an explicit stack, (3) raise the OS limit with sys.setrecursionlimit / ulimit -s unlimited.
  • Bounded stacks must check for overflow. Embedded code often uses a fixed-size array stack: int stk[64]. Pushing the 65th element silently corrupts adjacent memory unless you guard with if (top >= 64) error().
  • Do not iterate a stack non-destructively in client code. Looping while not s.is_empty(): print(s.pop()) drains the stack as a side effect of printing. If you needed it intact, you've lost it. Either expose a debug-only iterator on the implementation, or copy first.
  • Concurrent access. A naive Python or C stack is not thread-safe. Two threads pushing simultaneously can write the same buffer slot. Wrap with a lock, or use a concurrent data structure like Java's ConcurrentLinkedDeque.

Quick Checks

Quick Check

On an empty stack you run: push(1); push(2); pop(); push(3); pop(); peek(); What does the final peek() return, and what is the stack's size afterward?

Quick Check

Which one of these operations VIOLATES the LIFO contract — i.e., is NOT something a pure stack ADT supports?


With the abstraction nailed down, the next step is concrete: how do we actually build this thing? Section 7.2 implements the stack two ways—over a dynamic array and over a singly linked list—and compares the constants, the failure modes, and the cache behavior. Section 7.3 pulls back the curtain on the call stack itself, and from there the remaining sections turn the stack loose on expression evaluation, parentheses checking, monotonic-stack tricks, and the algorithms that hide a stack inside their state.

Loading comments...