Skip to content

Stacks

A stack is a linear data structure that follows the Last-In-First-Out (LIFO) principle. Think of it like a stack of plates - you can only add or remove plates from the top.

Interactive Stack Visualizer

Push and pop elements to see LIFO in action. Watch elements animate as they enter and leave the stack.

Data Structure Visualizer

LIFO - Last In, First Out

Value
Stack is empty — push an element
Size: 0 / 10

What is a Stack?

A stack supports two primary operations:

  • Push: Add an element to the top
  • Pop: Remove and return the top element
┌─────┐
│ 5 │ ← Top (most recently added)
├─────┤
│ 3 │
├─────┤
│ 8 │
├─────┤
│ 1 │ ← Bottom (first added)
└─────┘

Time Complexity

OperationAverageWorst
PushO(1)O(1)
PopO(1)O(1)
Peek/TopO(1)O(1)
SearchO(n)O(n)
isEmptyO(1)O(1)

Basic Operations

Stack Implementation

class Stack:
def __init__(self):
self.items = []
def push(self, item):
"""Add item to top of stack"""
self.items.append(item)
def pop(self):
"""Remove and return top item"""
if self.is_empty():
raise IndexError("Pop from empty stack")
return self.items.pop()
def peek(self):
"""Return top item without removing"""
if self.is_empty():
raise IndexError("Peek from empty stack")
return self.items[-1]
def is_empty(self):
"""Check if stack is empty"""
return len(self.items) == 0
def size(self):
"""Return number of items"""
return len(self.items)
# Using Python's built-in list as stack
stack = []
stack.append(1) # Push
stack.append(2)
top = stack.pop() # Pop: returns 2

Linked List Implementation

For frequent push/pop operations, a linked list implementation avoids array resizing:

class Node:
def __init__(self, value):
self.value = value
self.next = None
class LinkedStack:
def __init__(self):
self.top = None
self._size = 0
def push(self, value):
new_node = Node(value)
new_node.next = self.top
self.top = new_node
self._size += 1
def pop(self):
if self.is_empty():
raise IndexError("Pop from empty stack")
value = self.top.value
self.top = self.top.next
self._size -= 1
return value
def peek(self):
if self.is_empty():
raise IndexError("Peek from empty stack")
return self.top.value
def is_empty(self):
return self.top is None
def size(self):
return self._size

Common Patterns

1. Valid Parentheses

Check if brackets are properly matched and nested. This is a classic stack problem where we push opening brackets and pop when we encounter closing brackets.

Algorithm:

  1. Iterate through each character in the string
  2. If it’s an opening bracket, push it onto the stack
  3. If it’s a closing bracket, check if the stack is empty or if the top doesn’t match
  4. At the end, the stack should be empty for valid parentheses
Input: "({[]})"
Step 1: '(' → push → Stack: ['(']
Step 2: '{' → push → Stack: ['(', '{']
Step 3: '[' → push → Stack: ['(', '{', '[']
Step 4: ']' → pop '[' matches → Stack: ['(', '{']
Step 5: '}' → pop '{' matches → Stack: ['(']
Step 6: ')' → pop '(' matches → Stack: []
Result: Stack is empty → Valid!
def is_valid_parentheses(s: str) -> bool:
"""
Check if string has valid parentheses.
Approach:
- Use a stack to track opening brackets
- When we see a closing bracket, check if it matches the top of stack
- A valid string will have an empty stack at the end
Time Complexity: O(n) - we process each character once
Space Complexity: O(n) - in worst case, all opening brackets
Args:
s: String containing brackets ()[]{}
Returns:
True if brackets are valid, False otherwise
"""
stack = []
# Map closing brackets to their opening counterparts
mapping = {')': '(', '}': '{', ']': '['}
for char in s:
if char in mapping:
# It's a closing bracket
# Check if stack is empty or top doesn't match
if not stack or stack.pop() != mapping[char]:
return False
else:
# It's an opening bracket - push to stack
stack.append(char)
# Valid only if all brackets were matched (stack is empty)
return len(stack) == 0
# Examples with explanations
print(is_valid_parentheses("()[]{}")) # True - each opens and closes
print(is_valid_parentheses("([)]")) # False - wrong nesting order
print(is_valid_parentheses("{[]}")) # True - proper nesting
print(is_valid_parentheses("(")) # False - unclosed bracket

2. Monotonic Stack

A monotonic stack maintains elements in either increasing or decreasing order. It’s powerful for finding the “next greater” or “next smaller” element efficiently.

Key Insight: When a new element arrives that breaks the monotonic property, we pop elements and process them - they’ve found their answer!

Finding Next Greater Element for [4, 2, 1, 5, 3]:
Process 4: Stack empty, push index 0 → Stack: [0]
Process 2: 2 < 4, push index 1 → Stack: [0, 1]
Process 1: 1 < 2, push index 2 → Stack: [0, 1, 2]
Process 5: 5 > nums at indices 2,1,0
- Pop 2: result[2] = 5 (next greater for 1 is 5)
- Pop 1: result[1] = 5 (next greater for 2 is 5)
- Pop 0: result[0] = 5 (next greater for 4 is 5)
- Push 3 → Stack: [3]
Process 3: 3 < 5, push index 4 → Stack: [3, 4]
Remaining in stack have no next greater: result[3] = result[4] = -1
Result: [5, 5, 5, -1, -1]
def next_greater_element(nums: list) -> list:
"""
Find the next greater element for each position in the array.
A monotonic decreasing stack helps us efficiently find when
a larger element appears to the right of each position.
Time Complexity: O(n) - each element pushed and popped at most once
Space Complexity: O(n) - for the stack and result array
Args:
nums: List of integers
Returns:
List where result[i] is the next greater element for nums[i],
or -1 if none exists
"""
n = len(nums)
result = [-1] * n # Default: no greater element found
stack = [] # Store indices (not values) for position tracking
for i in range(n):
# While current element is greater than elements at stack indices
# Those elements have found their "next greater element"
while stack and nums[stack[-1]] < nums[i]:
idx = stack.pop()
result[idx] = nums[i]
# Push current index to stack
stack.append(i)
# Elements remaining in stack have no greater element to their right
return result
# Detailed example
nums = [4, 2, 1, 5, 3]
print(next_greater_element(nums)) # [5, 5, 5, -1, -1]
# Another example
nums = [1, 3, 2, 4]
print(next_greater_element(nums)) # [3, 4, 4, -1]

3. Min Stack

Design a stack that supports push, pop, top, and retrieving the minimum element in O(1) time.

Key Insight: Maintain a secondary stack that tracks the minimum at each level. When we push, we also track what the minimum is at that point.

Operations on MinStack:
push(5): main=[5], min=[5] (min is 5)
push(3): main=[5,3], min=[5,3] (min is 3)
push(7): main=[5,3,7], min=[5,3] (min still 3, don't push)
push(2): main=[5,3,7,2], min=[5,3,2] (new min is 2)
getMin(): return 2
pop(): main=[5,3,7], min=[5,3] (2 was min, pop from both)
getMin(): return 3
class MinStack:
"""
Stack with O(1) minimum retrieval.
We use two stacks:
- Main stack: stores all elements
- Min stack: stores minimum at each level
The min stack only grows when we see a new minimum,
making it space-efficient while maintaining O(1) getMin.
"""
def __init__(self):
self.stack = [] # Main stack for all elements
self.min_stack = [] # Tracks minimum at each "level"
def push(self, val: int) -> None:
"""
Push element onto stack.
Time: O(1)
"""
self.stack.append(val)
# Only push to min_stack if it's a new minimum (or equal)
if not self.min_stack or val <= self.min_stack[-1]:
self.min_stack.append(val)
def pop(self) -> None:
"""
Remove top element.
Time: O(1)
"""
val = self.stack.pop()
# If we're popping the current minimum, update min_stack
if val == self.min_stack[-1]:
self.min_stack.pop()
def top(self) -> int:
"""
Get top element without removing.
Time: O(1)
"""
return self.stack[-1]
def getMin(self) -> int:
"""
Retrieve minimum element in stack.
Time: O(1)
"""
return self.min_stack[-1]
# Usage example
minStack = MinStack()
minStack.push(-2)
minStack.push(0)
minStack.push(-3)
print(minStack.getMin()) # -3
minStack.pop()
print(minStack.top()) # 0
print(minStack.getMin()) # -2

4. Decode String

Decode encoded strings like "3[a2[bc]]" = "abcbcabcbcabcbc". This problem requires handling nested patterns using a stack.

Algorithm:

  1. When we see a digit, build the number (could be multi-digit like “12”)
  2. When we see [, save current state (string and number) to stack
  3. When we see ], pop from stack and repeat current string
  4. When we see a letter, append to current string
Decoding "3[a2[bc]]":
char 3: num = 3
char [: push ("", 3), reset → str="", num=0
char a: str = "a"
char 2: num = 2
char [: push ("a", 2), reset → str="", num=0
char b: str = "b"
char c: str = "bc"
char ]: pop ("a", 2) → str = "a" + "bc"*2 = "abcbc"
char ]: pop ("", 3) → str = "" + "abcbc"*3 = "abcbcabcbcabcbc"
def decode_string(s: str) -> str:
"""
Decode string with nested patterns like "3[a2[bc]]".
We use a stack to handle nested brackets. Each time we see '[',
we save our current progress. When we see ']', we complete
that level and combine with the previous state.
Time Complexity: O(n * max_k) where max_k is the largest repeat count
Space Complexity: O(n) for the stack
Args:
s: Encoded string with pattern k[encoded_string]
Returns:
Decoded string
"""
stack = []
current_num = 0
current_str = ""
for char in s:
if char.isdigit():
# Build the number (handles multi-digit like "12")
current_num = current_num * 10 + int(char)
elif char == '[':
# Save current state and start fresh
stack.append((current_str, current_num))
current_str = ""
current_num = 0
elif char == ']':
# Pop previous state and build result
prev_str, num = stack.pop()
current_str = prev_str + current_str * num
else:
# Regular character - append to current string
current_str += char
return current_str
# Examples with explanations
print(decode_string("3[a]2[bc]")) # "aaabcbc"
print(decode_string("3[a2[c]]")) # "accaccacc"
print(decode_string("2[abc]3[cd]ef")) # "abcabccdcdcdef"
print(decode_string("10[a]")) # "aaaaaaaaaa" (handles multi-digit)

5. Daily Temperatures

Given daily temperatures, find how many days until a warmer day for each position. This is a perfect monotonic stack application.

def daily_temperatures(temperatures: list) -> list:
"""
Find days until warmer temperature for each day.
Uses monotonic decreasing stack - when we find a warmer day,
all colder days in the stack have found their answer.
Time: O(n), Space: O(n)
"""
n = len(temperatures)
result = [0] * n
stack = [] # Store indices of days waiting for warmer temp
for i in range(n):
# Current temp is warmer than temps at indices in stack
while stack and temperatures[stack[-1]] < temperatures[i]:
prev_day = stack.pop()
result[prev_day] = i - prev_day # Days until warmer
stack.append(i)
return result
# Example
temps = [73, 74, 75, 71, 69, 72, 76, 73]
print(daily_temperatures(temps))
# Output: [1, 1, 4, 2, 1, 1, 0, 0]
# Day 0 (73°): wait 1 day for 74°
# Day 2 (75°): wait 4 days for 76°
# Day 6 (76°): no warmer day ahead

Applications

  1. Function Call Stack - Managing function calls and returns
  2. Undo/Redo Operations - Text editors, image editors
  3. Browser History - Back button navigation
  4. Expression Parsing - Compiler design, calculators
  5. Backtracking - DFS, maze solving
  6. Syntax Checking - Matching delimiters in code

Practice Problems

Easy

ProblemPatternCompanies
Valid ParenthesesMatchingAmazon, Google, Meta
Min StackAuxiliary StackAmazon, Microsoft
Implement Queue using StacksTwo StacksAmazon, Apple
Baseball GameSimulationAmazon

Medium

ProblemPatternCompanies
Daily TemperaturesMonotonic StackAmazon, Google
Decode StringNested StackAmazon, Google
Evaluate RPNExpressionAmazon, Microsoft
Asteroid CollisionSimulationAmazon

Hard

ProblemPatternCompanies
Largest Rectangle in HistogramMonotonic StackAmazon, Google, Meta
Trapping Rain WaterTwo Pointers/StackAmazon, Google
Basic CalculatorExpressionAmazon, Meta
Maximal RectangleDP + StackAmazon, Google

Key Takeaways

  1. LIFO principle - Last in, first out
  2. O(1) operations - Push, pop, and peek are constant time
  3. Monotonic stacks - Powerful for “next greater/smaller” problems
  4. Matching problems - Brackets, parentheses, tags
  5. State tracking - Save and restore states (backtracking, parsing)

Next Steps