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