Chapter 8 Stack and Queue
This chapter focuses on stack- and queue-driven techniques. Many of these problems can be solved using explicit stacks, monotonic stacks, or queue-based sweeps to efficiently process streaming data.
8.0.1 Stack Fundamentals
A stack is a Last-In-First-Out (LIFO) data structure that supports two primary operations: - Push: Add element to top - \(O(1)\) - Pop: Remove element from top - \(O(1)\) - Peek: View top element without removing - \(O(1)\)
Java Implementation:
Stack<Integer> stack = new Stack<>();
stack.push(1);
int top = stack.peek(); // Returns 1
int removed = stack.pop(); // Returns and removes 1
boolean empty = stack.isEmpty();When to Use Stacks:
Problems involving nested structures (parentheses, brackets)
Backtracking and undo operations
Depth-first exploration patterns
Processing data with most recent first priority
8.0.2 Monotonic Stack Pattern
A monotonic stack maintains elements in either strictly increasing or decreasing order. This pattern is extremely powerful for finding next greater/smaller element problems.
8.0.2.1 Monotonic Decreasing Stack (Find Next Greater)
A stack where elements are in descending order from bottom to top helps find the next greater element to the right.
Core Concept:
- When a new element is larger than stack top, we’ve found the “next greater” for all smaller elements
- Pop smaller elements and record their next greater
- Push current element
Template:
public int[] nextGreaterElements(int[] nums) {
int n = nums.length;
int[] result = new int[n];
Arrays.fill(result, -1);
Stack<Integer> stack = new Stack<>(); // Stores indices
for (int i = 0; i < n; i++) {
// Pop smaller elements - current is their next greater
while (!stack.isEmpty() && nums[stack.peek()] < nums[i]) {
int idx = stack.pop();
result[idx] = nums[i];
}
stack.push(i);
}
return result;
}Example: nums = [2, 1, 2, 4, 3]
i=0: stack=[0] // Push 2
i=1: stack=[0,1] // Push 1 (smaller than 2)
i=2: Pop 1, result[1]=2 // 2 is next greater for 1
stack=[0,2] // Push 2
i=3: Pop 2, result[2]=4 // 4 is next greater for 2
Pop 0, result[0]=4 // 4 is next greater for original 2
stack=[3] // Push 4
i=4: stack=[3,4] // Push 3
Result: [4, 2, 4, -1, -1]
8.0.2.2 Monotonic Increasing Stack (Find Next Smaller)
A stack where elements are in ascending order helps find the next smaller element.
Template:
public int[] nextSmallerElements(int[] nums) {
int n = nums.length;
int[] result = new int[n];
Arrays.fill(result, -1);
Stack<Integer> stack = new Stack<>();
for (int i = 0; i < n; i++) {
// Pop greater elements - current is their next smaller
while (!stack.isEmpty() && nums[stack.peek()] > nums[i]) {
int idx = stack.pop();
result[idx] = nums[i];
}
stack.push(i);
}
return result;
}8.0.2.3 Monotonic Stack Variations
| Variant | Stack Order | What to Find | When to Pop |
|---|---|---|---|
| Decreasing | Top \(\to\) Bottom: Decreasing | Next Greater Element | nums[i] > stack.top() |
| Increasing | Top \(\to\) Bottom: Increasing | Next Smaller Element | nums[i] < stack.top() |
| Previous Greater | Process right-to-left | Previous Greater | Same as next greater, reversed |
| Previous Smaller | Process right-to-left | Previous Smaller | Same as next smaller, reversed |
Applications:
- Next Greater Element problems
- Daily Temperatures - Classic monotonic decreasing stack
- Largest Rectangle in Histogram (next smaller on both sides)
- Trapping Rain Water (height boundaries)
- Stock Span Problem (consecutive smaller days)
- Car Fleet - Monotonic stack for time calculations
Time Complexity: \(O(n)\) - Each element pushed and popped once Space Complexity: \(O(n)\) - Stack storage
8.0.3 Parentheses and Bracket Matching
Stacks are the canonical solution for validating nested structures like parentheses, brackets, and tags.
Pattern:
- Push opening symbols onto stack
- When closing symbol appears, check if it matches stack top
- Stack should be empty at the end for valid expression
Template for Balanced Parentheses:
public boolean isValid(String s) {
Stack<Character> stack = new Stack<>();
for (char c : s.toCharArray()) {
// Opening brackets - push
if (c == '(' || c == '{' || c == '[') {
stack.push(c);
}
// Closing brackets - check match
else {
if (stack.isEmpty()) return false;
char top = stack.pop();
if (c == ')' && top != '(') return false;
if (c == '}' && top != '{') return false;
if (c == ']' && top != '[') return false;
}
}
return stack.isEmpty();
}Advanced Pattern - Longest Valid Parentheses:
For finding the longest valid substring, use stack to track indices instead of characters.
Approach:
- Push index when encountering
( - Pop when encountering
) - If stack becomes empty, push current index as new base
- Otherwise, calculate length:
current index - stack.peek()
Related Problems:
- Longest Valid Parentheses - Find longest valid substring
- Valid Parentheses (LeetCode 20) - Basic validation
- Maximum Score From Removing Substrings - Greedy with stack
Time Complexity: \(O(n)\) Space Complexity: \(O(n)\)
8.0.4 Stack-Based State Tracking
Stacks excel at maintaining contextual state during sequential processing.
Pattern: Min/Max Stack
Track minimum or maximum alongside normal stack operations in \(O(1)\) time.
Approach 1: Parallel Min Stack
class MinStack {
Stack<Integer> stack;
Stack<Integer> minStack;
public MinStack() {
stack = new Stack<>();
minStack = new Stack<>();
}
public void push(int val) {
stack.push(val);
// Push current min
int currentMin = minStack.isEmpty() ? val : Math.min(val, minStack.peek());
minStack.push(currentMin);
}
public void pop() {
stack.pop();
minStack.pop();
}
public int top() {
return stack.peek();
}
public int getMin() {
return minStack.peek();
}
}Approach 2: Single Stack with Pairs
class MinStack {
Stack<int[]> stack; // [value, currentMin]
public void push(int val) {
int currentMin = stack.isEmpty() ? val : Math.min(val, stack.peek()[1]);
stack.push(new int[]{val, currentMin});
}
public int getMin() {
return stack.peek()[1];
}
}Related Problems:
- Min Stack - Track minimum in \(O(1)\)
- Max Stack - Same pattern for maximum
Time Complexity: \(O(1)\) for all operations Space Complexity: \(O(n)\)
8.0.5 Queue Fundamentals
A queue is a First-In-First-Out (FIFO) data structure supporting: - Offer/Add: Add element to rear - \(O(1)\) - Poll/Remove: Remove element from front - \(O(1)\) - Peek: View front element - \(O(1)\)
Java Implementation:
Queue<Integer> queue = new LinkedList<>();
queue.offer(1);
int front = queue.peek(); // Returns 1
int removed = queue.poll(); // Returns and removes 1
boolean empty = queue.isEmpty();When to Use Queues:
Breadth-first exploration patterns (BFS)
Level-order processing (trees, graphs)
Task scheduling and time-based simulations
Processing data with first-come-first-served order
8.0.6 Implementing Queue Using Stacks
A classic problem demonstrating stack/queue relationships.
Approach 1: Two Stacks (Amortized O(1))
Use two stacks to simulate queue behavior:
- inStack: For enqueue operations
- outStack: For dequeue operations
Key Insight: Transfer elements from inStack to outStack only when outStack is empty.
class MyQueue {
Stack<Integer> inStack;
Stack<Integer> outStack;
public MyQueue() {
inStack = new Stack<>();
outStack = new Stack<>();
}
public void push(int x) {
inStack.push(x); // O(1)
}
public int pop() {
moveIfNeeded();
return outStack.pop(); // Amortized O(1)
}
public int peek() {
moveIfNeeded();
return outStack.peek();
}
private void moveIfNeeded() {
if (outStack.isEmpty()) {
while (!inStack.isEmpty()) {
outStack.push(inStack.pop());
}
}
}
public boolean empty() {
return inStack.isEmpty() && outStack.isEmpty();
}
}Why Amortized O(1)?
Each element is moved from
inStacktooutStackexactly onceOver \(n\) operations, total moves \(\le n\)
Average per operation: \(O(1)\)
Related Problems:
- Implement Queue using Stacks - Two-stack approach
- Implement Stack using Queues - Reverse pattern
Time Complexity:
- Push: \(O(1)\)
- Pop/Peek: Amortized \(O(1)\), worst-case \(O(n)\) Space Complexity: \(O(n)\)
8.0.7 Queue-Based Simulation
Queues naturally model time-based simulations and task scheduling problems.
Pattern:
- Process events in chronological order
- Use queue to maintain pending tasks/events
- Simulate time progression step-by-step
Common Applications:
Task scheduling with cooldowns
Time-based resource allocation
Process simulation
Example Pattern - Task Scheduling with Cooldown:
public int taskScheduling(char[] tasks, int cooldown) {
// Count task frequencies
Map<Character, Integer> freq = new HashMap<>();
for (char task : tasks) {
freq.put(task, freq.getOrDefault(task, 0) + 1);
}
// Process in rounds
int time = 0;
while (!freq.isEmpty()) {
List<Character> toRemove = new ArrayList<>();
// Execute up to (cooldown + 1) tasks
for (int i = 0; i <= cooldown && !freq.isEmpty(); i++) {
char task = getMaxFreqTask(freq);
freq.put(task, freq.get(task) - 1);
if (freq.get(task) == 0) {
toRemove.add(task);
}
time++;
}
// Remove completed tasks
for (char task : toRemove) {
freq.remove(task);
}
// Add idle time if needed
if (!freq.isEmpty() && executedThisRound < cooldown + 1) {
time += (cooldown + 1 - executedThisRound);
}
}
return time;
}Related Problems:
- Task Scheduler - CPU scheduling with cooldown
- Simulation problems requiring time-based processing
8.0.8 Stack vs Queue Decision Guide
| Consideration | Use Stack | Use Queue |
|---|---|---|
| Processing Order | Most recent first (LIFO) | Oldest first (FIFO) |
| Algorithm Pattern | DFS, Backtracking | BFS, Level Order |
| Structure Type | Nested (parentheses) | Sequential (tasks) |
| State Management | Undo/Redo, History | Task Queue, Buffer |
| Monotonic Problems | Next Greater/Smaller | - |
| Example Use Cases | Expression evaluation, Function calls, Undo operations | BFS traversal, Task scheduling, Message queue |
8.0.9 Common Stack/Queue Patterns Summary
| Pattern | Data Structure | Use Case | Time | Space |
|---|---|---|---|---|
| Monotonic Stack | Stack (Decreasing/Increasing) | Next greater/smaller element | \(O(n)\) | \(O(n)\) |
| Parentheses Matching | Stack | Validate nested structures | \(O(n)\) | \(O(n)\) |
| Min/Max Stack | Stack + Auxiliary Stack | Track extrema in \(O(1)\) | \(O(1)\) | \(O(n)\) |
| Queue using Stacks | Two Stacks | Implement queue behavior | Amortized \(O(1)\) | \(O(n)\) |
| Task Simulation | Queue | Time-based processing | \(O(n)\) | \(O(n)\) |
| BFS Traversal | Queue | Level-order exploration | \(O(V + E)\) | \(O(V)\) |
8.0.10 Problem-Solving Checklist
When approaching stack/queue problems:
- ✅ Identify the pattern:
- Need next greater/smaller? → Monotonic stack
- Nested structure validation? → Stack with matching
- Level-order or BFS? → Queue
- State tracking (min/max)? → Auxiliary stack/queue
- Time-based simulation? → Queue
- ✅ Choose the right variant:
- Monotonic stack: Decreasing (next greater) vs Increasing (next smaller)
- Stack: Store indices vs values vs pairs
- Queue: Simple queue vs priority queue vs deque
- ✅ Handle edge cases:
- Empty stack/queue before pop/peek
- All elements in increasing/decreasing order (monotonic stack)
- Single element
- Duplicate values
- ✅ Optimize:
- Store indices instead of values when needed
- Use monotonic stack to reduce \(O(n^2)\) to \(O(n)\)
- Consider amortized analysis for two-stack queue