Data Structure: Stack

An introduction to the Last-In-First-Out (LIFO) stack data structure and its common algorithmic patterns.

Introduction

Basic Operations

A stack typically supports the following operations, all of which run in $O(1)$ time:

Operation Description Time Complexity
push(x) Adds element x to the top of the stack. $O(1)$
pop() Removes and returns the top element. $O(1)$
peek() Returns the top element without removing it. $O(1)$
isEmpty() Checks if the stack is empty. $O(1)$

In Python, a standard list is commonly used as a stack:

stack = []
stack.append(1)  # push
stack.append(2)
top = stack[-1]  # peek
val = stack.pop() # pop

Real-world Examples



Pattern 1: Matching and Nesting

The most common use case for a stack is when you need to “remember” an opening element until its corresponding closing element is found. This is essential for parsing nested structures.

Leetcode 20. Valid Parentheses

Intuition

Code (Python)

class Solution:
    def isValid(self, s: str) -> bool:
        stack = []
        mapping = {")": "(", "}": "{", "]": "["}
        
        for char in s:
            if char in mapping:
                top_element = stack.pop() if stack else '#'
                if mapping[char] != top_element:
                    return False
            else:
                stack.append(char)
        
        return not stack

Complexity


Pattern 2: State Management (Min Stack)

Sometimes we need a stack to support standard operations plus a special utility, like finding the minimum element in the entire stack at any moment in $O(1)$ time.

Leetcode 155. Min Stack

Intuition

Code (Python)

class MinStack:
    def __init__(self):
        self.stack = []
        self.min_stack = []

    def push(self, val: int) -> None:
        self.stack.append(val)
        if not self.min_stack or val <= self.min_stack[-1]:
            self.min_stack.append(val)

    def pop(self) -> None:
        if self.stack.pop() == self.min_stack[-1]:
            self.min_stack.pop()

    def top(self) -> int:
        return self.stack[-1]

    def getMin(self) -> int:
        return self.min_stack[-1]

Advanced Preview: Monotonic Stack

A Monotonic Stack is a specialized stack where elements are always sorted (either increasing or decreasing). It is extremely powerful for finding the “next greater element” or “next smaller element” in an array in $O(N)$ time, which often replaces $O(N^2)$ brute force solutions.

We will cover this pattern in depth in a dedicated chapter.


Summary Cheat Sheet

Situation Data Structure
Need to reverse something Stack
Need to match nested pairs Stack
Need to remember the most recent state Stack
Need to find “Next Greater Element” Monotonic Stack

Chapter Outline

Leetcode 1475: Final Prices With a Special Discount in a Shop

U

Intuition

solution

class Solution:
    def finalPrices(self, prices: List[int]) -> List[int]:
        # Create a copy of prices array to store discounted prices
        result = prices.copy()

        stack = deque()

        for i in range(len(prices)):
            # Process items that can be discounted by current price
            while stack and prices[stack[-1]] >= prices[i]:
                # Apply discount to previous item using current price
                result[stack.pop()] -= prices[i]
            # Add current index to stack
            stack.append(i)

        return result

Reference

Note: If you are unfamiliar with the workings of monotonic stacks, try out these problems to practice:

  1. Next Greater Element I 🔗
  2. Next Greater Element II 🔗
  3. Daily Temperatures 🔗 For a more comprehensive understanding of stacks, check out the Stack Explore Card 🔗. This resource provides an in-depth look at the stack data structure, explaining its key concepts and applications with a variety of problems to solidify understanding of the pattern.

    References

    • [Python Stacks - Python Tutorial for Absolute Beginners Mosh](https://youtu.be/NKmasqr_Xkw?si=mXpU5YSSdQjSynaw)