Exercises — Stack — LIFO semantics, push - pop - peek, array and linked list implementations
Reminder of the vocabulary we will reuse (all from the parent note):
- push(x) — put
xon the top (the only accessible end). - pop() — remove and return the top element.
- peek() — return the top without removing it.
- LIFO — Last-In, First-Out: things leave in the reverse order they arrived.
Level 1 — Recognition
Goal: recognise LIFO behaviour and the meaning of each operation. No coding.
L1.1 — Which structure?
You are told: "the last item added must be the first removed." Is this a stack or a queue? Name the acronym.
Recall Solution L1.1
WHAT we look at: "last in, first out" is literally the definition. Answer: a stack. The acronym is LIFO (Last-In, First-Out). A queue is the opposite — FIFO, first-in first-out.
L1.2 — Read the top
A stack (written bottom→top) is [7, 3, 9]. What does peek() return, and what does pop() return? Does the stack change after each?
Recall Solution L1.2
The top is the rightmost element here, 9.
peek()returns 9 and the stack stays[7, 3, 9](peek never removes).pop()returns 9 and the stack becomes[7, 3](pop removes).
L1.3 — Empty sentinel
In the array implementation we set top = -1 for an empty stack. Why -1 and not 0? And what should pop() do when top == -1?
Recall Solution L1.3
top stores the index of the last filled slot. When one element sits at index 0, we need top = 0. So the empty state must be one below the first real index → top = -1. If we used 0 for empty, we could not tell "empty" from "one element at index 0".
When top == -1, pop()/peek() have nothing to act on → they underflow and must raise an error (see the boundary box above), never return arr[-1].
Level 2 — Application
Goal: mechanically run push/pop/peek and read off results.
The figure below has three rows, one per exercise, each showing the stack after every step as a pile that grows upward (the orange box is the current top; teal boxes are the ones below). Row 1 traces L2.1, row 2 traces L2.2 ("data" → "atad"), and the bottom row plots the top index for L2.3 rising and falling, with a dashed teal "full" line at top = 3.

If you cannot see the image: the full step-by-step state after every operation is written out inside each solution below, so the text alone is enough to follow the traces.
L2.1 — Trace the sequence
Operations on an empty stack: push(5), push(8), pop(), push(2), peek(), pop(), pop().
Give the value returned by each pop/peek, and the final stack.
Recall Solution L2.1
Track bottom→top (this matches the top row of the figure — the rightmost box in each little pile is the orange top):
push(5)→[5]push(8)→[5, 8]pop()→ returns 8, stack[5]push(2)→[5, 2]peek()→ returns 2, stack unchanged[5, 2]pop()→ returns 2, stack[5]pop()→ returns 5, stack[](now empty — a further pop would underflow) Returned values in order: pops = 8, 2, 5; peek = 2. Final stack empty.
L2.2 — Reverse via stack
Push the letters of "data" one by one, then pop until empty. Write the output string.
Recall Solution L2.2
Push d, a, t, a → stack (bottom→top) [d, a, t, a] (this is the tallest pile in the middle row of the figure).
Pop repeatedly: a, t, a, d.
Output = "atad" — the reverse of "data". LIFO output is reversal.
L2.3 — Array index bookkeeping
An ArrayStack has capacity = 4. Starting empty (top = -1), perform push, push, push, pop, push. What is top after each step? Is the stack ever full?
Recall Solution L2.3
push does top += 1; pop does top -= 1 (the bottom row of the figure plots exactly these top values, with the dashed line marking "full"):
- push →
top = 0 - push →
top = 1 - push →
top = 2 - pop →
top = 1 - push →
top = 2Full meanstop == capacity - 1 = 3(the last of the 4 slots, indices 0..3, is filled). We reach at mosttop = 2, so it is never full. Finaltop = 2(three elements stored).
Level 3 — Analysis
Goal: explain WHY, and find/fix bugs.
L3.1 — Why read before decrement?
In array pop, why must we read arr[top] before top -= 1? Show the wrong value we'd get if we decremented first, using stack [7, 3, 9] (top at index 2).
Recall Solution L3.1
Indices: arr = [7, 3, 9], top = 2.
- Correct order:
x = arr[2] = 9; thentop = 1. Returns 9 ✅ (the real top). - Wrong order:
top = 1first; thenx = arr[1] = 3. Returns 3 ✗ — the element below the top, and the real top9is silently leaked (still sitting at index 2, treated as removed). Rule: order = (read, shrink). Read whiletopstill points at the top.
L3.2 — Spot the linked-list bug
A student writes push as:
def push(self, x):
self.head = Node(x)
self.head.next = self.headWhat goes wrong? Trace pushing 1 onto empty, then 2.
Recall Solution L3.2
Bug: after self.head = Node(x), the name self.head already points to the new node. Then self.head.next = self.head makes the node point to itself, and the old top is lost.
Trace:
- push(1) on empty: node1 created,
head = node1,node1.next = node1(self-loop). Old head wasNone— already lost, but node1 loops to itself. - push(2): node2 created,
head = node2,node2.next = node2. node1 is now unreachable — the entire previous stack vanished, and node2 loops to itself. Any traversal (while cur: cur = cur.next) would loop forever. Fix — wire before rewiring the head:
def push(self, x):
self.head = Node(x, self.head) # new node.next = OLD head, then becomes headThe new node must point to the old head before head is reassigned.
L3.3 — Counter vs stack
For the single bracket type "()", a +1/-1 counter works. Give the shortest string over "()[]" where a shared counter (any ( or [ = +1, any ) or ] = −1) wrongly reports "balanced," and explain why a stack catches it.
Recall Solution L3.3
String: "([)]".
Counter view — accumulate a running total: add +1 for any opening bracket (( or [) and −1 for any closing bracket () or ]). Reading ([)] left to right, the running total (prefix sums) after each character is:
- after
(: - after
[: - after
): - after
]:
Sequence of running totals = [1, 2, 1, 0]. It never goes negative and ends at 0, so the counter declares it balanced ✗.
Stack view: push (; push [; now ) arrives, top is [ — mismatch (a ) must close a (), so it is invalid ✅.
Why: a counter remembers only how many brackets are open, not which kind and in what order. Correct nesting requires the most-recent opener to match — exactly LIFO. That is why bracket matching needs a stack, not a counter.
(Context — infix to postfix): Expression Evaluation — infix to postfix uses the same LIFO idea for operators: when a new operator arrives you pop operators of higher-or-equal precedence off a stack first, because the most-recently-pushed operator must be resolved before older ones — just like the most-recent bracket must close first.
Level 4 — Synthesis
Goal: combine the stack with another idea to solve a real task.
L4.1 — Postfix (RPN) evaluation
Evaluate the postfix expression "5 1 2 + 4 * + 3 -" using a value stack. Rule: numbers are pushed; an operator pops the top two values (b = first pop, a = second pop), computes a op b, and pushes the result. Give the final value.
Recall Solution L4.1
Read left→right, stack bottom→top:
5→[5]1→[5, 1]2→[5, 1, 2]+: b=2, a=1 →1+2=3→[5, 3]4→[5, 3, 4]*: b=4, a=3 →3*4=12→[5, 12]+: b=12, a=5 →5+12=17→[17]3→[17, 3]-: b=3, a=17 →17-3=14→[14]Final value = 14. Why the pop-order matters: for non-commutative-and/, the second pop is the left operanda. Getting it backwards would give3 - 17 = -14.
L4.2 — Two stacks make a queue
You may only use two stacks in and out. Design enqueue/dequeue so the pair behaves as a FIFO queue. Then run enqueue(1), enqueue(2), dequeue(), enqueue(3), dequeue(), dequeue() and give the dequeued order.
Recall Solution L4.2
Design:
enqueue(x): pushxontoin.dequeue(): ifoutis empty, pop everything frominintoout(this reverses order once), then pop fromout.
Reversing a LIFO stack once turns it into FIFO order — that is the whole trick. Trace:
enqueue(1): in=[1], out=[]enqueue(2): in=[1,2], out=[]dequeue(): out empty → move all: pop 2 then 1 into out → out=[2,1](bottom→top), in=[]. Pop out → 1. out=[2]enqueue(3): in=[3], out=[2]dequeue(): out not empty → pop out → 2. out=[]dequeue(): out empty → move in: pop 3 → out=[3], in=[]. Pop out → 3. Dequeued order: 1, 2, 3 — perfect FIFO. Cost: each element is pushed toinonce and moved tooutat most once, so across operations the total work is proportional to → amortized per operation (constant on average, even though a singledequeuethat moves everything is momentarily ). This "cheap-most-of-the-time, occasional-expensive-step" pattern is exactly the amortized analysis idea revisited in L5.3.
L4.3 — Iterative DFS with an explicit stack
Run Depth-First Search from node A on the graph below, using an explicit stack instead of recursion. Push neighbours in alphabetical order; when popping, mark-and-visit a node only if it is not already visited. Give the visit order.
The graph (undirected) is drawn below. Text description of the figure: four nodes A, B, C, D. A sits at the top and connects down-left to B and down-right to C. D sits at the bottom and connects up-left to B and up-right to C. So the edges are A–B, A–C, B–D, C–D (a 4-node ring: A–B–D–C–A).

Adjacency list: A: B, C · B: A, D · C: A, D · D: B, C.
Recall Solution L4.3
Push order is alphabetical, so the last pushed (deepest alphabetically) pops first — LIFO drives the "go deep" behaviour.
- Push A. Stack
[A]. - Pop A → visit A. Push its unvisited neighbours B, C → stack
[B, C](C on top). - Pop C → visit C. Neighbours A(visited), D → push D → stack
[B, D]. - Pop D → visit D. Neighbours B, C — C visited, B not yet visited → push B → stack
[B, B]. - Pop B → visit B. All neighbours visited → push nothing → stack
[B]. - Pop B → already visited → skip. Stack
[]. Done. Visit order: A, C, D, B. The stack replaces the recursion call stack — same LIFO, but now we hold the memory, so no risk of a call-stack overflow on deep graphs.
Level 5 — Mastery
Goal: design a data structure and argue its cost, or prove a property.
L5.1 — Min-Stack in O(1)
Design a stack that also supports getMin() returning the current minimum, with all four operations (push, pop, peek, getMin) in constant time . Describe the idea, then trace push(5), push(3), push(7), push(2), pop(), getMin().
Recall Solution L5.1
Idea: keep a second stack mins. On push(x), push min(x, current_min) onto mins (or x if empty). On pop, pop both stacks. Then getMin() = mins.peek(). Because each mins entry is the min of everything at-or-below it, it stays valid after any pop — no recomputation, so it touches only the tops → constant time.
Trace (main / mins, bottom→top):
- push(5): main
[5], mins[5] - push(3): min(3,5)=3 → main
[5,3], mins[5,3] - push(7): min(7,3)=3 → main
[5,3,7], mins[5,3,3] - push(2): min(2,3)=2 → main
[5,3,7,2], mins[5,3,3,2] - pop(): remove top of both → returns 2; main
[5,3,7], mins[5,3,3] - getMin():
mins.peek()= 3 ✅ Each op touches only tops → time, extra space formins.
L5.2 — Prove pop returns reverse-push order
Claim: if you push (in that order) onto an empty stack and then pop times, the pops come out . Prove it by induction on .
Recall Solution L5.2
Base case : push , pop → returns . Reverse of is . ✓ Inductive step: assume for any sequence of length the claim holds. Take length : push . The very last push put on top, so the first pop returns (the definition of push/top). After that pop, the stack is exactly the state after pushing . By the hypothesis, the remaining pops give . Concatenating: = reverse of the input. ✓ By induction the claim holds for all . This is why Example L2.2 reverses a string — reversal is the structural signature of LIFO.
L5.3 — Amortized cost of a dynamic-array stack
An ArrayStack backed by a dynamic array doubles its capacity whenever it fills, copying all existing elements into the new, larger array. A single push that triggers a resize costs (it copies all current elements). Prove that a sequence of pushes starting from an empty stack costs total — i.e. amortized (average) per push, even though individual resizes are expensive. Use the doubling series.
Recall Solution L5.3
Suppose we start at capacity 1 and double each time it fills: capacities hit A resize copies the current contents. Over pushes, the copies performed at successive resizes total This is a geometric series. Using the identity the sum equals . Since , we have , so the total copy work . Add the plain stores (one per push, each ): total work . Divide by the pushes: amortized per push. The occasional expensive resize is "paid for" by the many cheap pushes that happened before it — this is precisely the accounting done in Dynamic Array / Amortized Analysis. Key sum to remember:
Recall One-line self-check before you leave
LIFO in one sentence ::: The last element pushed is the first one popped — output is the reverse of input order.
Why read-then-decrement in array pop ::: Because top still points at the real top while you read; decrement first and you return the element below and leak the top.
Why a stack (not a counter) for [](){} ::: A counter forgets which bracket opened; only a stack remembers order, so ([)] is caught.
What happens on pop/peek of an empty stack ::: It underflows — the operation must raise an error, so guard with isEmpty first.
What happens on push into a full fixed array ::: It overflows — push must raise an error (guard with isFull); a dynamic array instead resizes and never overflows.