All guides
05

Foundations

Stack

Last-in, first-out: the structure behind parsing, backtracking, and matching problems.

Time: Push/Pop/Peek O(1) Space: O(n)
Concept Visualization: Stack

How It Works

A stack is a Last-In-First-Out (LIFO) collection: the only element you can add to or remove from is the one at the "top." Think of a stack of plates: you place new plates on top and you take plates off the top; you never grab one from the middle or bottom without first removing everything above it.

Conceptually a stack only needs to support three operations, and a plain array backs all of them efficiently in JavaScript:

  • Push: add an element to the top. O(1).
  • Pop: remove and return the top element. O(1).
  • Peek: look at the top element without removing it. O(1).

All three are O(1) because JavaScript arrays already track their length and support appending/removing from the end in constant time (no shifting required), so push/pop on the end of an array give you a stack for free. (Using unshift/shift at the front would technically also model a stack, but that's O(n) per operation, so always use the end of the array as the "top.")

Stacks are the natural fit whenever a problem has a nested or "most-recent-first" structure: matching parentheses/brackets, undo history, function call stacks, expression evaluation, and depth-first traversal (where an explicit stack replaces recursion).

// A plain array works perfectly as a stack: use push/pop at the end
const stack = [];

stack.push(1); // [1]
stack.push(2); // [1, 2]
stack.push(3); // [1, 2, 3]

console.log(stack[stack.length - 1]); // peek: 3
console.log(stack.pop()); // 3, stack is now [1, 2]

// Classic interview pattern: valid parentheses matching
function isValidParens(s) {
  const stack = [];
  const pairs = { ")": "(", "]": "[", "}": "{" };

  for (const ch of s) {
    if (ch === "(" || ch === "[" || ch === "{") {
      stack.push(ch);
    } else if (ch in pairs) {
      if (stack.pop() !== pairs[ch]) return false;
    }
  }
  return stack.length === 0;
}

isValidParens("{[()]}"); // true
isValidParens("{[(])}"); // false

// Explicit stack for iterative DFS (instead of recursion)
function iterativeDfs(graph, start) {
  const visited = new Set();
  const stack = [start];
  const order = [];

  while (stack.length > 0) {
    const node = stack.pop();
    if (visited.has(node)) continue;
    visited.add(node);
    order.push(node);
    for (const neighbor of graph[node]) {
      if (!visited.has(neighbor)) stack.push(neighbor);
    }
  }
  return order;
}

Common Mistakes

  • Using the front of the array as the "top": calling unshift/shift instead of push/pop still behaves like a stack logically, but each operation becomes O(n) since every other element has to shift. Always operate on the end of the array.
  • Popping without checking for emptiness: calling .pop() on an empty array returns undefined rather than throwing, which can silently corrupt logic (e.g. in parentheses matching) if you don't check stack.length first.
  • Reaching for a stack when order doesn't actually matter: if the problem needs "first-in-first-out" behavior instead, that's a queue, not a stack; mixing these up gives wrong answers, not just slow ones.
  • Forgetting the stack must end up empty for balanced/matching problems: checking only that no mismatch occurred during the scan isn't enough; leftover unmatched opens (stack.length !== 0 at the end) also means invalid input.
  • Overcomplicating with a linked-list-based stack: in JavaScript, a plain array already gives O(1) push/pop; a custom linked structure is rarely necessary and adds overhead.

Pattern Summary

Reach for a stack whenever a problem has nested structure or needs "undo the most recent thing" behavior: matching brackets, evaluating expressions, or converting recursive DFS into an iterative loop. The edge in interviews is recognizing that a plain array's push/pop already gives you a correct, O(1)-per-operation stack with zero extra machinery.

Advertisement