Skip to content

Largest Rectangle in Histogram

LeetCode 84 | Difficulty: Hard | Pattern: Monotonic Stack


Problem Statement

Given an array of integers heights representing the histogram's bar height where the width of each bar is 1, return the area of the largest rectangle in the histogram.

Examples

Example 1:

Input: heights = [2,1,5,6,2,3]
Output: 10
Explanation: The largest rectangle has area = 10 units (height=5, width=2)
     6
   5 _
   _| |
  |   | 2 3
 2| 1 |_|_|_|
 _|_|_|_|_|_|
 0 1 2 3 4 5

Example 2:

Input: heights = [2,4]
Output: 4

Constraints

  • 1 <= heights.length <= 10^5
  • 0 <= heights[i] <= 10^4

Intuition

For each bar, the largest rectangle with that bar as the minimum height extends:

  • Left: until we hit a shorter bar
  • Right: until we hit a shorter bar

Key Insight: Use a monotonic increasing stack. When we find a shorter bar, all taller bars in the stack have found their right boundary. Pop them and calculate their rectangle areas.


Visualization

Largest Rectangle in Histogram

For heights = [2, 1, 5, 6, 2, 3]:

Monotonic Increasing Stack tracks indices where heights can extend right.

When we see height 2 at index 4:
- Pop height 6 (index 3): width = 4-2-1 = 1, area = 6*1 = 6
- Pop height 5 (index 2): width = 4-1-1 = 2, area = 5*2 = 10  <- MAX!

This means the rectangle of height 5 extends from index 2 to 3.

Solution: Monotonic Stack

python
def largestRectangleArea(heights: list[int]) -> int:
    """
    Find largest rectangle using monotonic increasing stack.

    Stack stores indices. Pop when we find a shorter bar.
    The popped bar's rectangle extends from stack top to current index.

    Time: O(n)
    Space: O(n)
    """
    stack = []  # Monotonic increasing stack of indices
    max_area = 0
    n = len(heights)

    for i in range(n + 1):
        # Use height 0 as sentinel to flush remaining bars
        h = heights[i] if i < n else 0

        while stack and heights[stack[-1]] > h:
            height = heights[stack.pop()]
            # Width: from stack top (left boundary) to current i (right boundary)
            width = i if not stack else i - stack[-1] - 1
            max_area = max(max_area, height * width)

        stack.append(i)

    return max_area
java
public int largestRectangleArea(int[] heights) {
    Deque<Integer> stack = new ArrayDeque<>(); // monotonic increasing stack of indices
    int maxArea = 0;
    int n = heights.length;

    for (int i = 0; i <= n; i++) {
        int h = (i < n) ? heights[i] : 0; // sentinel height 0

        while (!stack.isEmpty() && heights[stack.peek()] > h) {
            int height = heights[stack.pop()];
            int width = stack.isEmpty() ? i : i - stack.peek() - 1;
            maxArea = Math.max(maxArea, height * width);
        }

        stack.push(i);
    }

    return maxArea;
}

Complexity: Time O(n) · Space O(n)

  • Time: Each index is pushed onto the stack exactly once and popped at most once, giving 2n total operations
  • Space: The stack stores indices; in the worst case (strictly increasing heights), all n indices are stored simultaneously

Why This Works

For each bar at index i that we pop:

  • Right boundary: Current index (the shorter bar that caused the pop)
  • Left boundary: The index below it in the stack (last bar shorter than it on the left)
  • Width: right - left - 1
Stack: [1, 2, 3] (heights: 1, 5, 6)
Current: index 4, height 2

Pop index 3 (height 6):
  Right boundary: 4
  Left boundary: 2 (stack top after pop)
  Width: 4 - 2 - 1 = 1
  Area: 6 * 1 = 6

Pop index 2 (height 5):
  Right boundary: 4
  Left boundary: 1 (stack top after pop)
  Width: 4 - 1 - 1 = 2
  Area: 5 * 2 = 10

Step-by-Step Walkthrough

For heights = [2, 1, 5, 6, 2, 3]:

Initial: stack = [], max_area = 0

i=0, h=2:
  Stack empty, push 0
  stack = [0]

i=1, h=1:
  heights[0]=2 > 1, pop 0
  height = 2, width = 1 (stack empty, width = i = 1)
  area = 2 * 1 = 2, max_area = 2
  Push 1
  stack = [1]

i=2, h=5:
  heights[1]=1 < 5, push 2
  stack = [1, 2]

i=3, h=6:
  heights[2]=5 < 6, push 3
  stack = [1, 2, 3]

i=4, h=2:
  heights[3]=6 > 2, pop 3
  height = 6, width = 4 - 2 - 1 = 1
  area = 6, max_area = 6

  heights[2]=5 > 2, pop 2
  height = 5, width = 4 - 1 - 1 = 2
  area = 10, max_area = 10

  heights[1]=1 < 2, push 4
  stack = [1, 4]

i=5, h=3:
  heights[4]=2 < 3, push 5
  stack = [1, 4, 5]

i=6 (sentinel h=0):
  Pop 5: height=3, width=6-4-1=1, area=3
  Pop 4: height=2, width=6-1-1=4, area=8
  Pop 1: height=1, width=6, area=6

Final max_area = 10

Alternative: Two-Pass Approach

python
def largestRectangleArea(heights: list[int]) -> int:
    """
    Find left and right boundaries separately, then calculate areas.

    Time: O(n)
    Space: O(n)
    """
    n = len(heights)

    # For each bar, find index of first shorter bar on left
    left_boundary = [-1] * n
    stack = []
    for i in range(n):
        while stack and heights[stack[-1]] >= heights[i]:
            stack.pop()
        left_boundary[i] = stack[-1] if stack else -1
        stack.append(i)

    # For each bar, find index of first shorter bar on right
    right_boundary = [n] * n
    stack = []
    for i in range(n - 1, -1, -1):
        while stack and heights[stack[-1]] >= heights[i]:
            stack.pop()
        right_boundary[i] = stack[-1] if stack else n
        stack.append(i)

    # Calculate max area
    max_area = 0
    for i in range(n):
        width = right_boundary[i] - left_boundary[i] - 1
        max_area = max(max_area, heights[i] * width)

    return max_area

Complexity: Time O(n) · Space O(n)

  • Time: Two separate O(n) passes to compute left and right boundaries, plus one O(n) pass to compute areas = O(3n) = O(n)
  • Space: Three arrays of size n (left_boundary, right_boundary, and stack at any time) = O(3n) = O(n)

Complexity Analysis

ApproachTimeSpace
Brute ForceO(n^2)O(1)
Monotonic StackO(n)O(n)
Two-PassO(n)O(n)
Divide & ConquerO(n log n)O(log n)

Why O(n)?

  • Each index is pushed once and popped once
  • Total operations: 2n = O(n)

Edge Cases

python
def test_largest_rectangle():
    # Single bar
    assert largestRectangleArea([5]) == 5

    # All same height
    assert largestRectangleArea([3, 3, 3, 3]) == 12

    # Increasing
    assert largestRectangleArea([1, 2, 3, 4]) == 6  # 2*3 or 3*2

    # Decreasing
    assert largestRectangleArea([4, 3, 2, 1]) == 6

    # Valley
    assert largestRectangleArea([3, 1, 3]) == 3

    # Peak
    assert largestRectangleArea([1, 3, 1]) == 3

    # Contains zero
    assert largestRectangleArea([2, 0, 2]) == 2

Common Mistakes

  1. Forgetting the sentinel

    python
    # Without sentinel, bars remaining in stack aren't processed
    # Must iterate to n (inclusive) with h=0 sentinel
  2. Wrong width calculation

    python
    # When stack is empty after pop:
    width = i  # Not i - (-1) - 1
  3. Using >= instead of >

    python
    # For equal heights, we want to keep them in stack
    # Pop only when strictly greater

Applications

1. Maximal Rectangle in Binary Matrix (LeetCode 85)

python
def maximalRectangle(matrix: list[list[str]]) -> int:
    """
    Treat each row as base of histogram.
    Heights accumulate for consecutive 1s.
    """
    if not matrix:
        return 0

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

    for row in matrix:
        for i in range(n):
            # Update heights: reset to 0 if '0', else add 1
            heights[i] = heights[i] + 1 if row[i] == '1' else 0

        max_area = max(max_area, largestRectangleArea(heights))

    return max_area

Complexity: Time O(m * n) · Space O(n)

  • Time: For each of m rows, we update n heights in O(n) and run the histogram algorithm in O(n), giving O(m * n) total
  • Space: The heights array stores n values, and the stack within largestRectangleArea uses O(n) space

2. Maximum Score of a Good Subarray (LeetCode 1793)

Similar histogram approach with additional constraint on including index k.


Interview Tips

What Interviewers Look For

  1. Monotonic Stack Pattern: Recognize when to use it
  2. Boundary Calculation: Understand left and right boundaries
  3. Edge Cases: Handle empty stack, equal heights, zeros

Common Follow-up Questions

  1. "Explain why the stack is monotonic increasing"

    • Decreasing elements have found their right boundary
    • We need to track potential left boundaries
  2. "What if we want the rectangle with maximum width instead of area?"

    • Find minimum height in range, binary search or segment tree
  3. "How would you handle negative heights?"

    • Shift all heights by minimum value
  4. "Can you do it with O(1) extra space?"

    • No, need to track left/right boundaries somehow

Maximal Rectangle (LeetCode 85)

Problem: Given a binary matrix, find the largest rectangle containing only 1s.

Key Insight: Treat each row as the base of a histogram. Heights accumulate for consecutive 1s, reset on 0s.

Approach: For each row, build histogram heights, then apply Largest Rectangle in Histogram algorithm.

Complexity: O(m * n) time, O(n) space

Trapping Rain Water (LeetCode 42)

Problem: Given elevation map, compute how much water can be trapped after raining.

Key Insight: Water at position i = min(left_max, right_max) - height[i]. Can use two pointers or monotonic stack.

Approach: Two pointer: move pointer with smaller max toward center, calculate water as you go.

Complexity: O(n) time, O(1) space (two pointer)

Container With Most Water (LeetCode 11)

Problem: Given heights array, find two lines that form container holding most water.

Key Insight: Area = min(height[i], height[j]) * (j - i). Move pointer of shorter line inward.

Approach: Greedy two pointers. Width decreases, so only move shorter line hoping for taller one.

Complexity: O(n) time, O(1) space

Maximum Score of Good Subarray (LeetCode 1793)

Problem: Find max score of subarray containing index k, where score = min(subarray) * length.

Key Insight: Similar to histogram problem but must include index k. Expand from k while tracking minimum.

Approach: Two pointers starting at k. Expand toward larger neighbor to maximize minimum.

Complexity: O(n) time, O(1) space


References