3.2.5 · D4Linear Data Structures

Exercises — Stack — LIFO semantics, push - pop - peek, array and linked list implementations

3,724 words17 min readBack to topic

Reminder of the vocabulary we will reuse (all from the parent note):

  • push(x) — put x on 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.

Figure — Stack — LIFO semantics, push - pop - peek, array and linked list implementations

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):

  1. push(5)[5]
  2. push(8)[5, 8]
  3. pop() → returns 8, stack [5]
  4. push(2)[5, 2]
  5. peek() → returns 2, stack unchanged [5, 2]
  6. pop() → returns 2, stack [5]
  7. 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 = 2 Full means top == capacity - 1 = 3 (the last of the 4 slots, indices 0..3, is filled). We reach at most top = 2, so it is never full. Final top = 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; then top = 1. Returns 9 ✅ (the real top).
  • Wrong order: top = 1 first; then x = arr[1] = 3. Returns 3 ✗ — the element below the top, and the real top 9 is silently leaked (still sitting at index 2, treated as removed). Rule: order = (read, shrink). Read while top still 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.head

What 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 was None — 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 head

The 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:

  1. 5[5]
  2. 1[5, 1]
  3. 2[5, 1, 2]
  4. +: b=2, a=1 → 1+2=3[5, 3]
  5. 4[5, 3, 4]
  6. *: b=4, a=3 → 3*4=12[5, 12]
  7. +: b=12, a=5 → 5+12=17[17]
  8. 3[17, 3]
  9. -: 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 operand a. Getting it backwards would give 3 - 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): push x onto in.
  • dequeue(): if out is empty, pop everything from in into out (this reverses order once), then pop from out.

Reversing a LIFO stack once turns it into FIFO order — that is the whole trick. Trace:

  1. enqueue(1): in=[1], out=[]
  2. enqueue(2): in=[1,2], out=[]
  3. dequeue(): out empty → move all: pop 2 then 1 into out → out=[2,1] (bottom→top), in=[]. Pop out → 1. out=[2]
  4. enqueue(3): in=[3], out=[2]
  5. dequeue(): out not empty → pop out → 2. out=[]
  6. 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 to in once and moved to out at most once, so across operations the total work is proportional to amortized per operation (constant on average, even though a single dequeue that 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).

Figure — Stack — LIFO semantics, push - pop - peek, array and linked list implementations

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.

  1. Push A. Stack [A].
  2. Pop A → visit A. Push its unvisited neighbours B, C → stack [B, C] (C on top).
  3. Pop C → visit C. Neighbours A(visited), D → push D → stack [B, D].
  4. Pop D → visit D. Neighbours B, C — C visited, B not yet visited → push B → stack [B, B].
  5. Pop B → visit B. All neighbours visited → push nothing → stack [B].
  6. 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 for mins.

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.