Stacks and Queues
Stacks and Queues are linear data structures that follow specific ordering principles for adding and removing elements. They are widely used in algorithms and system design.
Stack
A Stack is a Last-In-First-Out (LIFO) data structure where the last element added is the first one to be removed.
Real-World Analogies:
- Stack of plates (add/remove from top)
- Browser back button (most recent page first)
- Undo operation in text editor
- Function call stack in programming
Stack Operations
| Operation | Description | Time Complexity |
|---|---|---|
| push(item) | Add item to top | O(1) |
| pop() | Remove and return top item | O(1) |
| peek() | View top item without removing | O(1) |
| isEmpty() | Check if stack is empty | O(1) |
| size() | Get number of elements | O(1) |
Implementing Stack in Python
# Using Python list
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 not self.is_empty():
return self.items.pop()
return None
def peek(self):
"""View top item without removing"""
if not self.is_empty():
return self.items[-1]
return None
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)
# Usage
stack = Stack()
stack.push(1)
stack.push(2)
stack.push(3)
print(stack.pop()) # 3
print(stack.peek()) # 2
print(stack.size()) # 2
Simple Stack using List
# Stack using built-in list
stack = []
# Push
stack.append(1)
stack.append(2)
stack.append(3)
# Pop
top = stack.pop() # 3
# Peek
top = stack[-1] if stack else None # 2
# Check empty
is_empty = len(stack) == 0
Stack Applications
1. Balanced Parentheses
def is_balanced(expression):
"""
Check if parentheses are balanced
Time Complexity: O(n)
Space Complexity: O(n)
"""
stack = []
matching = {'(': ')', '{': '}', '[': ']'}
for char in expression:
if char in matching:
stack.append(char)
elif char in matching.values():
if not stack or matching[stack.pop()] != char:
return False
return len(stack) == 0
# Test
print(is_balanced("()")) # True
print(is_balanced("()[]{}")) # True
print(is_balanced("(]")) # False
print(is_balanced("([)]")) # False
print(is_balanced("{[]}")) # True
2. Reverse String
def reverse_string(s):
"""
Reverse a string using stack
Time Complexity: O(n)
Space Complexity: O(n)
"""
stack = []
# Push all characters
for char in s:
stack.append(char)
# Pop all characters
reversed_s = ''
while stack:
reversed_s += stack.pop()
return reversed_s
# Test
print(reverse_string("hello")) # "olleh"
# Python way
print("hello"[::-1]) # "olleh"
3. Evaluate Postfix Expression
def evaluate_postfix(expression):
"""
Evaluate postfix expression (Reverse Polish Notation)
Time Complexity: O(n)
Space Complexity: O(n)
"""
stack = []
operators = {'+', '-', '*', '/'}
for token in expression.split():
if token not in operators:
stack.append(int(token))
else:
b = stack.pop()
a = stack.pop()
if token == '+':
result = a + b
elif token == '-':
result = a - b
elif token == '*':
result = a * b
elif token == '/':
result = a // b
stack.append(result)
return stack.pop()
# Test
print(evaluate_postfix("2 3 +")) # 5
print(evaluate_postfix("2 3 + 4 *")) # 20
print(evaluate_postfix("5 1 2 + 4 * + 3 -")) # 14
Queue
A Queue is a First-In-First-Out (FIFO) data structure where the first element added is the first one to be removed.
Real-World Analogies:
- Line at a ticket counter
- Print job queue
- Task scheduling in operating systems
- Breadth-First Search in graphs
Queue Operations
| Operation | Description | Time Complexity |
|---|---|---|
| enqueue(item) | Add item to rear | O(1) |
| dequeue() | Remove and return front item | O(1) |
| front() | View front item | O(1) |
| isEmpty() | Check if queue is empty | O(1) |
| size() | Get number of elements | O(1) |
Implementing Queue in Python
from collections import deque
class Queue:
def __init__(self):
self.items = deque()
def enqueue(self, item):
"""Add item to rear of queue"""
self.items.append(item)
def dequeue(self):
"""Remove and return front item"""
if not self.is_empty():
return self.items.popleft()
return None
def front(self):
"""View front item without removing"""
if not self.is_empty():
return self.items[0]
return None
def is_empty(self):
"""Check if queue is empty"""
return len(self.items) == 0
def size(self):
"""Return number of items"""
return len(self.items)
# Usage
queue = Queue()
queue.enqueue(1)
queue.enqueue(2)
queue.enqueue(3)
print(queue.dequeue()) # 1
print(queue.front()) # 2
print(queue.size()) # 2
Simple Queue using deque
from collections import deque
# Queue using deque
queue = deque()
# Enqueue
queue.append(1)
queue.append(2)
queue.append(3)
# Dequeue
front = queue.popleft() # 1
# Front
front = queue[0] if queue else None # 2
# Check empty
is_empty = len(queue) == 0
Queue Applications
1. Hot Potato Simulation
def hot_potato(names, num):
"""
Simulate hot potato game
Time Complexity: O(n * num)
Space Complexity: O(n)
"""
from collections import deque
queue = deque(names)
while len(queue) > 1:
# Pass the potato num times
for _ in range(num):
queue.append(queue.popleft())
# Remove the person holding potato
queue.popleft()
return queue[0]
# Test
players = ["Alice", "Bob", "Charlie", "David", "Eve"]
print(hot_potato(players, 3)) # Winner
2. Level Order Traversal (BFS)
def level_order_traversal(root):
"""
Print tree nodes level by level
Time Complexity: O(n)
Space Complexity: O(n)
"""
if not root:
return
from collections import deque
queue = deque([root])
result = []
while queue:
level_size = len(queue)
level = []
for _ in range(level_size):
node = queue.popleft()
level.append(node.value)
if node.left:
queue.append(node.left)
if node.right:
queue.append(node.right)
result.append(level)
return result
Deque (Double-Ended Queue)
A Deque allows insertion and deletion at both ends.
from collections import deque
# Create deque
dq = deque([1, 2, 3])
# Add to right
dq.append(4) # [1, 2, 3, 4]
# Add to left
dq.appendleft(0) # [0, 1, 2, 3, 4]
# Remove from right
dq.pop() # 4
# Remove from left
dq.popleft() # 0
# Result: [1, 2, 3]
Priority Queue
A Priority Queue where elements are served based on priority, not insertion order.
import heapq
class PriorityQueue:
def __init__(self):
self.heap = []
def push(self, item, priority):
"""Add item with priority (lower number = higher priority)"""
heapq.heappush(self.heap, (priority, item))
def pop(self):
"""Remove and return highest priority item"""
if self.heap:
return heapq.heappop(self.heap)[1]
return None
def is_empty(self):
return len(self.heap) == 0
# Usage
pq = PriorityQueue()
pq.push("Task 1", 3)
pq.push("Task 2", 1)
pq.push("Task 3", 2)
print(pq.pop()) # Task 2 (priority 1)
print(pq.pop()) # Task 3 (priority 2)
print(pq.pop()) # Task 1 (priority 3)
Stack vs Queue Comparison
| Feature | Stack | Queue |
|---|---|---|
| Order | LIFO (Last In First Out) | FIFO (First In First Out) |
| Primary Ops | push(), pop() | enqueue(), dequeue() |
| Use Cases | Undo/Redo, Recursion, DFS | Task scheduling, BFS |
| Python | list with append/pop | deque with append/popleft |
Common Interview Problems
Next Greater Element
def next_greater_element(arr):
"""
Find next greater element for each element
Time Complexity: O(n)
Space Complexity: O(n)
"""
stack = []
result = [-1] * len(arr)
for i in range(len(arr)):
while stack and arr[stack[-1]] < arr[i]:
index = stack.pop()
result[index] = arr[i]
stack.append(i)
return result
# Test
nums = [4, 5, 2, 10, 8]
print(next_greater_element(nums)) # [5, 10, 10, -1, -1]
Implement Queue using Stacks
class QueueUsingStacks:
def __init__(self):
self.stack1 = [] # For enqueue
self.stack2 = [] # For dequeue
def enqueue(self, item):
"""O(1)"""
self.stack1.append(item)
def dequeue(self):
"""Amortized O(1)"""
if not self.stack2:
while self.stack1:
self.stack2.append(self.stack1.pop())
return self.stack2.pop() if self.stack2 else None
# Test
q = QueueUsingStacks()
q.enqueue(1)
q.enqueue(2)
q.enqueue(3)
print(q.dequeue()) # 1
print(q.dequeue()) # 2
Practice Problems
Try solving these problems: - Min Stack (get minimum in O(1)) - Valid Parentheses - Implement Stack using Queues - Sliding Window Maximum - Largest Rectangle in Histogram
When to Use
- Stack: When you need LIFO behavior (undo operations, parsing, DFS)
- Queue: When you need FIFO behavior (task scheduling, BFS, buffering)
- Deque: When you need operations at both ends
- Priority Queue: When elements have different priorities
Next Tutorial: Linked Lists