The stack
A stack is a LIFO container where you push elements on top and pop them from the top. Both operations run in O(1) constant time. Stacks model the undo/redo history, expression parsing, and any situation where the last thing added must be the first thing removed.
Last lesson you learned the two-pointer technique on a linked list. Today you will learn about the stack—a simple and powerful data structure. A stack is like a stack of plates: you add plates on top, and you remove plates from the top. The last plate added is the first one to come off. In computing, you use stacks for undo/redo history (each action pushes a new state onto the stack), expression parsing (matching brackets and operators), and anywhere the most recent thing is the most important. A stack enforces one rule: LIFO (last-in-first-out).
After this lesson you can explain the LIFO discipline and the three core stack operations (push, pop, peek), implement a stack using an array or a linked list, explain why all stack operations run in O(1) time, solve bracket-matching and expression-validation problems using a stack, and recognize when a stack is the right data structure for a problem.
The LIFO rule:
Why does this discipline matter? Any time you need to process things in reverse order of arrival — walk back through history, match nested delimiters, or return from nested function calls — LIFO is the natural fit. Understanding it now will make you recognize the pattern instantly in real problems.
A stack is a linear data structure where all insertions and deletions happen at one end, called the top. The rule is LIFO (Last In, First Out): the element added most recently is the one removed first.
Imagine a stack of dinner plates in a cupboard. When you grab a plate, you take from the top (the last one placed there). When you put a plate back, it goes on top. You never grab from the middle or bottom. That is LIFO.
The three operations:
- Push: Add an element to the top of the stack. Returns nothing; updates the stack in-place.
- Pop: Remove and return the top element of the stack. If the stack is empty, either return null or raise an error (implementation-dependent).
- Peek (or top): Return the top element without removing it. Lets you see what is there before you commit to popping.
const stack = []; // Empty stack
// Push: add 10, 20, 30
stack.push(10); // Stack: [10]
stack.push(20); // Stack: [10, 20]
stack.push(30); // Stack: [10, 20, 30]
// Peek: see the top
console.log(stack[stack.length - 1]); // 30 (without removing)
// Pop: remove the top
const top = stack.pop(); // Returns 30. Stack: [10, 20]Array implementation:
An array naturally models a stack. The end of the array is the top. Push appends to the end; pop removes from the end. Both run in O(1) amortized time (in JavaScript, the language hides array resizing).
class Stack {
constructor() {
this.items = [];
}
push(value) {
this.items.push(value);
}
pop() {
if (this.items.length === 0) return null;
return this.items.pop();
}
peek() {
if (this.items.length === 0) return null;
return this.items[this.items.length - 1];
}
isEmpty() {
return this.items.length === 0;
}
}Linked-list implementation:
A stack can also be backed by a linked list. Push and pop both work at the head (no traversal needed). This is also O(1).
class Node {
constructor(value, next = null) {
this.value = value;
this.next = next;
}
}
class StackLinkedList {
constructor() {
this.top = null; // Head pointer
}
push(value) {
const newNode = new Node(value, this.top);
this.top = newNode;
}
pop() {
if (this.top === null) return null;
const value = this.top.value;
this.top = this.top.next;
return value;
}
peek() {
if (this.top === null) return null;
return this.top.value;
}
isEmpty() {
return this.top === null;
}
}A worked application: balanced brackets
One of the most common uses of stacks is to check if brackets, parentheses, and braces are balanced in an expression.
The problem: Given a string with characters (, ), [, ], {, }, check if every opening bracket has a matching closing bracket in the correct order.
Example: "({[]})" is balanced. "({[}])" is not (the } closes the wrong bracket).
The algorithm: Process the string from left to right.
- If you see an opening bracket (
(,[,{), push it onto the stack. - If you see a closing bracket (
),],}), pop the stack and check if it matches. If it does not match, return false. If the stack is empty, return false (no opening bracket to match). - At the end, the stack must be empty. If not, there are unclosed opening brackets.
Example trace: "{[]}"
- See
{: push{. Stack:[{] - See
[: push[. Stack:[{, [] - See
]: pop[. Does[match]? Yes. Stack:[{] - See
}: pop{. Does{match}? Yes. Stack:[] - End: stack is empty. Balanced!
Example trace: "{[}]"
- See
{: push{. Stack:[{] - See
[: push[. Stack:[{, [] - See
}: pop[. Does[match}? No. Return false. Not balanced.
Array-backed stack:
class Stack {
constructor() {
this.items = [];
}
push(value) {
this.items.push(value);
}
pop() {
if (this.items.length === 0) return null;
return this.items.pop();
}
peek() {
if (this.items.length === 0) return null;
return this.items[this.items.length - 1];
}
isEmpty() {
return this.items.length === 0;
}
size() {
return this.items.length;
}
}Checking for balanced brackets:
function isBalanced(s) {
const stack = new Stack();
const pairs = { ')': '(', ']': '[', '}': '{' };
for (let char of s) {
if (char === '(' || char === '[' || char === '{') {
stack.push(char);
} else if (char === ')' || char === ']' || char === '}') {
const top = stack.pop();
if (top !== pairs[char]) {
return false;
}
}
}
return stack.isEmpty();
}Try both in the interactive runner:
Let us trace checking if "({[]})" is balanced.
for (let char of s) { if (char === '(' || char === '[' || char === '{') { stack.push(char); } else if (char === ')' || char === ']' || char === '}') { const top = stack.pop(); if (top !== pairs[char]) return false; } } return stack.isEmpty(); Stack operations:
| Operation | Time | Space |
|---|---|---|
| Push | O(1) | O(1) |
| Pop | O(1) | O(1) |
| Peek | O(1) | O(1) |
| isEmpty | O(1) | O(1) |
All operations run in O(1) constant time because they only touch the top of the stack. No traversal is needed; no loop iterates over the structure.
Array vs. linked-list implementation:
Both have O(1) push and pop. The difference is memory layout:
- Array: Cache-friendly, but resizing (when the array grows) is O(n) and rare.
- Linked list: No resizing, but each node costs extra memory for the pointer.
For most problems, array is simpler. Use linked-list if memory is scarce or if resizing is unacceptable.
Balanced-brackets checker:
- Time: O(n), where n is the length of the string. Each character is processed once.
- Space: O(n) in the worst case (all opening brackets), but typically much less.
After pushing 10, 20, and 30 onto a stack (in that order), what does pop return?
Which operation lets you check what is on top of the stack without removing it?
Is the string '([)]' balanced?
For the balanced-brackets algorithm, what is the time complexity?
Which implementation of a stack is less memory-efficient because each node stores a pointer?
▸Edge cases
Empty stack operations: When the stack is empty and you try to pop or peek, what should happen? Different languages differ: some return null, some raise an exception. In JavaScript, Array.pop() returns undefined. When implementing a stack, you should decide upfront: return null/undefined for an empty pop, or throw an error? For the bracket-matching problem, we return null and check for it before using the value.
▸Common mistake
“A stack is just an array.” No. A stack is an abstract discipline (LIFO with only top access), not a data structure. An array is a data structure (random access, indexing). You can implement a stack using an array, but the stack hides the array’s ability to access the middle or bottom. Do not think of them as the same thing—the stack imposes rules.
▸More practice
Follow-up: undo/redo. A real-world use of stacks is undo/redo in text editors. Each action pushes a state onto the undo stack. When you press Ctrl+Z, pop from the undo stack and restore the previous state, while pushing the current state onto the redo stack. This is a beautiful application of LIFO.
Why is a stack the ideal data structure for checking balanced brackets instead of, say, a queue or an array?
The stack: LIFO and three operations
-
LIFO rule: The last element pushed is the first one popped. Think of a stack of plates.
-
Three operations: push (add to top, O(1)), pop (remove from top, O(1)), peek (see top without removing, O(1)). All are constant time.
-
Two implementations: Array (simpler, cache-friendly, rare O(n) resize) or linked list (no resize, extra pointer per node).
-
Balanced-brackets checker: Process left to right. Push opening brackets, pop and match closing brackets. If stack is empty at the end, all brackets are balanced. O(n) time, O(n) space.
-
Real-world uses: Undo/redo, expression parsing, function call stacks, depth-first search traversal.
-
Key insight: A stack is an abstract discipline, not a data structure. You can implement it with an array or a linked list—both give O(1) operations.
Now when you see brackets, parentheses, or any nesting that must be unwound in reverse order, you will reach for a stack — and you will write the solution in minutes instead of hours.
Practice
Start at the top. Tasks go easiest → hardest: recall a fact, apply it to a case, then a senior-level stretch. Open one, attempt it, then reveal.
Something unclear?
Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.
Apply this
Put this lesson to work on a real build.