Advanced

Monotonic Stack

A monotonic stack maintains elements in strictly increasing or decreasing order. This pattern solves "next greater/smaller element" problems in O(n) time, replacing brute-force O(n²) approaches.

What is a Monotonic Stack?

# Monotonic Decreasing Stack (most common):
# Elements from bottom to top are in decreasing order.
# When a larger element arrives, pop smaller elements.
#
# Processing array [2, 1, 5, 6, 2, 3]:
#
#   Push 2:  stack = [2]
#   Push 1:  stack = [2, 1]     (1 < 2, maintain decreasing)
#   Push 5:  pop 1, pop 2       (5 > 1 and 5 > 2)
#            stack = [5]
#   Push 6:  pop 5              (6 > 5)
#            stack = [6]
#   Push 2:  stack = [6, 2]     (2 < 6, maintain decreasing)
#   Push 3:  pop 2              (3 > 2)
#            stack = [6, 3]
#
# Key insight: Each element is pushed and popped at most once -> O(n)

Problem 1: Next Greater Element I (LC 496)

Problem: Given two arrays nums1 (subset) and nums2, find the next greater element for each element in nums1 within nums2. Return -1 if no greater element exists.

# nums1 = [4, 1, 2], nums2 = [1, 3, 4, 2]
#
# For each element in nums2, find its next greater:
#   1 -> next greater is 3
#   3 -> next greater is 4
#   4 -> no next greater = -1
#   2 -> no next greater = -1
#
# Answer for nums1: [-1, 3, -1]
#
# Stack trace on nums2 = [1, 3, 4, 2]:
#   i=0, val=1: push 1             stack: [1]
#   i=1, val=3: pop 1 -> map[1]=3  stack: []
#               push 3             stack: [3]
#   i=2, val=4: pop 3 -> map[3]=4  stack: []
#               push 4             stack: [4]
#   i=3, val=2: push 2             stack: [4, 2]
#   End: remaining have no next greater

def nextGreaterElement(nums1, nums2):
    """Find next greater element using monotonic stack.

    Build a map: element -> its next greater in nums2.
    Then look up each element of nums1.

    Time:  O(n + m) where n=len(nums1), m=len(nums2)
    Space: O(m) for the map and stack
    """
    next_greater = {}
    stack = []

    for num in nums2:
        # Pop elements smaller than current
        while stack and stack[-1] < num:
            next_greater[stack.pop()] = num
        stack.append(num)

    # Remaining elements have no next greater
    return [next_greater.get(num, -1) for num in nums1]

Problem 2: Largest Rectangle in Histogram (LC 84)

Problem: Given an array of bar heights, find the largest rectangle area in the histogram.

# Heights: [2, 1, 5, 6, 2, 3]
#
# Visual histogram:
#            ___
#        ___|   |
#       |   |   |       ___
#  ___  |   |   |  ___ |   |
# |   | |   |   | |   ||   |
# | 2 | | 5 | 6 | | 2 || 3 |
# |___|_|___|___|_|___|_|___|
#   0   1   2   3   4   5
#
# Largest rectangle: height=5, width=2 (indices 2-3) = 10
#
# Stack approach: maintain increasing heights.
# When a shorter bar arrives, calculate areas for taller bars.
#
# Stack trace (indices):
#   i=0, h=2: push 0                   stack: [0]
#   i=1, h=1: pop 0, area=2*1=2        stack: []
#             push 1                    stack: [1]
#   i=2, h=5: push 2                   stack: [1, 2]
#   i=3, h=6: push 3                   stack: [1, 2, 3]
#   i=4, h=2: pop 3, area=6*1=6        stack: [1, 2]
#             pop 2, area=5*2=10        stack: [1]
#             push 4                    stack: [1, 4]
#   i=5, h=3: push 5                   stack: [1, 4, 5]
#   End: pop 5, area=3*1=3             stack: [1, 4]
#        pop 4, area=2*3=6             stack: [1]
#        pop 1, area=1*6=6             stack: []
#   Max area = 10

def largestRectangleArea(heights):
    """Find largest rectangle in histogram.

    Use a stack of indices. For each bar, when we find a
    shorter bar, pop and calculate the area using the
    popped bar's height and the width between boundaries.

    Time:  O(n) - each bar pushed/popped once
    Space: O(n) - stack size
    """
    stack = []  # stores indices
    max_area = 0
    heights.append(0)  # sentinel to flush remaining bars

    for i in range(len(heights)):
        while stack and heights[i] < heights[stack[-1]]:
            h = heights[stack.pop()]
            # Width: from stack top (left boundary) to i (right boundary)
            w = i if not stack else i - stack[-1] - 1
            max_area = max(max_area, h * w)

        stack.append(i)

    heights.pop()  # remove sentinel
    return max_area

Problem 3: Maximal Rectangle (LC 85)

Problem: Given a 2D binary matrix filled with '0' and '1', find the largest rectangle containing only '1's.

# Matrix:
#   ["1","0","1","0","0"]
#   ["1","0","1","1","1"]
#   ["1","1","1","1","1"]
#   ["1","0","0","1","0"]
#
# Key insight: Treat each row as the base of a histogram.
# Build histogram heights row by row:
#
# Row 0: [1, 0, 1, 0, 0]  -> largest rect = 1
# Row 1: [2, 0, 2, 1, 1]  -> largest rect = 3
# Row 2: [3, 1, 3, 2, 2]  -> largest rect = 6  <-- answer
# Row 3: [4, 0, 0, 3, 0]  -> largest rect = 4
#
# For each row, apply "Largest Rectangle in Histogram"

def maximalRectangle(matrix):
    """Find largest rectangle of 1s in a binary matrix.

    Build histogram heights for each row, then apply
    the largest rectangle in histogram algorithm.

    Time:  O(rows * cols) - histogram per row
    Space: O(cols) - heights array
    """
    if not matrix or not matrix[0]:
        return 0

    cols = len(matrix[0])
    heights = [0] * cols
    max_area = 0

    for row in matrix:
        # Update histogram heights
        for j in range(cols):
            if row[j] == '1':
                heights[j] += 1
            else:
                heights[j] = 0

        # Apply largest rectangle in histogram
        max_area = max(max_area, largestRectangleArea(heights[:]))

    return max_area

Problem 4: Online Stock Span (LC 901)

Problem: Design a class that collects daily stock prices and returns the span (number of consecutive days the price was less than or equal to today's price, including today).

# Prices:  [100, 80, 60, 70, 60, 75, 85]
# Spans:   [1,   1,  1,  2,  1,  4,  6]
#
# Day 0: price=100, no previous day         span=1
# Day 1: price=80,  80 < 100                span=1
# Day 2: price=60,  60 < 80                 span=1
# Day 3: price=70,  70 > 60                 span=2 (days 2-3)
# Day 4: price=60,  60 < 70                 span=1
# Day 5: price=75,  75 > 60, 75 > 70, 75 > 60  span=4 (days 2-5)
# Day 6: price=85,  85 > 75, 85 > 80        span=6 (days 1-6)
#
# Stack stores (price, span) pairs.
# When current price >= stack top, pop and accumulate span.

class StockSpanner:
    """Calculate stock span using a monotonic stack.

    Stack stores (price, span) tuples in decreasing price order.
    When a new price is >= stack top, pop and add their spans.

    Each call: O(1) amortized time
    Space: O(n) total for n calls
    """
    def __init__(self):
        self.stack = []  # (price, span)

    def next(self, price):
        span = 1

        # Pop all prices <= current price
        while self.stack and self.stack[-1][0] <= price:
            span += self.stack.pop()[1]

        self.stack.append((price, span))
        return span

Problem 5: Sum of Subarray Minimums (LC 907)

Problem: Given an array of integers, find the sum of min(subarray) for every contiguous subarray. Return the answer modulo 10^9 + 7.

# Input: arr = [3, 1, 2, 4]
#
# All subarrays and their minimums:
#   [3]       -> min = 3
#   [3,1]     -> min = 1
#   [3,1,2]   -> min = 1
#   [3,1,2,4] -> min = 1
#   [1]       -> min = 1
#   [1,2]     -> min = 1
#   [1,2,4]   -> min = 1
#   [2]       -> min = 2
#   [2,4]     -> min = 2
#   [4]       -> min = 4
#   Sum = 3+1+1+1+1+1+1+2+2+4 = 17
#
# Key insight: For each element arr[i], count how many
# subarrays have arr[i] as their minimum.
#
# For arr[i], find:
#   left[i]  = distance to previous smaller element
#   right[i] = distance to next smaller-or-equal element
#
# Number of subarrays where arr[i] is min = left[i] * right[i]
# Contribution = arr[i] * left[i] * right[i]

def sumSubarrayMins(arr):
    """Sum of minimums of all subarrays.

    Use two monotonic stacks to find:
    - Previous Less Element (PLE) distance for each index
    - Next Less-or-Equal Element (NLE) distance for each index

    Contribution of arr[i] = arr[i] * left[i] * right[i]

    Time:  O(n) - two passes with monotonic stacks
    Space: O(n) - stack and arrays
    """
    MOD = 10**9 + 7
    n = len(arr)
    left = [0] * n   # distance to previous smaller
    right = [0] * n  # distance to next smaller or equal
    stack = []

    # Find previous less element distances
    for i in range(n):
        while stack and arr[stack[-1]] >= arr[i]:
            stack.pop()
        left[i] = i - stack[-1] if stack else i + 1
        stack.append(i)

    # Find next less or equal element distances
    stack = []
    for i in range(n - 1, -1, -1):
        while stack and arr[stack[-1]] > arr[i]:
            stack.pop()
        right[i] = stack[-1] - i if stack else n - i
        stack.append(i)

    # Calculate total sum
    result = 0
    for i in range(n):
        result = (result + arr[i] * left[i] * right[i]) % MOD

    return result

Complexity Summary

ProblemTimeSpaceStack Type
Next Greater ElementO(n)O(n)Decreasing (pop on greater)
Largest RectangleO(n)O(n)Increasing (pop on shorter)
Maximal RectangleO(r×c)O(c)Histogram per row
Stock SpanO(1) amort.O(n)Decreasing (pop on >=)
Subarray MinimumsO(n)O(n)Two increasing stacks
Pattern Recognition: If a problem asks about "next greater," "next smaller," "span," or "subarray min/max," think monotonic stack. The key decision is whether to use an increasing or decreasing stack. Increasing stacks find "next smaller"; decreasing stacks find "next greater."