LeetCode 84 - Largest Rectangle in Histogram
This problem asks us to find the largest rectangular area that can be formed inside a histogram. A histogram is represented as an array of integers called heights, where each integer describes the height of a bar, and every bar has a width of exactly 1.
Difficulty: 🔴 Hard
Topics: Array, Stack, Monotonic Stack
Solution
LeetCode 84 - Largest Rectangle in Histogram
Problem Understanding
This problem asks us to find the largest rectangular area that can be formed inside a histogram. A histogram is represented as an array of integers called heights, where each integer describes the height of a bar, and every bar has a width of exactly 1.
The goal is to determine the maximum possible area of any rectangle that can be formed using one or more adjacent bars. A rectangle does not need to span the entire histogram. Instead, it can extend across any continuous range of bars, but its height is limited by the shortest bar within that range.
For example, if we have:
heights = [2,1,5,6,2,3]
We might consider the bars [5,6]. Since the shortest height in that segment is 5, we can create a rectangle of height 5 and width 2, producing an area of:
5 × 2 = 10
This turns out to be the largest rectangle in the histogram.
The input is an integer array where:
heights[i]represents the height of thei-thbar.- Every bar has width
1. - Bars are placed next to each other with no gaps.
The expected output is a single integer representing the maximum rectangle area possible.
The constraints are important:
1 <= heights.length <= 10^5
0 <= heights[i] <= 10^4
The array size can be as large as 100,000, which immediately tells us that expensive nested-loop approaches may be too slow. An O(n²) or O(n³) solution risks timing out on large inputs. This strongly suggests that we need a linear or near-linear solution.
Several edge cases can trip up naive implementations. Histograms with increasing heights, decreasing heights, or repeated equal heights behave differently and often reveal logical mistakes. Arrays containing zeros are also important because a height of 0 effectively breaks a rectangle. A histogram with only one bar is another special case where the answer is simply that bar’s height.
The problem guarantees that the input array is non-empty and contains non-negative integers, so we never need to handle invalid input or negative heights.
Approaches
Brute Force Approach
A straightforward way to solve this problem is to examine every possible contiguous subarray of bars.
For each starting index, we can expand toward the right while keeping track of the minimum height encountered so far. Since the rectangle height is limited by the smallest bar in the range, the area for every segment becomes:
minimum height × width
We compute this for all possible ranges and track the maximum area.
This approach works because it explicitly checks every valid rectangle. Since every possible contiguous region is evaluated, the maximum area found must be correct.
However, it is too slow for the given constraints. There are O(n²) possible intervals, and while we can optimize minimum tracking to avoid recomputation, the overall complexity still becomes O(n²). With n = 100,000, this is impractical.
Optimal Approach, Monotonic Increasing Stack
The key insight is that for every bar, we want to determine:
- How far it can extend to the left
- How far it can extend to the right
before encountering a shorter bar.
If we know these boundaries, we can calculate the maximum rectangle where this bar is the limiting height.
For a bar at index i:
area = height[i] × width
where:
width = right_boundary - left_boundary - 1
Instead of explicitly searching left and right for every bar, which would be expensive, we use a monotonic increasing stack.
The stack stores indices of bars in increasing height order. Whenever we encounter a shorter bar, we know that the taller bars on the stack cannot extend further. This gives us enough information to compute their maximum rectangle immediately.
This is efficient because every element is pushed and popped at most once, giving an O(n) solution.
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(n²) | O(1) | Check every contiguous range and compute rectangle areas |
| Optimal | O(n) | O(n) | Monotonic increasing stack processes each bar once |
Algorithm Walkthrough
Optimal Algorithm Using a Monotonic Stack
- Initialize an empty stack and a variable
max_area = 0.
The stack will store indices of histogram bars in increasing order of height. We store indices instead of values because we need positions to calculate widths later. 2. Iterate through all bars in the histogram.
For each bar, compare its height with the bar at the top of the stack. 3. Maintain the increasing property of the stack.
If the current bar is taller than or equal to the top of the stack, push its index onto the stack.
This preserves the increasing height order. 4. When a shorter bar appears, pop taller bars.
If the current height is smaller than the height of the bar on top of the stack, we know that the taller bar cannot continue beyond this point.
We repeatedly pop from the stack and calculate areas. 5. Calculate the rectangle for the popped bar.
After popping:
- The current index gives the right boundary.
- The new top of the stack gives the left boundary.
- If the stack becomes empty, the rectangle extends to the beginning.
The width becomes:
width = current_index - stack[-1] - 1
or:
width = current_index
if the stack is empty.
Then calculate:
area = popped_height × width
Update max_area.
6. Continue processing until all bars are examined.
7. Add a sentinel value.
To ensure all remaining bars are processed, append a virtual height 0 at the end. This forces the stack to empty naturally.
8. Return max_area.
Why it works
The key invariant is that the stack always contains indices of bars in increasing height order. When a shorter bar appears, we immediately know the right boundary for every taller bar that must be removed. Since the stack also preserves information about previous smaller elements, we automatically know the left boundary as well.
Every bar is processed exactly once as a potential limiting height for a rectangle, guaranteeing that the maximum rectangle is never missed.
Python Solution
from typing import List
class Solution:
def largestRectangleArea(self, heights: List[int]) -> int:
stack = []
max_area = 0
# Add sentinel height to process remaining bars
heights.append(0)
for index, height in enumerate(heights):
while stack and heights[stack[-1]] > height:
top_index = stack.pop()
current_height = heights[top_index]
if stack:
width = index - stack[-1] - 1
else:
width = index
area = current_height * width
max_area = max(max_area, area)
stack.append(index)
return max_area
The implementation starts by initializing an empty stack and a variable to track the maximum area.
A sentinel value 0 is appended to the array. This is an elegant trick that ensures all remaining bars in the stack are processed at the end without requiring separate cleanup logic.
During iteration, the stack maintains increasing heights. Whenever the current bar is shorter than the top of the stack, we repeatedly pop taller bars because they can no longer extend further.
For each popped bar, the current index acts as the right boundary. The new top of the stack gives the left boundary. This allows us to calculate the width and therefore the area.
After processing necessary pops, the current index is added to the stack so the increasing order property remains intact.
Finally, the maximum rectangle area is returned.
Go Solution
func largestRectangleArea(heights []int) int {
stack := []int{}
maxArea := 0
// Add sentinel value
heights = append(heights, 0)
for i, height := range heights {
for len(stack) > 0 && heights[stack[len(stack)-1]] > height {
topIndex := stack[len(stack)-1]
stack = stack[:len(stack)-1]
currentHeight := heights[topIndex]
var width int
if len(stack) > 0 {
width = i - stack[len(stack)-1] - 1
} else {
width = i
}
area := currentHeight * width
if area > maxArea {
maxArea = area
}
}
stack = append(stack, i)
}
return maxArea
}
The Go implementation follows exactly the same logic as the Python version. The primary difference is stack handling. In Go, slices are used as stacks, with append() for push operations and slicing for pops.
Go does not have Python's built-in max() function, so maximum area updates are performed with an explicit comparison.
There are no special concerns about integer overflow because the maximum possible rectangle area is:
10^5 × 10^4 = 10^9
which fits comfortably inside Go's int type.
Worked Examples
Example 1
heights = [2,1,5,6,2,3]
After appending sentinel:
[2,1,5,6,2,3,0]
| Index | Height | Stack Before | Action | Area Calculated | Max Area |
|---|---|---|---|---|---|
| 0 | 2 | [] | Push 0 | - | 0 |
| 1 | 1 | [0] | Pop 2 | 2 × 1 = 2 | 2 |
| 1 | 1 | [] | Push 1 | - | 2 |
| 2 | 5 | [1] | Push 2 | - | 2 |
| 3 | 6 | [1,2] | Push 3 | - | 2 |
| 4 | 2 | [1,2,3] | Pop 6 | 6 × 1 = 6 | 6 |
| 4 | 2 | [1,2] | Pop 5 | 5 × 2 = 10 | 10 |
| 4 | 2 | [1] | Push 4 | - | 10 |
| 5 | 3 | [1,4] | Push 5 | - | 10 |
| 6 | 0 | [1,4,5] | Pop 3 | 3 × 1 = 3 | 10 |
| 6 | 0 | [1,4] | Pop 2 | 2 × 4 = 8 | 10 |
| 6 | 0 | [1] | Pop 1 | 1 × 6 = 6 | 10 |
Final answer:
10
The largest rectangle comes from bars [5,6], where height 5 spans width 2.
Example 2
heights = [2,4]
After appending sentinel:
[2,4,0]
| Index | Height | Stack Before | Action | Area Calculated | Max Area |
|---|---|---|---|---|---|
| 0 | 2 | [] | Push 0 | - | 0 |
| 1 | 4 | [0] | Push 1 | - | 0 |
| 2 | 0 | [0,1] | Pop 4 | 4 × 1 = 4 | 4 |
| 2 | 0 | [0] | Pop 2 | 2 × 2 = 4 | 4 |
Final answer:
4
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(n) | Every bar is pushed and popped at most once |
| Space | O(n) | The stack may store all indices in the worst case |
The linear time complexity comes from the fact that each element enters and leaves the stack only once. Even though there is a nested while loop, the total number of pop operations across the entire execution is bounded by n.
The space complexity is O(n) because, in the worst case of strictly increasing heights, all indices remain in the stack simultaneously.
Test Cases
solution = Solution()
assert solution.largestRectangleArea([2, 1, 5, 6, 2, 3]) == 10 # Example 1
assert solution.largestRectangleArea([2, 4]) == 4 # Example 2
assert solution.largestRectangleArea([5]) == 5 # Single bar
assert solution.largestRectangleArea([0]) == 0 # Single zero
assert solution.largestRectangleArea([1, 2, 3, 4, 5]) == 9 # Increasing heights
assert solution.largestRectangleArea([5, 4, 3, 2, 1]) == 9 # Decreasing heights
assert solution.largestRectangleArea([2, 2, 2, 2]) == 8 # All equal heights
assert solution.largestRectangleArea([0, 0, 0]) == 0 # All zeros
assert solution.largestRectangleArea([2, 1, 2]) == 3 # Valley shape
assert solution.largestRectangleArea([4, 2, 0, 3, 2, 5]) == 6 # Mixed heights
assert solution.largestRectangleArea([10000] * 100000) == 1000000000 # Stress test
| Test | Why |
|---|---|
[2,1,5,6,2,3] |
Verifies the primary example |
[2,4] |
Verifies small input |
[5] |
Single-element histogram |
[0] |
Minimum possible height |
[1,2,3,4,5] |
Increasing sequence stresses stack growth |
[5,4,3,2,1] |
Decreasing sequence stresses repeated popping |
[2,2,2,2] |
Equal heights test width expansion |
[0,0,0] |
All bars have zero height |
[2,1,2] |
Valley shape tests boundary logic |
[4,2,0,3,2,5] |
Mixed pattern with reset points |
[10000] * 100000 |
Maximum-size stress case |
Edge Cases
Single Bar Histogram
A histogram containing only one bar, such as:
[5]
can be easy to mishandle if the implementation assumes multiple boundaries exist. Since there is only one possible rectangle, the answer is simply the height itself. The algorithm handles this naturally because the sentinel value forces the single bar to be popped and evaluated.
Strictly Increasing Heights
An input like:
[1,2,3,4,5]
is tricky because no rectangle gets computed during the main iteration. Every bar is simply pushed onto the stack. Without proper cleanup, the algorithm would miss the answer entirely.
The sentinel 0 solves this issue by triggering pops for all remaining bars at the end.
Strictly Decreasing Heights
For input:
[5,4,3,2,1]
every new bar causes immediate popping. This can expose bugs in width calculation because boundaries change rapidly.
The implementation correctly computes widths by using the new top of the stack after each pop. This guarantees the left boundary is always accurate.
Histograms with Zero Heights
An input like:
[2,0,2]
contains a zero-height bar that effectively splits the histogram into separate sections.
The algorithm handles this naturally because a height of 0 forces all taller bars to be popped, preventing rectangles from incorrectly spanning across the zero.