Module F-6·12 min read

LIFO structure, the call stack, and balanced-bracket and expression-parsing patterns.

Written by Jatin Jain Saraf · Architect of SupraScan

Module F-6 — Stacks: LIFO, the Call Stack, and Expression Parsing

What this module covers: A stack is one of the simplest structures in this course — and it's already running underneath every function call you've ever written, whether you knew it or not. This module covers the stack as an abstract data type, how it's built on top of the array from Module F-2, the call stack that governs recursion and stack overflows, and the balanced-bracket and expression-parsing patterns a stack solves cleanly.


The Stack: Last-In, First-Out

A stack supports exactly three operations, all O(1): push (add to the top), pop (remove from the top), and peek (look at the top without removing it). You can only ever interact with the most recently added item — everything below it is inaccessible until the items above it are popped off.

class Stack<T> {
  private items: T[] = [];
 
  push(item: T): void {
    this.items.push(item); // O(1) — Module F-2: appending at the end never shifts anything
  }
 
  pop(): T | undefined {
    return this.items.pop(); // O(1) — removing from the end never shifts anything
  }
 
  peek(): T | undefined {
    return this.items[this.items.length - 1]; // O(1) — direct index access
  }
 
  isEmpty(): boolean {
    return this.items.length === 0;
  }
}

Notice this is just a thin wrapper around the array from Module F-2 — a stack isn't a new memory layout, it's a discipline about which operations you allow. Restricting yourself to push/pop/peek at one end is precisely what keeps every operation O(1): those are exactly the array operations that never require shifting other elements.

Analogy: A stack is a pile of plates. You put a new plate on top (push), and you take the top plate off (pop). You cannot pull a plate from the middle without first removing everything stacked above it — the same constraint the data structure enforces.


The Call Stack: Where Your Functions Actually Live

Every time you call a function, the runtime pushes a stack frame — the function's local variables, its arguments, and the address to return to — onto the call stack. When the function returns, its frame is popped, and execution resumes exactly where the caller left off.

function three() { console.log('three'); }      // pushed last, popped first
function two() { three(); }                      // pushed second
function one() { two(); }                        // pushed first
one();
// Call stack grows: one() -> two() -> three()
// Call stack unwinds: three() returns -> two() returns -> one() returns

This is a real stack, following the exact same LIFO rule as the Stack class above — the last function called is the first one to return, because it's sitting on top of everyone waiting on it. This is also why deep, unbounded recursion causes a stack overflow: each recursive call pushes another frame, and if the recursion never reaches its base case, frames pile up until the call stack — a fixed-size region of memory — runs out of room.

function infiniteRecursion(n: number): number {
  return infiniteRecursion(n + 1); // no base case — every call pushes another frame, forever
}
 
infiniteRecursion(0);
// RangeError: Maximum call stack size exceeded

Module P-1 (Recursion and the Call Stack) goes deep on this — how the call stack grows and shrinks with correct, base-cased recursion, and how to reason about how many frames a given recursive function will actually push.


Balanced Parentheses

The single most common interview and real-world use of a stack: checking whether brackets in an expression are properly nested and closed.

function isBalanced(expression: string): boolean {
  const stack: string[] = [];
  const pairs: Record<string, string> = { ')': '(', ']': '[', '}': '{' };
 
  for (const char of expression) {
    if (char === '(' || char === '[' || char === '{') {
      stack.push(char); // an opening bracket — remember it, we'll need to match it later
    } else if (char === ')' || char === ']' || char === '}') {
      if (stack.pop() !== pairs[char]) return false; // wrong bracket type, or nothing left to match
    }
  }
 
  return stack.length === 0; // every opening bracket must have been matched and popped
}
 
isBalanced('({[]})'); // true
isBalanced('({[}])'); // false — `}` arrives while `[` is on top of the stack, not `{`
isBalanced('(()');    // false — one unmatched opening bracket remains

Sign in to keep reading

The rest of this module is free — sign in with Google to unlock it and track your progress.

Sign in & Register

Discussion

0

Join the discussion

Loading comments...