awesome-everything RU
↑ Back to the climb

Algorithms from zero

The stack

Crux 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.
◷ 22 min

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

Goal

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 idea

The LIFO rule:

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:

  1. Push: Add an element to the top of the stack. Returns nothing; updates the stack in-place.
  2. Pop: Remove and return the top element of the stack. If the stack is empty, either return null or raise an error (implementation-dependent).
  3. 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.

  1. If you see an opening bracket ((, [, {), push it onto the stack.
  2. 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).
  3. 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.
The code

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:

Output
 
Step-by-step trace

Let us trace checking if "({[]})" is balanced.

1 for (let char of s) {
2 if (char === '(' || char === '[' || char === '{') {
3 stack.push(char);
4 } else if (char === ')' || char === ']' || char === '}') {
5 const top = stack.pop();
6 if (top !== pairs[char]) return false;
7 }
8 }
9 return stack.isEmpty();

Complexity

Stack operations:

OperationTimeSpace
PushO(1)O(1)
PopO(1)O(1)
PeekO(1)O(1)
isEmptyO(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.
Practice 0 / 5

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.

Check yourself
Quiz

Why is a stack the ideal data structure for checking balanced brackets instead of, say, a queue or an array?

Recap

The stack: LIFO and three operations

  1. LIFO rule: The last element pushed is the first one popped. Think of a stack of plates.

  2. 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.

  3. Two implementations: Array (simpler, cache-friendly, rare O(n) resize) or linked list (no resize, extra pointer per node).

  4. 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.

  5. Real-world uses: Undo/redo, expression parsing, function call stacks, depth-first search traversal.

  6. 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.

Next, you will learn queues—the opposite of stacks. A queue enforces FIFO (First In, First Out). You enqueue at the back and dequeue from the front. Queues model waiting lines, breadth-first search, and any situation where fairness (first come, first served) matters.

Continue the climb ↑Queues and deques
shortcuts expand
search
K
prev piece
k
next piece
j
cycle tier
t
this menu
?
sources4
expand
  1. 01
  2. 02
  3. 03
  4. 04

Trademarks belong to their respective owners. Editorial reference only.