Monotonic Stack/Queue Patterns
Level 3: Intermediate Advanced - Master monotonic data structures for efficient problem solving
Monotonic Stack/Queue Patterns
Understanding Monotonic Stack
What is a Monotonic Stack?
A monotonic stack is a stack data structure that maintains elements in either strictly increasing or decreasing order. When a new element violates the monotonic property, elements are popped until the property is restored.
📚 Real-Life Analogy:
Think of organizing books on a shelf by height: you want all books arranged from shortest to tallest. When you add a new book, you remove any taller books from the right side until you find the correct position, maintaining the height order.
🔍 Monotonic Stack Operation:
Array: [2, 1, 2, 4, 3, 1] (Finding next greater element for each) Step-by-step with Decreasing Monotonic Stack: Initial: Stack = [], Result = [_, _, _, _, _, _] i=0, arr[0]=2: Stack = [2] (index 0) Result = [_, _, _, _, _, _] i=1, arr[1]=1: 1 < 2 (maintains decreasing order) Stack = [2, 1] (indices [0, 1]) Result = [_, _, _, _, _, _] i=2, arr[2]=2: 2 > 1, so pop 1 → next greater for index 1 is 2 2 = 2, so pop 2 → next greater for index 0 is 2 Stack = [2] (index 2) Result = [2, 2, _, _, _, _] i=3, arr[3]=4: 4 > 2, so pop 2 → next greater for index 2 is 4 Stack = [4] (index 3) Result = [2, 2, 4, _, _, _] i=4, arr[4]=3: 3 < 4 (maintains decreasing order) Stack = [4, 3] (indices [3, 4]) Result = [2, 2, 4, _, _, _] i=5, arr[5]=1: 1 < 3 (maintains decreasing order) Stack = [4, 3, 1] (indices [3, 4, 5]) Result = [2, 2, 4, _, _, _] Remaining elements in stack have no next greater element Final Result = [2, 2, 4, -1, -1, -1] Visual representation of stack states: │ 2 │ │ 1 ─────┼───── → Stack becomes [2,1] │ │ 2 2 │ 1 ─────┼───── → 2 > 1, need to pop until monotonic │ │ │ 2 ─────┼───── → Stack becomes [2] (new element) │
🎯 When to Use Monotonic Stack:
- Next/Previous Greater Element: Find the next larger element for each array element
- Largest Rectangle Problems: Find maximum area rectangles in histograms
- Temperature Problems: Days until warmer weather
- Stock Span Problems: Consecutive days with lower prices
- Trapping Rain Water: Calculate water that can be trapped
Next Greater Element Problems
Basic Next Greater Element
For each element in an array, find the next element to the right that is greater. If no such element exists, return -1. This is the fundamental monotonic stack problem.
🌍 Real-World Applications:
📈 Stock Analysis: Find the next day when stock price will be higher
🌡️ Weather Forecasting: Next day with higher temperature
🏗️ Building Heights: Next taller building in city skyline
📊 Performance Metrics: Next period with better performance
TypeScript
/** * Find the next greater element for each element in the array * * @param nums - Array of integers to process * @returns Array where result[i] is the next greater element for nums[i], or -1 if none exists * @throws Error if input array is null or undefined * * Time Complexity: O(n) - each element is pushed and popped at most once * Space Complexity: O(n) - for the result array and stack * * Example: nextGreaterElement([2, 1, 2, 4, 3, 1]) → [4, 2, 4, -1, -1, -1] */ function nextGreaterElement(nums: number[]): number[] { // Input validation if (!nums) { throw new Error("Input array cannot be null or undefined"); } if (nums.length === 0) { return []; } // Handle single element case if (nums.length === 1) { return [-1]; } const result = new Array(nums.length).fill(-1); const stack: number[] = []; // Store indices for (let i = 0; i < nums.length; i++) { // Pop elements smaller than current element while (stack.length > 0 && nums[stack[stack.length - 1]] < nums[i]) { const index = stack.pop()!; result[index] = nums[i]; } stack.push(i); } return result; } /** * Calculate how many days until a warmer temperature for each day * * @param temperatures - Array of daily temperatures * @returns Array where result[i] is days until warmer temperature, or 0 if never * @throws Error if input array is null or contains invalid temperatures * * Time Complexity: O(n) - each element processed once * Space Complexity: O(n) - for stack and result array * * Example: dailyTemperatures([73, 74, 75, 71, 69, 72, 76, 73]) → [1,1,4,2,1,1,0,0] */ function dailyTemperatures(temperatures: number[]): number[] { // Input validation if (!temperatures) { throw new Error("Temperatures array cannot be null or undefined"); } if (temperatures.length === 0) { return []; } // Validate temperature values for (let i = 0; i < temperatures.length; i++) { if (typeof temperatures[i] !== 'number' || isNaN(temperatures[i])) { throw new Error(`Invalid temperature at index ${i}: ${temperatures[i]}`); } } const result = new Array(temperatures.length).fill(0); const stack: number[] = []; for (let i = 0; i < temperatures.length; i++) { while (stack.length > 0 && temperatures[stack[stack.length - 1]] < temperatures[i]) { const index = stack.pop()!; result[index] = i - index; // Days difference } stack.push(i); } return result; } /** * Find next greater element in circular array (array wraps around) * * @param nums - Array of integers treated as circular * @returns Array where result[i] is next greater element (may wrap around), or -1 if none * @throws Error if input array is null or undefined * * Time Complexity: O(n) - each element processed at most twice * Space Complexity: O(n) - for stack and result array * * Example: nextGreaterElementsCircular([1,2,1]) → [2,-1,2] */ function nextGreaterElementsCircular(nums: number[]): number[] { // Input validation if (!nums) { throw new Error("Input array cannot be null or undefined"); } if (nums.length === 0) { return []; } if (nums.length === 1) { return [-1]; } const n = nums.length; const result = new Array(n).fill(-1); const stack: number[] = []; // Process array twice to handle circular nature for (let i = 0; i < 2 * n; i++) { const currentIndex = i % n; while (stack.length > 0 && nums[stack[stack.length - 1]] < nums[currentIndex]) { const index = stack.pop()!; result[index] = nums[currentIndex]; } // Only push in first iteration to avoid duplicates if (i < n) { stack.push(currentIndex); } } return result; } /** * Calculate stock span for each day (consecutive days with price <= current day) * * @param prices - Array of stock prices for consecutive days * @returns Array where span[i] is number of consecutive days (including current) with price <= prices[i] * @throws Error if input array is null or contains invalid prices * * Time Complexity: O(n) - each element pushed/popped once * Space Complexity: O(n) - for stack and result array * * Example: stockSpan([100, 80, 60, 70, 60, 75, 85]) → [1,1,1,2,1,4,6] */ function stockSpan(prices: number[]): number[] { // Input validation if (!prices) { throw new Error("Prices array cannot be null or undefined"); } if (prices.length === 0) { return []; } // Validate price values for (let i = 0; i < prices.length; i++) { if (typeof prices[i] !== 'number' || isNaN(prices[i]) || prices[i] < 0) { throw new Error(`Invalid price at index ${i}: ${prices[i]}`); } } const span = new Array(prices.length); const stack: number[] = []; // Store indices for (let i = 0; i < prices.length; i++) { // Pop indices of prices smaller than or equal to current price while (stack.length > 0 && prices[stack[stack.length - 1]] <= prices[i]) { stack.pop(); } // Span is distance to previous greater element span[i] = stack.length === 0 ? i + 1 : i - stack[stack.length - 1]; stack.push(i); } return span; } /** * Remove k digits from number to form the smallest possible number * * @param num - String representation of a number * @param k - Number of digits to remove * @returns Smallest possible number after removing k digits * @throws Error if inputs are invalid * * Time Complexity: O(n) - each digit processed once * Space Complexity: O(n) - for stack storage * * Example: removeKdigits("1432219", 3) → "1219" */ function removeKdigits(num: string, k: number): string { // Input validation if (!num || typeof num !== 'string') { throw new Error("Number must be a valid string"); } if (!/^\d+$/.test(num)) { throw new Error("Number string must contain only digits"); } if (typeof k !== 'number' || k < 0 || !Number.isInteger(k)) { throw new Error("k must be a non-negative integer"); } if (k >= num.length) { return "0"; } if (k === 0) { return num; } const stack: string[] = []; let toRemove = k; for (const digit of num) { // Remove larger digits from stack if we can still remove digits while (stack.length > 0 && stack[stack.length - 1] > digit && toRemove > 0) { stack.pop(); toRemove--; } stack.push(digit); } // Remove remaining digits from end if needed while (toRemove > 0 && stack.length > 0) { stack.pop(); toRemove--; } // Build result and handle leading zeros const result = stack.join('').replace(/^0+/, '') || '0'; return result; } // Example usage and test cases function demonstrateMonotonicStack(): void { console.log("Next Greater Element:"); console.log(nextGreaterElement([2, 1, 2, 4, 3, 1])); // [4, 2, 4, -1, -1, -1] console.log("\nDaily Temperatures:"); console.log(dailyTemperatures([73, 74, 75, 71, 69, 72, 76, 73])); // [1,1,4,2,1,1,0,0] console.log("\nStock Span:"); console.log(stockSpan([100, 80, 60, 70, 60, 75, 85])); // [1,1,1,2,1,4,6] console.log("\nRemove K Digits:"); console.log(removeKdigits("1432219", 3)); // "1219" }
Python
from typing import List def next_greater_element(nums: List[int]) -> List[int]: """ Find the next greater element for each element in the array Args: nums: List of integers to process Returns: List where result[i] is the next greater element for nums[i], or -1 if none exists Raises: ValueError: If input list is None Time Complexity: O(n) - each element is pushed and popped at most once Space Complexity: O(n) - for the result list and stack Example: next_greater_element([2, 1, 2, 4, 3, 1]) → [4, 2, 4, -1, -1, -1] """ # Input validation if nums is None: raise ValueError("Input list cannot be None") if len(nums) == 0: return [] # Handle single element case if len(nums) == 1: return [-1] result = [-1] * len(nums) stack = [] # Store indices for i, num in enumerate(nums): # Pop elements smaller than current element while stack and nums[stack[-1]] < num: index = stack.pop() result[index] = num stack.append(i) return result def daily_temperatures(temperatures: List[int]) -> List[int]: """ Calculate how many days until a warmer temperature for each day Args: temperatures: List of daily temperatures Returns: List where result[i] is days until warmer temperature, or 0 if never Raises: ValueError: If input list is None or contains invalid temperatures Time Complexity: O(n) - each element processed once Space Complexity: O(n) - for stack and result list Example: daily_temperatures([73, 74, 75, 71, 69, 72, 76, 73]) → [1,1,4,2,1,1,0,0] """ # Input validation if temperatures is None: raise ValueError("Temperatures list cannot be None") if len(temperatures) == 0: return [] # Validate temperature values for i, temp in enumerate(temperatures): if not isinstance(temp, (int, float)) or temp != temp: # Check for NaN raise ValueError(f"Invalid temperature at index {i}: {temp}") result = [0] * len(temperatures) stack = [] for i, temp in enumerate(temperatures): while stack and temperatures[stack[-1]] < temp: prev_index = stack.pop() result[prev_index] = i - prev_index # Days difference stack.append(i) return result def next_greater_elements_circular(nums: List[int]) -> List[int]: """ Find next greater element in circular array (array wraps around) Args: nums: List of integers treated as circular Returns: List where result[i] is next greater element (may wrap around), or -1 if none Raises: ValueError: If input list is None Time Complexity: O(n) - each element processed at most twice Space Complexity: O(n) - for stack and result list Example: next_greater_elements_circular([1,2,1]) → [2,-1,2] """ # Input validation if nums is None: raise ValueError("Input list cannot be None") if len(nums) == 0: return [] if len(nums) == 1: return [-1] n = len(nums) result = [-1] * n stack = [] # Process array twice to handle circular nature for i in range(2 * n): current_index = i % n while stack and nums[stack[-1]] < nums[current_index]: index = stack.pop() result[index] = nums[current_index] # Only push in first iteration if i < n: stack.append(current_index) return result def stock_span(prices: List[float]) -> List[int]: """ Calculate stock span for each day (consecutive days with price <= current day) Args: prices: List of stock prices for consecutive days Returns: List where span[i] is number of consecutive days (including current) with price <= prices[i] Raises: ValueError: If input list is None or contains invalid prices Time Complexity: O(n) - each element pushed/popped once Space Complexity: O(n) - for stack and result list Example: stock_span([100, 80, 60, 70, 60, 75, 85]) → [1,1,1,2,1,4,6] """ # Input validation if prices is None: raise ValueError("Prices list cannot be None") if len(prices) == 0: return [] # Validate price values for i, price in enumerate(prices): if not isinstance(price, (int, float)) or price != price or price < 0: raise ValueError(f"Invalid price at index {i}: {price}") span = [0] * len(prices) stack = [] # Store indices for i, price in enumerate(prices): # Pop indices of prices smaller than or equal to current price while stack and prices[stack[-1]] <= price: stack.pop() # Span is distance to previous greater element span[i] = i + 1 if not stack else i - stack[-1] stack.append(i) return span def remove_k_digits(num: str, k: int) -> str: """ Remove k digits from number to form the smallest possible number Args: num: String representation of a number k: Number of digits to remove Returns: Smallest possible number after removing k digits Raises: ValueError: If inputs are invalid Time Complexity: O(n) - each digit processed once Space Complexity: O(n) - for stack storage Example: remove_k_digits("1432219", 3) → "1219" """ # Input validation if not isinstance(num, str) or num is None: raise ValueError("Number must be a valid string") if not num.isdigit(): raise ValueError("Number string must contain only digits") if not isinstance(k, int) or k < 0: raise ValueError("k must be a non-negative integer") if k >= len(num): return "0" if k == 0: return num stack = [] to_remove = k for digit in num: # Remove larger digits from stack if possible while stack and stack[-1] > digit and to_remove > 0: stack.pop() to_remove -= 1 stack.append(digit) # Remove remaining digits from end if needed while to_remove > 0 and stack: stack.pop() to_remove -= 1 # Build result and handle leading zeros result = ''.join(stack).lstrip('0') return result or '0' # Advanced: Next Greater Element with frequency def next_greater_frequency(arr): """Find next element with greater frequency""" from collections import Counter freq = Counter(arr) result = [-1] * len(arr) stack = [] for i, num in enumerate(arr): while stack and freq[arr[stack[-1]]] < freq[num]: index = stack.pop() result[index] = num stack.append(i) return result # Monotonic Queue for Sliding Window Maximum from collections import deque def sliding_window_maximum(nums, k): """Find maximum in each sliding window of size k""" if not nums or k == 0: return [] dq = deque() # Store indices in decreasing order of values result = [] for i in range(len(nums)): # Remove indices outside current window while dq and dq[0] <= i - k: dq.popleft() # Remove indices of smaller elements while dq and nums[dq[-1]] < nums[i]: dq.pop() dq.append(i) # Add to result if window is of size k if i >= k - 1: result.append(nums[dq[0]]) return result # Example usage def demonstrate_monotonic_patterns(): print("Next Greater Element:") print(next_greater_element([2, 1, 2, 4, 3, 1])) # [4, 2, 4, -1, -1, -1] print("\nDaily Temperatures:") print(daily_temperatures([73, 74, 75, 71, 69, 72, 76, 73])) # [1,1,4,2,1,1,0,0] print("\nStock Span:") print(stock_span([100, 80, 60, 70, 60, 75, 85])) # [1,1,1,2,1,4,6] print("\nSliding Window Maximum:") print(sliding_window_maximum([1,3,-1,-3,5,3,6,7], 3)) # [3,3,5,5,6,7]
OCaml
(* Next Greater Element *) let next_greater_element nums = let n = Array.length nums in let result = Array.make n (-1) in let stack = ref [] in for i = 0 to n - 1 do (* Pop elements smaller than current element *) while !stack <> [] && nums.(List.hd !stack) < nums.(i) do let index = List.hd !stack in stack := List.tl !stack; result.(index) <- nums.(i) done; stack := i :: !stack done; result (* Daily Temperatures *) let daily_temperatures temperatures = let n = Array.length temperatures in let result = Array.make n 0 in let stack = ref [] in for i = 0 to n - 1 do while !stack <> [] && temperatures.(List.hd !stack) < temperatures.(i) do let prev_index = List.hd !stack in stack := List.tl !stack; result.(prev_index) <- i - prev_index done; stack := i :: !stack done; result (* Stock Span Problem *) let stock_span prices = let n = Array.length prices in let span = Array.make n 0 in let stack = ref [] in for i = 0 to n - 1 do (* Pop smaller or equal prices *) while !stack <> [] && prices.(List.hd !stack) <= prices.(i) do stack := List.tl !stack done; (* Calculate span *) span.(i) <- (match !stack with | [] -> i + 1 | h :: _ -> i - h); stack := i :: !stack done; span (* Remove K Digits *) let remove_k_digits num k = let digits = List.init (String.length num) (String.get num) in let stack = ref [] in let to_remove = ref k in List.iter (fun digit -> (* Remove larger digits from stack *) while !stack <> [] && List.hd !stack > digit && !to_remove > 0 do stack := List.tl !stack; decr to_remove done; stack := digit :: !stack ) digits; (* Remove remaining digits from end *) while !to_remove > 0 do stack := List.tl !stack; decr to_remove done; (* Build result *) let result_chars = List.rev !stack in let result = String.of_seq (List.to_seq result_chars) in (* Remove leading zeros *) let trimmed = Str.global_replace (Str.regexp "^0+") "" result in if trimmed = "" then "0" else trimmed (* Sliding Window Maximum using Deque *) module Deque = struct type 'a t = { mutable front: 'a list; mutable back: 'a list; } let create () = { front = []; back = [] } let push_back dq x = dq.back <- x :: dq.back let pop_back dq = match dq.back with | h :: t -> dq.back <- t; h | [] -> dq.front <- List.rev dq.front; dq.back <- List.tl dq.front; dq.front <- []; List.hd (List.rev (List.tl (List.rev dq.front))) let pop_front dq = match dq.front with | h :: t -> dq.front <- t; h | [] -> dq.front <- List.rev dq.back; dq.back <- []; let h = List.hd dq.front in dq.front <- List.tl dq.front; h let front dq = match dq.front with | h :: _ -> h | [] -> List.hd (List.rev dq.back) let is_empty dq = dq.front = [] && dq.back = [] end let sliding_window_maximum nums k = let n = Array.length nums in if n = 0 || k = 0 then [||] else let dq = Deque.create () in let result = ref [] in for i = 0 to n - 1 do (* Remove indices outside current window *) while not (Deque.is_empty dq) && Deque.front dq <= i - k do ignore (Deque.pop_front dq) done; (* Remove indices of smaller elements *) while not (Deque.is_empty dq) && nums.(i) > nums.(Deque.front dq) do ignore (Deque.pop_back dq) done; Deque.push_back dq i; (* Add to result if window is complete *) if i >= k - 1 then result := nums.(Deque.front dq) :: !result done; Array.of_list (List.rev !result) (* Example usage *) let demonstrate_patterns () = let nums = [|2; 1; 2; 4; 3; 1|] in Printf.printf "Next Greater Element: "; Array.iter (Printf.printf "%d ") (next_greater_element nums); print_newline (); let temps = [|73; 74; 75; 71; 69; 72; 76; 73|] in Printf.printf "Daily Temperatures: "; Array.iter (Printf.printf "%d ") (daily_temperatures temps); print_newline ()
Problem | Time Complexity | Space Complexity | Key Insight |
---|---|---|---|
Next Greater Element | O(n) | O(n) | Each element pushed/popped once |
Daily Temperatures | O(n) | O(n) | Store indices for distance calculation |
Stock Span | O(n) | O(n) | Count consecutive smaller elements |
Remove K Digits | O(n) | O(n) | Greedy removal of larger digits |
Interview Tips & Pattern Recognition
When to Use Monotonic Stack/Deque
Recognizing when to apply monotonic stack or deque patterns is crucial for interview success. Look for specific keywords and problem structures that hint at these patterns.
🔍 Pattern Recognition Keywords:
- "Next greater/smaller element": Classic monotonic stack
- "Days until warmer/colder": Temperature-style problems
- "Largest rectangle/area": Histogram problems
- "Sliding window maximum/minimum": Monotonic deque
- "Trapped water": Stack for calculating areas between barriers
- "Remove K elements to optimize": Greedy removal with stack
💡 Problem-Solving Strategy:
- Step 1: Identify if the problem involves finding the "next" or "previous" element with certain properties
- Step 2: Check if maintaining order (increasing/decreasing) would help solve the problem efficiently
- Step 3: Determine if you need LIFO (stack) or need to remove from both ends (deque)
- Step 4: Consider what information to store: values, indices, or both
- Step 5: Handle edge cases: empty stack/deque, remaining elements
🎯 Common Interview Mistakes to Avoid:
- Storing values instead of indices: Often you need positions for distance calculations
- Wrong monotonic order: Increasing vs decreasing - think about what you need to maintain
- Forgetting to process remaining elements: Elements left in stack after main loop
- Off-by-one errors: Especially in sliding window problems with indices
- Not handling duplicates correctly: Use ≤ or < appropriately
🌟 Key Takeaways for Interviews:
⚡ Time Complexity: Most monotonic stack/deque problems are O(n) because each element is pushed and popped at most once
💾 Space Optimization: Sometimes you can optimize space by processing in specific order (right to left vs left to right)
🔄 Circular Arrays: Process array twice (2n iterations) to handle circular nature
🎨 Clean Code: Use meaningful variable names like `stack`, `deque`, `indices` rather than generic names