Monotonic Stack and Queue Patterns

1. Next Greater Element (Stack Pattern)

Concept Stack Property Operation Complexity
Monotonic Stack Elements in increasing/decreasing order Pop elements that violate monotonicity O(n)
Next Greater Decreasing stack (top to bottom) Pop smaller elements when larger found O(n) amortized
Previous Greater Same as next greater, scan left to right Stack top is previous greater O(n)
Circular Array Process array twice (index % n) Handle wraparound with modulo O(n)

Example: Next Greater Element

function nextGreaterElement(nums) {
  const n = nums.length;
  const result = new Array(n).fill(-1);
  const stack = []; // Stores indices
  
  for (let i = 0; i < n; i++) {
    // Pop all smaller elements - current is their next greater
    while (stack.length > 0 && nums[stack[stack.length - 1]] < nums[i]) {
      const index = stack.pop();
      result[index] = nums[i];
    }
    
    stack.push(i);
  }
  
  // Remaining elements have no next greater (-1)
  return result;
}

console.log(nextGreaterElement([4, 5, 2, 10, 8]));
// [5, 10, 10, -1, -1]

// Trace:
// i=0, nums[0]=4: stack=[0]
// i=1, nums[1]=5: pop 0 (4<5), result[0]=5, stack=[1]
// i=2, nums[2]=2: stack=[1,2]
// i=3, nums[3]=10: pop 2 (2<10), pop 1 (5<10), result[2]=10, result[1]=10, stack=[3]
// i=4, nums[4]=8: stack=[3,4]
// Final: result[3]=-1, result[4]=-1

Example: Next Greater Element II (Circular Array)

function nextGreaterElementsCircular(nums) {
  const n = nums.length;
  const result = new Array(n).fill(-1);
  const stack = [];
  
  // Process array twice to handle circular nature
  for (let i = 0; i < 2 * n; i++) {
    const num = nums[i % n];
    
    while (stack.length > 0 && nums[stack[stack.length - 1]] < num) {
      const index = stack.pop();
      result[index] = num;
    }
    
    // Only push indices in first pass
    if (i < n) {
      stack.push(i);
    }
  }
  
  return result;
}

console.log(nextGreaterElementsCircular([1, 2, 1])); // [2, -1, 2]
// In circular: [1,2,1,1,2,1]
// 1 → 2, 2 → -1 (largest), 1 → 2 (wraps around)

console.log(nextGreaterElementsCircular([1, 2, 3, 4, 3])); 
// [2, 3, 4, -1, 4]

Example: Next Greater Element I (Subset Problem)

function nextGreaterElementSubset(nums1, nums2) {
  // nums1 is subset of nums2
  const nextGreater = new Map();
  const stack = [];
  
  // Build next greater map for nums2
  for (const num of nums2) {
    while (stack.length > 0 && stack[stack.length - 1] < num) {
      nextGreater.set(stack.pop(), num);
    }
    stack.push(num);
  }
  
  // Map nums1 to their next greater in nums2
  return nums1.map(num => nextGreater.get(num) ?? -1);
}

console.log(nextGreaterElementSubset([4, 1, 2], [1, 3, 4, 2]));
// [-, 3, 3]
// 4: no next greater → -1
// 1: next greater is 3
// 2: no next greater → -1
Note: Monotonic stack works in O(n) amortized time because each element is pushed and popped at most once. The stack maintains elements in decreasing order for "next greater" problems.

2. Next Smaller Element Pattern

Variation Stack Order Condition Use Case
Next Smaller Increasing stack Pop when current < stack.top() Find next smaller to the right
Previous Smaller Increasing stack Stack top after popping larger Find previous smaller to the left
Next Greater Decreasing stack Pop when current > stack.top() Find next greater to the right
Previous Greater Decreasing stack Stack top after popping smaller Find previous greater to the left

Example: Next Smaller Element

function nextSmallerElement(nums) {
  const n = nums.length;
  const result = new Array(n).fill(-1);
  const stack = [];
  
  for (let i = 0; i < n; i++) {
    // Pop all greater elements - current is their next smaller
    while (stack.length > 0 && nums[stack[stack.length - 1]] > nums[i]) {
      const index = stack.pop();
      result[index] = nums[i];
    }
    
    stack.push(i);
  }
  
  return result;
}

console.log(nextSmallerElement([4, 5, 2, 10, 8]));
// [2, 2, -1, 8, -1]

// Previous and Next for all four variants
function findPrevNextGreaterSmaller(nums) {
  const n = nums.length;
  
  // Next Greater
  const nextGreater = new Array(n).fill(-1);
  let stack = [];
  for (let i = 0; i < n; i++) {
    while (stack.length && nums[stack[stack.length - 1]] < nums[i]) {
      nextGreater[stack.pop()] = nums[i];
    }
    stack.push(i);
  }
  
  // Previous Greater
  const prevGreater = new Array(n).fill(-1);
  stack = [];
  for (let i = 0; i < n; i++) {
    while (stack.length && nums[stack[stack.length - 1]] <= nums[i]) {
      stack.pop();
    }
    if (stack.length) prevGreater[i] = nums[stack[stack.length - 1]];
    stack.push(i);
  }
  
  // Next Smaller
  const nextSmaller = new Array(n).fill(-1);
  stack = [];
  for (let i = 0; i < n; i++) {
    while (stack.length && nums[stack[stack.length - 1]] > nums[i]) {
      nextSmaller[stack.pop()] = nums[i];
    }
    stack.push(i);
  }
  
  // Previous Smaller
  const prevSmaller = new Array(n).fill(-1);
  stack = [];
  for (let i = 0; i < n; i++) {
    while (stack.length && nums[stack[stack.length - 1]] >= nums[i]) {
      stack.pop();
    }
    if (stack.length) prevSmaller[i] = nums[stack[stack.length - 1]];
    stack.push(i);
  }
  
  return { nextGreater, prevGreater, nextSmaller, prevSmaller };
}

const arr = [3, 7, 8, 4];
console.log(findPrevNextGreaterSmaller(arr));
// nextGreater:  [7, 8, -1, -1]
// prevGreater:  [-1, -1, -1, 8]
// nextSmaller:  [-1, 4, 4, -1]
// prevSmaller:  [-1, 3, 7, 3]

Monotonic Stack Patterns

  • Next Greater: Decreasing stack, pop smaller
  • Next Smaller: Increasing stack, pop greater
  • Previous Greater: Decreasing stack, check top
  • Previous Smaller: Increasing stack, check top
  • Key: Stack order determines what we find
Note: Stack Choice
  • Decreasing: Find greater elements
  • Increasing: Find smaller elements
  • Right scan: Next element
  • Left scan: Previous element

3. Largest Rectangle in Histogram

Approach Strategy Key Insight Complexity
Monotonic Stack Track previous/next smaller for each bar Width = right - left - 1, area = height * width O(n)
Brute Force For each bar, expand left and right Find max width with current height O(n²)
Divide & Conquer Find min in range, recurse left/right Max is min*width or in subproblem O(n log n)

Example: Largest Rectangle in Histogram

function largestRectangleArea(heights) {
  const stack = []; // Stores indices
  let maxArea = 0;
  
  for (let i = 0; i <= heights.length; i++) {
    // Use 0 as sentinel to pop all remaining bars
    const h = i === heights.length ? 0 : heights[i];
    
    // Pop bars taller than current
    while (stack.length > 0 && heights[stack[stack.length - 1]] > h) {
      const height = heights[stack.pop()];
      
      // Width: from element after previous stack top to current-1
      const width = stack.length === 0 ? i : i - stack[stack.length - 1] - 1;
      
      maxArea = Math.max(maxArea, height * width);
    }
    
    stack.push(i);
  }
  
  return maxArea;
}

console.log(largestRectangleArea([2, 1, 5, 6, 2, 3])); // 10

// Trace for [2,1,5,6,2,3]:
// i=0, h=2: stack=[0]
// i=1, h=1: pop 0 (2>1), area=2*1=2, stack=[1]
// i=2, h=5: stack=[1,2]
// i=3, h=6: stack=[1,2,3]
// i=4, h=2: 
//   pop 3 (6>2), area=6*1=6
//   pop 2 (5>2), area=5*2=10 ✓
//   stack=[1,4]
// i=5, h=3: stack=[1,4,5]
// i=6, h=0: pop all, check areas

Example: Using Previous and Next Smaller Arrays

function largestRectangleAreaExplicit(heights) {
  const n = heights.length;
  
  // Find previous smaller (left boundary)
  const prevSmaller = new Array(n).fill(-1);
  let stack = [];
  for (let i = 0; i < n; i++) {
    while (stack.length && heights[stack[stack.length - 1]] >= heights[i]) {
      stack.pop();
    }
    if (stack.length) prevSmaller[i] = stack[stack.length - 1];
    stack.push(i);
  }
  
  // Find next smaller (right boundary)
  const nextSmaller = new Array(n).fill(n);
  stack = [];
  for (let i = 0; i < n; i++) {
    while (stack.length && heights[stack[stack.length - 1]] > heights[i]) {
      nextSmaller[stack.pop()] = i;
    }
    stack.push(i);
  }
  
  // Calculate max area
  let maxArea = 0;
  for (let i = 0; i < n; i++) {
    const width = nextSmaller[i] - prevSmaller[i] - 1;
    const area = heights[i] * width;
    maxArea = Math.max(maxArea, area);
  }
  
  return maxArea;
}

// Visualization for [2,1,5,6,2,3]:
// Index:        0  1  2  3  4  5
// Heights:      2  1  5  6  2  3
// PrevSmaller: -1 -1  1  2  1  4
// NextSmaller:  1  6  4  4  6  6
// 
// For i=2 (height=5):
//   left = prevSmaller[2] + 1 = 1 + 1 = 2
//   right = nextSmaller[2] - 1 = 4 - 1 = 3
//   width = 3 - 2 + 1 = 2
//   area = 5 * 2 = 10

Example: Maximal Rectangle in Binary Matrix

function maximalRectangle(matrix) {
  if (!matrix.length || !matrix[0].length) return 0;
  
  const m = matrix.length;
  const n = matrix[0].length;
  const heights = new Array(n).fill(0);
  let maxArea = 0;
  
  for (let row = 0; row < m; row++) {
    // Build histogram heights for current row
    for (let col = 0; col < n; col++) {
      if (matrix[row][col] === '1') {
        heights[col]++;
      } else {
        heights[col] = 0;
      }
    }
    
    // Find largest rectangle in current histogram
    maxArea = Math.max(maxArea, largestRectangleArea(heights));
  }
  
  return maxArea;
}

const matrix = [
  ['1','0','1','0','0'],
  ['1','0','1','1','1'],
  ['1','1','1','1','1'],
  ['1','0','0','1','0']
];
console.log(maximalRectangle(matrix)); // 6

// Row 0: heights = [1,0,1,0,0]
// Row 1: heights = [2,0,2,1,1]
// Row 2: heights = [3,1,3,2,2] → max area = 6
// Row 3: heights = [4,0,0,3,0]
Note: For histogram problems, the key insight is: for each bar, find the largest rectangle using this bar as the minimum height. Width is determined by previous and next smaller elements.

4. Sliding Window Maximum (Deque)

Approach Data Structure Property Complexity
Monotonic Deque Deque storing decreasing values Front always has max in current window O(n)
Brute Force Scan window for max each time Check all k elements per window O(n*k)
Heap/Priority Queue Max heap with lazy deletion Extract max, ignore out-of-window O(n log k)

Example: Sliding Window Maximum Using Deque

function maxSlidingWindow(nums, k) {
  const result = [];
  const deque = []; // Stores indices
  
  for (let i = 0; i < nums.length; i++) {
    // Remove elements outside current window
    while (deque.length && deque[0] < i - k + 1) {
      deque.shift();
    }
    
    // Remove elements smaller than current (they can't be max)
    while (deque.length && nums[deque[deque.length - 1]] < nums[i]) {
      deque.pop();
    }
    
    deque.push(i);
    
    // Add max of current window to result
    if (i >= k - 1) {
      result.push(nums[deque[0]]);
    }
  }
  
  return result;
}

console.log(maxSlidingWindow([1,3,-1,-3,5,3,6,7], 3));
// [3, 3, 5, 5, 6, 7]

// Trace for k=3:
// i=0: deque=[0], window=[1]
// i=1: remove 0 (1<3), deque=[1], window=[1,3]
// i=2: deque=[1,2], window=[1,3,-1], max=3
// i=3: deque=[1,3], window=[3,-1,-3], max=3
// i=4: remove 1,2,3 (all<5), deque=[4], window=[-1,-3,5], max=5
// i=5: deque=[4,5], window=[-3,5,3], max=5
// i=6: remove 4,5 (all<6), deque=[6], window=[5,3,6], max=6
// i=7: remove 6 (6<7), deque=[7], window=[3,6,7], max=7

Example: Min and Max in Sliding Window

function minMaxSlidingWindow(nums, k) {
  const maxResult = [];
  const minResult = [];
  const maxDeque = []; // Decreasing deque for max
  const minDeque = []; // Increasing deque for min
  
  for (let i = 0; i < nums.length; i++) {
    // Remove out-of-window elements
    while (maxDeque.length && maxDeque[0] < i - k + 1) maxDeque.shift();
    while (minDeque.length && minDeque[0] < i - k + 1) minDeque.shift();
    
    // Maintain decreasing deque for max
    while (maxDeque.length && nums[maxDeque[maxDeque.length - 1]] < nums[i]) {
      maxDeque.pop();
    }
    
    // Maintain increasing deque for min
    while (minDeque.length && nums[minDeque[minDeque.length - 1]] > nums[i]) {
      minDeque.pop();
    }
    
    maxDeque.push(i);
    minDeque.push(i);
    
    if (i >= k - 1) {
      maxResult.push(nums[maxDeque[0]]);
      minResult.push(nums[minDeque[0]]);
    }
  }
  
  return { max: maxResult, min: minResult };
}

console.log(minMaxSlidingWindow([1,3,-1,-3,5,3,6,7], 3));
// max: [3, 3, 5, 5, 6, 7]
// min: [-1, -3, -3, -3, 3, 3]
Note: Monotonic deque maintains a decreasing sequence of indices for max (increasing for min). Front has the maximum, back removes smaller elements that can never be maximum.

5. Stock Span Problem

Concept Definition Approach Complexity
Stock Span Number of consecutive days ≤ current price Count days from current back to previous greater O(n)
Monotonic Stack Store indices of decreasing prices Span = current index - previous greater index O(n)
Brute Force For each day, scan backwards Count until price > current O(n²)

Example: Stock Span

function calculateSpan(prices) {
  const n = prices.length;
  const span = new Array(n);
  const stack = []; // Stores indices
  
  for (let i = 0; i < n; i++) {
    // Pop all prices less than or equal to current
    while (stack.length && prices[stack[stack.length - 1]] <= prices[i]) {
      stack.pop();
    }
    
    // Span = distance to previous greater (or start of array)
    span[i] = stack.length === 0 ? i + 1 : i - stack[stack.length - 1];
    
    stack.push(i);
  }
  
  return span;
}

console.log(calculateSpan([100, 80, 60, 70, 60, 75, 85]));
// [1, 1, 1, 2, 1, 4, 6]

// Explanation:
// Day 0 (100): no previous days, span=1
// Day 1 (80): 80<100, span=1
// Day 2 (60): 60<80, span=1
// Day 3 (70): 70>60, includes day 2, span=2
// Day 4 (60): 60<70, span=1
// Day 5 (75): 75>60,60,70,60, includes days 2-4, span=4
// Day 6 (85): 85>75,60,70,60,80, includes days 1-5, span=6

Example: Online Stock Span (Class Design)

class StockSpanner {
  constructor() {
    this.stack = []; // [price, span]
    this.index = 0;
  }
  
  next(price) {
    let span = 1;
    
    // Pop all lower or equal prices and accumulate their spans
    while (this.stack.length && this.stack[this.stack.length - 1][0] <= price) {
      span += this.stack.pop()[1];
    }
    
    this.stack.push([price, span]);
    return span;
  }
}

const spanner = new StockSpanner();
console.log(spanner.next(100)); // 1
console.log(spanner.next(80));  // 1
console.log(spanner.next(60));  // 1
console.log(spanner.next(70));  // 2
console.log(spanner.next(60));  // 1
console.log(spanner.next(75));  // 4
console.log(spanner.next(85));  // 6

// Alternative: Store with indices
class StockSpannerWithIndex {
  constructor() {
    this.stack = []; // [index, price]
    this.index = -1;
  }
  
  next(price) {
    this.index++;
    
    while (this.stack.length && this.stack[this.stack.length - 1][1] <= price) {
      this.stack.pop();
    }
    
    const span = this.stack.length === 0 
      ? this.index + 1 
      : this.index - this.stack[this.stack.length - 1][0];
    
    this.stack.push([this.index, price]);
    return span;
  }
}

6. Trapping Rain Water Problem

Approach Strategy Key Insight Complexity
Two Pointers Track left_max and right_max Water = min(left_max, right_max) - height O(n), O(1) space
Monotonic Stack Calculate horizontal layers Fill by height differences O(n), O(n) space
DP Arrays Precompute left_max and right_max Two passes to build arrays O(n), O(n) space

Example: Trapping Rain Water (Two Pointers)

function trap(height) {
  if (!height.length) return 0;
  
  let left = 0, right = height.length - 1;
  let leftMax = 0, rightMax = 0;
  let water = 0;
  
  while (left < right) {
    if (height[left] < height[right]) {
      // Process left side
      if (height[left] >= leftMax) {
        leftMax = height[left];
      } else {
        water += leftMax - height[left];
      }
      left++;
    } else {
      // Process right side
      if (height[right] >= rightMax) {
        rightMax = height[right];
      } else {
        water += rightMax - height[right];
      }
      right--;
    }
  }
  
  return water;
}

console.log(trap([0,1,0,2,1,0,1,3,2,1,2,1])); // 6

// Visualization:
//     ██
// ██▓▓██▓██
// ██▓███▓███
// Water (▓) = 6 units

Example: Trapping Rain Water (Monotonic Stack)

function trapStack(height) {
  const stack = []; // Stores indices
  let water = 0;
  
  for (let i = 0; i < height.length; i++) {
    // Fill water when we find a taller bar
    while (stack.length && height[i] > height[stack[stack.length - 1]]) {
      const top = stack.pop();
      
      if (!stack.length) break;
      
      const distance = i - stack[stack.length - 1] - 1;
      const boundedHeight = Math.min(
        height[i],
        height[stack[stack.length - 1]]
      ) - height[top];
      
      water += distance * boundedHeight;
    }
    
    stack.push(i);
  }
  
  return water;
}

// DP approach with precomputed arrays
function trapDP(height) {
  const n = height.length;
  if (n === 0) return 0;
  
  const leftMax = new Array(n);
  const rightMax = new Array(n);
  
  // Build left max array
  leftMax[0] = height[0];
  for (let i = 1; i < n; i++) {
    leftMax[i] = Math.max(leftMax[i - 1], height[i]);
  }
  
  // Build right max array
  rightMax[n - 1] = height[n - 1];
  for (let i = n - 2; i >= 0; i--) {
    rightMax[i] = Math.max(rightMax[i + 1], height[i]);
  }
  
  // Calculate water
  let water = 0;
  for (let i = 0; i < n; i++) {
    water += Math.min(leftMax[i], rightMax[i]) - height[i];
  }
  
  return water;
}

console.log(trapStack([0,1,0,2,1,0,1,3,2,1,2,1])); // 6
console.log(trapDP([0,1,0,2,1,0,1,3,2,1,2,1])); // 6

Example: Trapping Rain Water II (2D Grid)

function trapRainWater2D(heightMap) {
  if (!heightMap.length || !heightMap[0].length) return 0;
  
  const m = heightMap.length;
  const n = heightMap[0].length;
  
  // Min heap: [height, row, col]
  const pq = new MinHeap((a, b) => a[0] - b[0]);
  const visited = Array(m).fill(null).map(() => Array(n).fill(false));
  
  // Add all border cells to heap
  for (let i = 0; i < m; i++) {
    for (let j = 0; j < n; j++) {
      if (i === 0 || i === m - 1 || j === 0 || j === n - 1) {
        pq.push([heightMap[i][j], i, j]);
        visited[i][j] = true;
      }
    }
  }
  
  const dirs = [[0,1], [1,0], [0,-1], [-1,0]];
  let water = 0;
  let maxHeight = 0;
  
  while (pq.size() > 0) {
    const [height, row, col] = pq.pop();
    maxHeight = Math.max(maxHeight, height);
    
    for (const [dr, dc] of dirs) {
      const newRow = row + dr;
      const newCol = col + dc;
      
      if (newRow >= 0 && newRow < m && 
          newCol >= 0 && newCol < n && 
          !visited[newRow][newCol]) {
        
        visited[newRow][newCol] = true;
        
        // Water trapped = max seen height - current height
        if (heightMap[newRow][newCol] < maxHeight) {
          water += maxHeight - heightMap[newRow][newCol];
        }
        
        pq.push([heightMap[newRow][newCol], newRow, newCol]);
      }
    }
  }
  
  return water;
}

// Helper MinHeap class (reuse from previous sections)
class MinHeap {
  constructor(compareFn) {
    this.heap = [];
    this.compare = compareFn || ((a, b) => a - b);
  }
  
  push(val) {
    this.heap.push(val);
    this.bubbleUp(this.heap.length - 1);
  }
  
  pop() {
    if (this.size() === 0) return null;
    if (this.size() === 1) return this.heap.pop();
    const min = this.heap[0];
    this.heap[0] = this.heap.pop();
    this.bubbleDown(0);
    return min;
  }
  
  bubbleUp(i) {
    while (i > 0) {
      const p = Math.floor((i - 1) / 2);
      if (this.compare(this.heap[p], this.heap[i]) <= 0) break;
      [this.heap[p], this.heap[i]] = [this.heap[i], this.heap[p]];
      i = p;
    }
  }
  
  bubbleDown(i) {
    while (true) {
      let min = i;
      const left = 2 * i + 1, right = 2 * i + 2;
      if (left < this.size() && this.compare(this.heap[left], this.heap[min]) < 0) min = left;
      if (right < this.size() && this.compare(this.heap[right], this.heap[min]) < 0) min = right;
      if (min === i) break;
      [this.heap[i], this.heap[min]] = [this.heap[min], this.heap[i]];
      i = min;
    }
  }
  
  size() { return this.heap.length; }
}
Note: For trapping rain water, the key insight is: water at position i = min(leftMax, rightMax) - height[i]. Two pointers optimize space by tracking maxes on-the-fly without extra arrays.

Summary: Monotonic Stack Queue Pattern

  • Next Greater Element: Decreasing stack - pop smaller when larger found - O(n) amortized
  • Next Smaller Element: Increasing stack - pop greater when smaller found - mirror of next greater
  • Largest Rectangle: For each bar, find previous/next smaller - width between them, area = height * width
  • Sliding Window Max: Monotonic deque (decreasing) - front is max, remove out-of-window from front
  • Stock Span: Days since last greater price - use stack to track previous greater index
  • Trapping Rain Water: Two pointers with leftMax/rightMax - water = min(maxes) - height
  • Stack vs Deque: Stack for next/previous elements, Deque for sliding window max/min
  • Monotonic Property: Decreasing for greater, increasing for smaller - maintains order invariant
  • Key Insight: Each element pushed and popped once - amortized O(n) despite nested loops