Foundations
Stack
Last-in, first-out: the structure behind parsing, backtracking, and matching problems.
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/shiftinstead ofpush/popstill 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 returnsundefinedrather than throwing, which can silently corrupt logic (e.g. in parentheses matching) if you don't checkstack.lengthfirst. - 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 !== 0at 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