DEV Community

jayk0001
jayk0001

Posted on • Edited on

DSA Fundamentals: Stack - From Theory to LeetCode Practice

DSA Fundamentals: Stack - From Theory to LeetCode Practice

The Stack is one of the most fundamental data structures in computer science, following the Last In, First Out (LIFO) principle. Understanding stacks is essential for solving problems involving balanced expressions, undo operations, function call management, and many other algorithmic challenges.

This comprehensive guide combines theoretical understanding with practical problem-solving, featuring solutions to essential LeetCode problems that demonstrate core stack patterns.

Understanding Stack: LIFO Principle

Stack Fundamentals

Stack is a linear data structure that follows the LIFO (Last In, First Out) principle - the last element added is the first one to be removed. Think of it like a stack of trays at a restaurant: you add new trays on top and remove from the top.

Real-World Analogies

  • Stack of plates: Add and remove from the top
  • Browser history: Back button (most recent pages first)
  • Undo functionality: Reverse most recent actions
  • Function call stack: Most recent function call executes first

Stack Operations

Core Operations:

Operation Description Time Complexity Python Implementation
push(x) Add element to top O(1) stack.append(x)
pop() Remove and return top element O(1) stack.pop()
peek() / top() View top element without removing O(1) stack[-1]
isEmpty() Check if stack is empty O(1) not stack or len(stack) == 0
size() Get number of elements O(1) len(stack)

Python Stack Implementation

Using List (Array):

# Initialize empty stack stack = [] # Push operations - O(1) stack.append(1) stack.append(2) stack.append(3) # Stack: [1, 2, 3] (3 is at top)  # Peek operation - O(1) top_element = stack[-1] # Returns 3  # Pop operation - O(1) removed = stack.pop() # Returns 3 # Stack: [1, 2]  # Check if empty - O(1) is_empty = not stack # Returns False is_empty = len(stack) == 0 # Returns False  # Size operation - O(1) size = len(stack) # Returns 2 
Enter fullscreen mode Exit fullscreen mode

Using collections.deque (Recommended for complex scenarios):

from collections import deque # Initialize stack = deque() # Operations (same interface) stack.append(1) # Push top = stack[-1] # Peek removed = stack.pop() # Pop 
Enter fullscreen mode Exit fullscreen mode

Stack vs Array vs Queue

Feature Stack (LIFO) Queue (FIFO) Array
Access Pattern Last In, First Out First In, First Out Random Access
Primary Operations push, pop (from end) enqueue, dequeue (from both ends) Access by index
Access Time O(1) for top O(1) for front/rear O(1) for any index
Use Cases Undo, recursion, parsing Task scheduling, BFS General storage, random access
Python Implementation list with append/pop deque with append/popleft list

Essential LeetCode Problems & Solutions

Let's explore fundamental stack problems that demonstrate core algorithmic patterns.

1. Valid Parentheses (LeetCode 20)

Problem: Determine if a string containing brackets (), {}, [] is valid. Valid means:

  • Open brackets must be closed by the same type
  • Open brackets must be closed in correct order
  • Every close bracket has corresponding open bracket

Examples:

  • "()" → True
  • "()[]{}" → True
  • "(]" → False
  • "([)]" → False (wrong order)

Approach: Use stack to track open brackets. When encountering close bracket, check if it matches most recent open bracket.

class Solution: def isValid(self, s: str) -> bool: # Map closing brackets to their opening counterparts  bracket_map = {"}": "{", "]": "[", ")": "("} stack = [] for char in s: # If it's an opening bracket  if char not in bracket_map: stack.append(char) else: # It's a closing bracket  # Check if stack is empty (no matching open bracket)  if not stack: return False # Pop most recent opening bracket  popped = stack.pop() # Check if it matches the closing bracket  if popped != bracket_map[char]: return False # Valid if all brackets matched (stack empty)  return not stack 
Enter fullscreen mode Exit fullscreen mode

Time Complexity: O(n) - Single pass through string

Space Complexity: O(n) - Stack can hold all opening brackets in worst case

Key Concepts:

  • LIFO nature: Most recent opening bracket should match current closing bracket
  • Hash map for pairing: Quick lookup of bracket pairs
  • Empty stack check: Closing bracket without opening bracket
  • Final stack check: All brackets must be matched (empty stack)

Edge Cases:

  • Empty string: Valid (return True)
  • Only opening brackets: Invalid (stack not empty)
  • Only closing brackets: Invalid (stack empty when checking)
  • Mismatched types: "([)]" - caught by matching check

2. Min Stack (LeetCode 155)

Problem: Design a stack that supports push, pop, top, and retrieving minimum element in constant time.

Operations Required:

  • push(val): Push element onto stack
  • pop(): Remove element from top
  • top(): Get top element
  • getMin(): Retrieve minimum element (O(1))

Approach: Use two stacks - one for data, one for tracking minimum values at each level.

class MinStack: def __init__(self): # Main stack for data  self.data_stack = [] # Stack to track minimum at each level  self.min_stack = [] def push(self, val: int) -> None: # Always push to data stack  self.data_stack.append(val) # Push to min stack  if not self.min_stack: # First element is the minimum  self.min_stack.append(val) else: # Current minimum is min of new value and previous minimum  current_min = self.min_stack[-1] self.min_stack.append(min(current_min, val)) def pop(self) -> None: # Pop from both stacks to maintain sync  self.data_stack.pop() self.min_stack.pop() def top(self) -> int: # Return top of data stack  return self.data_stack[-1] def getMin(self) -> int: # Return top of min stack (current minimum)  return self.min_stack[-1] # Usage example: # obj = MinStack() # obj.push(-2) # data: [-2], min: [-2] # obj.push(0) # data: [-2, 0], min: [-2, -2] # obj.push(-3) # data: [-2, 0, -3], min: [-2, -2, -3] # obj.getMin() # Returns -3 # obj.pop() # data: [-2, 0], min: [-2, -2] # obj.top() # Returns 0 # obj.getMin() # Returns -2 
Enter fullscreen mode Exit fullscreen mode

Time Complexity: O(1) for all operations

Space Complexity: O(n) - Two stacks, each storing n elements

Key Concepts:

  • Auxiliary stack pattern: Track additional information alongside main data
  • Sync maintenance: Both stacks must stay synchronized
  • Min tracking: Store minimum value at each stack level
  • Trade-off: Space (extra stack) for time (O(1) min retrieval)

Why Two Stacks?

  • Without min_stack: Finding minimum requires O(n) scan through data_stack
  • With min_stack: Minimum always available at top in O(1)
  • Space cost: O(n) extra space for guaranteed O(1) retrieval

Alternative Approach (Space Optimization):

# Store (value, current_min) tuples in single stack class MinStack: def __init__(self): self.stack = [] # Store (value, min_at_this_level)  def push(self, val: int) -> None: if not self.stack: self.stack.append((val, val)) else: current_min = self.stack[-1][1] self.stack.append((val, min(val, current_min))) def pop(self) -> None: self.stack.pop() def top(self) -> int: return self.stack[-1][0] def getMin(self) -> int: return self.stack[-1][1] 
Enter fullscreen mode Exit fullscreen mode

3. Daily Temperatures (LeetCode 739)

Problem: Given array of daily temperatures, return array where answer[i] is the number of days until a warmer temperature. If no warmer day, use 0.

Example:

Input: temperatures = [73, 74, 75, 71, 69, 72, 76, 73] Output: [ 1, 1, 4, 2, 1, 1, 0, 0] Explanation: - Day 0 (73°): Next warmer is day 1 (74°) → 1 day wait - Day 1 (74°): Next warmer is day 2 (75°) → 1 day wait - Day 2 (75°): Next warmer is day 6 (76°) → 4 days wait - Day 6 (76°): No warmer day → 0 
Enter fullscreen mode Exit fullscreen mode

Approach: Use monotonic decreasing stack to track indices of temperatures waiting for warmer day.

class Solution: def dailyTemperatures(self, temperatures: List[int]) -> List[int]: # Initialize result array with zeros  result = [0] * len(temperatures) # Stack stores indices of temperatures waiting for warmer day  stack = [] for i, temp in enumerate(temperatures): # While current temp is warmer than temperatures in stack  while stack and temperatures[stack[-1]] < temp: # Pop previous index  prev_index = stack.pop() # Calculate days to wait  result[prev_index] = i - prev_index # Add current index to stack  stack.append(i) return result 
Enter fullscreen mode Exit fullscreen mode

Time Complexity: O(n) - Each element pushed and popped at most once

Space Complexity: O(n) - Stack can hold all indices in worst case

Key Concepts:

  • Monotonic stack: Maintains decreasing temperature order
  • Index storage: Store indices, not values, to calculate distance
  • Next greater element pattern: Finding next element that satisfies condition
  • Efficient lookup: O(1) amortized for finding next warmer day

Visualization:

temperatures = [73, 74, 75, 71, 69, 72, 76, 73] i=0, temp=73: stack=[] → push 0 → stack=[0] i=1, temp=74: 74>73 → pop 0, result[0]=1 → push 1 → stack=[1] i=2, temp=75: 75>74 → pop 1, result[1]=1 → push 2 → stack=[2] i=3, temp=71: 71<75 → push 3 → stack=[2,3] i=4, temp=69: 69<71 → push 4 → stack=[2,3,4] i=5, temp=72: 72>69 → pop 4, result[4]=1 72>71 → pop 3, result[3]=2 72<75 → push 5 → stack=[2,5] i=6, temp=76: 76>72 → pop 5, result[5]=1 76>75 → pop 2, result[2]=4 push 6 → stack=[6] i=7, temp=73: 73<76 → push 7 → stack=[6,7] Remaining in stack get 0 (no warmer day found) 
Enter fullscreen mode Exit fullscreen mode

Why Monotonic Stack?

  • Maintains order: Stack always has decreasing temperatures
  • Efficient processing: When warmer temp found, resolve all colder temps in stack
  • Single pass: Each element processed exactly once

4. Evaluate Reverse Polish Notation (LeetCode 150)

Problem: Evaluate arithmetic expression in Reverse Polish Notation (RPN). Valid operators: +, -, *, /.

Reverse Polish Notation:

  • Operators come after operands
  • No parentheses needed
  • Left-to-right evaluation

Examples:

["2", "1", "+", "3", "*"] → ((2 + 1) * 3) = 9 ["4", "13", "5", "/", "+"] → (4 + (13 / 5)) = 6 ["10", "6", "9", "3", "+", "-11", "*", "/", "*", "17", "+", "5", "+"] → 22 
Enter fullscreen mode Exit fullscreen mode

Approach: Use stack to store operands. When encountering operator, pop two operands, apply operation, push result.

class Solution: def evalRPN(self, tokens: List[str]) -> int: stack = [] operators = {"+", "-", "*", "/"} for token in tokens: if token not in operators: # It's a number, push to stack  stack.append(int(token)) else: # It's an operator, pop two operands  # Note: LIFO order matters!  second = stack.pop() # Right operand  first = stack.pop() # Left operand  # Perform operation  if token == "+": result = first + second elif token == "-": result = first - second elif token == "*": result = first * second elif token == "/": # Integer division (truncate toward zero)  result = int(first / second) # Push result back to stack  stack.append(result) # Final result is the only element in stack  return stack[-1] 
Enter fullscreen mode Exit fullscreen mode

Time Complexity: O(n) - Single pass through tokens

Space Complexity: O(n) - Stack holds operands

Key Concepts:

  • RPN evaluation: Natural fit for stack data structure
  • Operand order: LIFO means second pop is first operand
  • Intermediate results: Push back to stack for future operations
  • Division truncation: Python's int() truncates toward zero

Important: Operand Order Matters!

# For subtraction: 5 - 3 stack = [5, 3] second = stack.pop() # 3 first = stack.pop() # 5 result = first - second # 5 - 3 = 2 ✓  # Wrong order would give: 3 - 5 = -2 ✗ 
Enter fullscreen mode Exit fullscreen mode

Division Edge Case:

# Python division behavior # Standard division: -3 / 2 = -1.5 # int() truncation: int(-1.5) = -1 (toward zero) # Floor division: -3 // 2 = -2 (toward -infinity)  # For RPN, use int(a / b) for truncation toward zero 
Enter fullscreen mode Exit fullscreen mode

Step-by-step Example:

tokens = ["2", "1", "+", "3", "*"] Step 1: "2" → stack = [2] Step 2: "1" → stack = [2, 1] Step 3: "+" → pop 1, pop 2 → 2+1=3 → stack = [3] Step 4: "3" → stack = [3, 3] Step 5: "*" → pop 3, pop 3 → 3*3=9 → stack = [9] Result: 9 
Enter fullscreen mode Exit fullscreen mode

Core Stack Patterns

1. Balanced Expression Validation Pattern

When to use:

  • Matching pairs (brackets, tags, quotes)
  • Nested structures
  • Balanced sequences

Examples: Valid Parentheses, Valid HTML Tags, Remove Invalid Parentheses

Template:

def validate_balanced(expression): stack = [] pairs = {')': '(', '}': '{', ']': '['} # closing -> opening  for char in expression: if char not in pairs: # Opening bracket  stack.append(char) else: # Closing bracket  if not stack or stack.pop() != pairs[char]: return False return not stack # All matched if empty 
Enter fullscreen mode Exit fullscreen mode

Key characteristics:

  • Use hash map for pair matching
  • Stack stores opening elements
  • Check stack emptiness at end

2. Monotonic Stack Pattern

When to use:

  • Next greater/smaller element problems
  • Finding spans or ranges
  • Temperature, stock price problems

Examples: Daily Temperatures, Next Greater Element, Stock Span Problem

Monotonic Increasing Stack Template:

def next_greater_elements(arr): result = [-1] * len(arr) stack = [] # Stores indices  for i in range(len(arr)): # Maintain increasing order  while stack and arr[stack[-1]] < arr[i]: prev_index = stack.pop() result[prev_index] = arr[i] stack.append(i) return result 
Enter fullscreen mode Exit fullscreen mode

Monotonic Decreasing Stack Template:

def next_smaller_elements(arr): result = [-1] * len(arr) stack = [] # Stores indices  for i in range(len(arr)): # Maintain decreasing order  while stack and arr[stack[-1]] > arr[i]: prev_index = stack.pop() result[prev_index] = arr[i] stack.append(i) return result 
Enter fullscreen mode Exit fullscreen mode

Key characteristics:

  • Store indices, not values (to calculate distances)
  • Maintain increasing or decreasing order
  • O(n) time - each element pushed/popped once

3. Expression Evaluation Pattern

When to use:

  • Calculator problems
  • Postfix/prefix notation
  • Expression parsing

Examples: Evaluate RPN, Basic Calculator, Expression Parsing

Template:

def evaluate_postfix(tokens): stack = [] operators = {'+', '-', '*', '/'} for token in tokens: if token not in operators: stack.append(int(token)) else: b = stack.pop() # Right operand  a = stack.pop() # Left operand  result = apply_operation(a, b, token) stack.append(result) return stack[-1] 
Enter fullscreen mode Exit fullscreen mode

Key characteristics:

  • Stack stores operands/intermediate results
  • Operators trigger pop and computation
  • Order matters for non-commutative operations

4. Auxiliary Stack Pattern (Min/Max Tracking)

When to use:

  • Need to track additional property (min, max, etc.)
  • O(1) retrieval of special element required
  • Stack modifications affect tracked property

Examples: Min Stack, Max Stack, Stack with getMin()

Template:

class SpecialStack: def __init__(self): self.data_stack = [] self.special_stack = [] # Tracks min/max/etc  def push(self, val): self.data_stack.append(val) if not self.special_stack: self.special_stack.append(val) else: # Update special property  current_special = self.special_stack[-1] new_special = combine(current_special, val) self.special_stack.append(new_special) def pop(self): self.data_stack.pop() self.special_stack.pop() def get_special(self): return self.special_stack[-1] 
Enter fullscreen mode Exit fullscreen mode

Key characteristics:

  • Two synchronized stacks
  • Trade space for O(1) retrieval
  • Both stacks must stay in sync

Performance Optimization Tips

1. Choose Right Container

# For simple stack operations: Use list stack = [] stack.append(1) # O(1) stack.pop() # O(1)  # For deque operations (both ends): Use collections.deque from collections import deque stack = deque() stack.append(1) # O(1) stack.pop() # O(1) stack.appendleft(1) # O(1) - bonus operation 
Enter fullscreen mode Exit fullscreen mode

Key insight: Python list is optimized for stack operations. Use deque only if you need operations on both ends.

2. Store Indices vs Values in Monotonic Stack

# Store indices (more flexible) def next_greater_with_distance(arr): result = [-1] * len(arr) stack = [] # Store indices  for i in range(len(arr)): while stack and arr[stack[-1]] < arr[i]: prev_idx = stack.pop() result[prev_idx] = i - prev_idx # Can calculate distance  stack.append(i) return result # Store values (only if distance not needed) def next_greater_simple(arr): stack = [] # Store values  result = [] for num in reversed(arr): while stack and stack[-1] <= num: stack.pop() result.append(stack[-1] if stack else -1) stack.append(num) return result[::-1] 
Enter fullscreen mode Exit fullscreen mode

Key insight: Store indices when you need to calculate distances or positions.

3. Avoid Unnecessary Operations

# Inefficient: Check same condition multiple times for token in tokens: if token == "+": result = a + b elif token == "-": result = a - b elif token == "*": result = a * b elif token == "/": result = a / b stack.append(result) # Efficient: Use dictionary for operator dispatch operators = { '+': lambda a, b: a + b, '-': lambda a, b: a - b, '*': lambda a, b: a * b, '/': lambda a, b: int(a / b) } for token in tokens: if token in operators: b, a = stack.pop(), stack.pop() stack.append(operators[token](a, b)) else: stack.append(int(token)) 
Enter fullscreen mode Exit fullscreen mode

4. Early Termination in Validation

# Optimized: Return False immediately def is_valid(s): stack = [] for char in s: if char == '(': stack.append(char) else: if not stack: # Early termination  return False stack.pop() return not stack # Also check impossible cases early def is_valid(s): if len(s) % 2 != 0: # Odd length can't be balanced  return False # ... rest of logic 
Enter fullscreen mode Exit fullscreen mode

Common Pitfalls and Solutions

1. Forgetting to Check Empty Stack

# Wrong: Potential error on empty stack def process_stack(stack): top = stack.pop() # Error if stack empty!  # Correct: Always check before pop def process_stack(stack): if not stack: return None top = stack.pop() return top # Or use try-except def process_stack(stack): try: return stack.pop() except IndexError: return None 
Enter fullscreen mode Exit fullscreen mode

2. Wrong Operand Order in RPN

# Wrong: Reversed operands second = stack.pop() first = stack.pop() result = second - first # Wrong! Should be first - second  # Correct: Remember LIFO order second = stack.pop() # Popped second, was pushed second first = stack.pop() # Popped first, was pushed first result = first - second # Correct order for subtraction 
Enter fullscreen mode Exit fullscreen mode

3. Not Handling Final Stack State

# Wrong: Assume stack has exactly one element def evaluate(tokens): stack = [] # ... process tokens ...  return stack[-1] # What if stack is empty or has multiple elements?  # Correct: Validate final state def evaluate(tokens): stack = [] # ... process tokens ...  if len(stack) != 1: raise ValueError("Invalid expression") return stack[0] 
Enter fullscreen mode Exit fullscreen mode

4. Confusing Peek vs Pop

# Wrong: Pop when you just want to look def check_top(stack): top = stack.pop() # Modifies stack!  return top == target # Correct: Peek without modifying def check_top(stack): if not stack: return False return stack[-1] == target # Just look, don't pop 
Enter fullscreen mode Exit fullscreen mode

5. Integer Division in Python

# Wrong: Floor division for RPN result = first // second # -3 // 2 = -2 (wrong for RPN)  # Correct: Truncate toward zero result = int(first / second) # int(-3 / 2) = int(-1.5) = -1  # Explanation: # Floor division (//): Always rounds toward -infinity # int() truncation: Rounds toward zero # RPN expects truncation toward zero 
Enter fullscreen mode Exit fullscreen mode

Stack Time Complexity Summary

Problem Approach Time Complexity Space Complexity
Valid Parentheses Stack matching O(n) O(n)
Min Stack Auxiliary stack O(1) per operation O(n)
Daily Temperatures Monotonic stack O(n) O(n)
Evaluate RPN Stack evaluation O(n) O(n)
Next Greater Element Monotonic stack O(n) O(n)
Basic Calculator Stack with operators O(n) O(n)

Key Insights:

  • Push/Pop: Always O(1)
  • Full processing: O(n) - each element touched constant times
  • Monotonic stack: O(n) - each element pushed/popped at most once
  • Space: Usually O(n) worst case for stack storage

Queue Quick Overview

Queue follows FIFO (First In, First Out) - like a line at a store. The first person to arrive is the first to be served.

Queue Operations

Operation Description Time Complexity Python Implementation
enqueue(x) Add element to rear O(1) queue.append(x)
dequeue() Remove element from front O(1) queue.popleft()
front() View front element O(1) queue[0]
isEmpty() Check if empty O(1) not queue

Python Queue Implementation

from collections import deque # Initialize queue queue = deque() # Enqueue (add to rear) queue.append(1) queue.append(2) queue.append(3) # Queue: [1, 2, 3] (1 is front, 3 is rear)  # Dequeue (remove from front) front = queue.popleft() # Returns 1 # Queue: [2, 3]  # Peek front front_element = queue[0] # Returns 2  # Check empty is_empty = len(queue) == 0 # Returns False 
Enter fullscreen mode Exit fullscreen mode

Note: We'll explore Queue in depth when covering Tree traversal (BFS) and other graph algorithms where Queue is the primary data structure.

Conclusion

Stack is a fundamental data structure with elegant solutions to many algorithmic problems. Key takeaways:

Core Concepts Mastered:

Stack Fundamentals:

  • LIFO principle: Last In, First Out access pattern
  • O(1) operations: Push, pop, peek all constant time
  • Python implementation: List with append/pop
  • Use cases: Parsing, expression evaluation, backtracking

Essential Patterns Mastered:

Pattern 1: Balanced expression validation (Valid Parentheses)

Pattern 2: Monotonic stack for next greater/smaller (Daily Temperatures)

Pattern 3: Expression evaluation (Evaluate RPN)

Pattern 4: Auxiliary stack for tracking properties (Min Stack)

Problem-Solving Strategies:

  • Matching pairs: Use stack with hash map for pair validation
  • Next greater/smaller: Use monotonic stack pattern
  • Expression evaluation: Natural fit for stack (RPN, calculators)
  • Track min/max: Auxiliary stack maintains property in O(1)
  • Nested structures: Stack handles arbitrary nesting depth

Key Optimization Principles:

  1. Store indices in monotonic stacks for distance calculations
  2. Check empty before pop to avoid errors
  3. Remember operand order in non-commutative operations
  4. Synchronize auxiliary stacks for property tracking
  5. Use early termination in validation problems

When to Use Stack:

  • Need to reverse order or process in LIFO manner
  • Matching/balancing problems (parentheses, tags)
  • Next greater/smaller element problems
  • Expression parsing and evaluation
  • Backtracking and undo operations
  • Function call simulation

Next Steps:

Building on stack foundations, future topics will cover:

  • Queue and BFS (Tree level-order traversal)
  • Binary Trees and Traversals (using stack for DFS)
  • Graph Algorithms (DFS with stack, BFS with queue)
  • Backtracking (stack-based state management)

The problems covered here represent fundamental stack patterns that appear across technical interviews and competitive programming. Master these patterns, and you'll recognize stack opportunities in complex problems.


Practice Problems for Mastery:

Stack Fundamentals:

  • 20. Valid Parentheses
  • 155. Min Stack
  • 232. Implement Queue using Stacks
  • 225. Implement Stack using Queues
  • 1047. Remove All Adjacent Duplicates in String

Monotonic Stack:

  • 739. Daily Temperatures
  • 496. Next Greater Element I
  • 503. Next Greater Element II
  • 84. Largest Rectangle in Histogram
  • 42. Trapping Rain Water

Expression Evaluation:

  • 150. Evaluate Reverse Polish Notation
  • 224. Basic Calculator
  • 227. Basic Calculator II
  • 772. Basic Calculator III

Advanced Stack:

  • 394. Decode String
  • 716. Max Stack
  • 895. Maximum Frequency Stack
  • 946. Validate Stack Sequences

References:

This comprehensive guide provides a solid foundation for mastering stack data structures and recognizing stack patterns in algorithmic problem-solving.

Top comments (0)