Trapping Rain Water (LeetCode
Learn how to solve Trapping Rain Water using brute force, prefix/suffix arrays, and the optimal two-pointer approach. Includes intuition, dry runs, Java code, correctness proof, complexity analysis, edge cases, interview tips, and production applications.
Introduction
Trapping Rain Water (LeetCode #42) is one of the most celebrated hard-level problems in coding interviews.
It is frequently asked at companies like:
- Amazon
- Microsoft
- Meta
- Apple
- Netflix
- Uber
- Goldman Sachs
Interviewers use this problem to evaluate whether a candidate can:
- Reason about water trapped between barriers
- Optimize from O(n²) brute force to O(n) with prefix arrays
- Further optimize to O(n) time and O(1) space using Two Pointers
- Explain the invariant behind the Two Pointer approach convincingly
The key insight is:
The water above any bar is determined by the minimum of the maximum height to its left and the maximum height to its right, minus its own height.
For each index i:
water[i] = max(0, min(leftMax[i], rightMax[i]) - height[i])
The Two Pointer approach computes this without storing the full prefix and suffix arrays, by maintaining running maximums from both ends.
Problem Statement
Given an array height of n non-negative integers representing an elevation map where the width of each bar is 1, compute how much water it can trap after raining.
Return the total units of trapped water.
Formula
Water trapped above each bar:
water[i] = max(0, min(leftMax[i], rightMax[i]) - height[i])
Total water:
sum of water[i] for all i
Example 1
Input
height = [0, 1, 0, 2, 1, 0, 1, 3, 2, 1, 2, 1]
Output
6
Explanation:
The elevation map traps 6 units of rain water.
Between index 2 and index 7, water is trapped at several positions.
Example 2
Input
height = [4, 2, 0, 3, 2, 5]
Output
9
Explanation:
index 1: min(4, 5) - 2 = 2
index 2: min(4, 5) - 0 = 4
index 3: min(4, 5) - 3 = 1
index 4: min(4, 5) - 2 = 2
Total = 2 + 4 + 1 + 2 = 9
Example 3
Input
height = [3, 0, 2, 0, 4]
Output
7
Explanation:
index 1: min(3, 4) - 0 = 3
index 2: min(3, 4) - 2 = 1
index 3: min(3, 4) - 0 = 3
Total = 3 + 1 + 3 = 7
Important Problem Guarantees
The LeetCode problem guarantees:
n >= 10 <= height[i] <= 100,000- No negative heights.
- A single bar or two bars trap no water — valid water requires at least one bar on each side.
Core Intuition
Imagine standing at any bar.
Water can only be trapped above that bar if there is a taller (or equal) bar to both its left and its right.
The water level above that bar is:
min(tallest bar to the left, tallest bar to the right)
The actual water above the bar is that level minus the bar's own height.
If the bar is taller than the water level, no water is trapped (result is 0).
graph TD
Bar_at_index_i["Bar at index i"] --> Water_level_min_leftMax_righ["Water level = min(leftMax, rightMax)"]
Water_level_min_leftMax_righ["Water level = min(leftMax, rightMax)"] --> Water_trapped_max_0_waterLev["Water trapped = max(0, waterLevel - height(i))"]
Approach 1 — Brute Force
Core Idea
For each bar at index i:
- Scan left to find the maximum height to the left.
- Scan right to find the maximum height to the right.
- Compute the water trapped above this bar.
Sum all contributions.
Java Code — Brute Force
public class TrappingRainWaterBruteForce {
public int trap(int[] height) {
int totalWater = 0;
int n = height.length;
for (int i = 1; i < n - 1; i++) {
int leftMax = 0;
for (int left = 0; left <= i; left++) {
leftMax = Math.max(leftMax, height[left]);
}
int rightMax = 0;
for (int right = i; right < n; right++) {
rightMax = Math.max(rightMax, height[right]);
}
int waterAbove =
Math.min(leftMax, rightMax)
- height[i];
totalWater += waterAbove;
}
return totalWater;
}
}
Brute Force Complexity
| Complexity | Value |
|---|---|
| Time | O(n²) |
| Space | O(1) |
For each of n bars, two inner scans each up to n steps.
At LeetCode constraints (n up to 100,000), this exceeds the time limit.
Why Brute Force Is Inefficient
For:
n = 100,000
the number of inner scan steps is approximately:
100,000 × 100,000 = 10 billion operations
The left and right maximums for every index are recomputed from scratch each time, even though adjacent indices share most of the scan.
Approach 2 — Prefix and Suffix Arrays
Core Idea
Precompute:
leftMax[i]— maximum height from index0toi(inclusive).rightMax[i]— maximum height from indexiton-1(inclusive).
Then compute trapped water in a single pass.
Algorithm
Build leftMax array:
leftMax[0] = height[0]
leftMax[i] = max(leftMax[i-1], height[i])
Build rightMax array:
rightMax[n-1] = height[n-1]
rightMax[i] = max(rightMax[i+1], height[i])
For each i:
water[i] = max(0, min(leftMax[i], rightMax[i]) - height[i])
Return sum of water[]
Java Code — Prefix and Suffix Arrays
public class TrappingRainWaterPrefixSuffix {
public int trap(int[] height) {
int n = height.length;
if (n < 3) return 0;
int[] leftMax = new int[n];
int[] rightMax = new int[n];
leftMax[0] = height[0];
for (int i = 1; i < n; i++) {
leftMax[i] = Math.max(leftMax[i - 1], height[i]);
}
rightMax[n - 1] = height[n - 1];
for (int i = n - 2; i >= 0; i--) {
rightMax[i] = Math.max(rightMax[i + 1], height[i]);
}
int totalWater = 0;
for (int i = 1; i < n - 1; i++) {
int waterLevel =
Math.min(leftMax[i], rightMax[i]);
totalWater +=
Math.max(0, waterLevel - height[i]);
}
return totalWater;
}
}
Prefix/Suffix Complexity
| Complexity | Value |
|---|---|
| Time | O(n) |
| Space | O(n) |
Three linear passes and two arrays of size n.
This is the most intuitive O(n) approach, but uses O(n) space.
Why Not Always Use Prefix Arrays?
The prefix/suffix approach is correct but uses two extra arrays.
For:
n = 100,000,000 integers
two arrays of int require:
2 × 100,000,000 × 4 bytes = 800 MB
This is unacceptable in memory-constrained environments.
The Two Pointer approach achieves the same O(n) time with O(1) space.
Approach 3 — Optimal Two Pointer Solution
Core Idea
Instead of precomputing all left and right maximums, maintain two running variables:
int leftMax = 0;
int rightMax = 0;
Use two pointers — one starting from the left, one from the right — converging inward.
At each step, process the side with the smaller maximum.
If leftMax <= rightMax:
water[left] = leftMax - height[left] (safe: right side is >= leftMax)
left++
Else:
water[right] = rightMax - height[right] (safe: left side is >= rightMax)
right--
Why This Is Safe
When leftMax <= rightMax:
The water above height[left] is bounded by leftMax (the smaller side).
We do not need to know the exact rightMax beyond the current position —
we already know it is at least as tall as leftMax.
So water[left] = max(0, leftMax - height[left]) is exact.
When rightMax < leftMax:
The water above height[right] is bounded by rightMax (the smaller side).
We already know the left side is at least as tall as rightMax.
So water[right] = max(0, rightMax - height[right]) is exact.
This is the key invariant that makes the Two Pointer approach correct.
Algorithm
graph TD
left_0["left = 0"] --> right_n_1["right = n - 1"]
right_n_1["right = n - 1"] --> leftMax_0["leftMax = 0"]
leftMax_0["leftMax = 0"] --> rightMax_0["rightMax = 0"]
rightMax_0["rightMax = 0"] --> total_0["total = 0"]
total_0["total = 0"] --> Loop_while_left_right["Loop while left right:"]
Loop_while_left_right["Loop while left right:"] --> height_left_height_right["height(left) = height(right)"]
height_left_height_right["height(left) = height(right)"] --> Yes["Yes →"]
Yes["Yes →"] --> leftMax_max_leftMax_height_l["leftMax = max(leftMax, height(left))"]
leftMax_max_leftMax_height_l["leftMax = max(leftMax, height(left))"] --> total_max_0_leftMax_height_l["total += max(0, leftMax - height(left))"]
total_max_0_leftMax_height_l["total += max(0, leftMax - height(left))"] --> left["left++"]
left["left++"] --> No["No →"]
No["No →"] --> rightMax_max_rightMax_height["rightMax = max(rightMax, height(right))"]
rightMax_max_rightMax_height["rightMax = max(rightMax, height(right))"] --> total_max_0_rightMax_height_["total += max(0, rightMax - height(right))"]
total_max_0_rightMax_height_["total += max(0, rightMax - height(right))"] --> right["right--"]
right["right--"] --> Return_total["Return total"]
Java Code — Optimal Two Pointer Solution
public class TrappingRainWater {
public int trap(int[] height) {
int left = 0;
int right = height.length - 1;
int leftMax = 0;
int rightMax = 0;
int total = 0;
while (left < right) {
if (height[left] <= height[right]) {
leftMax = Math.max(leftMax, height[left]);
total += leftMax - height[left];
left++;
} else {
rightMax = Math.max(rightMax, height[right]);
total += rightMax - height[right];
right--;
}
}
return total;
}
}
LeetCode-Style Implementation
public class Solution {
public int trap(int[] height) {
int left = 0;
int right = height.length - 1;
int lMax = 0;
int rMax = 0;
int water = 0;
while (left < right) {
if (height[left] <= height[right]) {
lMax = Math.max(lMax, height[left]);
water += lMax - height[left];
left++;
} else {
rMax = Math.max(rMax, height[right]);
water += rMax - height[right];
right--;
}
}
return water;
}
}
Two Pointer Flow
graph TD
Initialize_left_0_right_n_1["Initialize left = 0, right = n-1"] --> leftMax_0_rightMax_0_total_0["leftMax = 0, rightMax = 0, total = 0"]
leftMax_0_rightMax_0_total_0["leftMax = 0, rightMax = 0, total = 0"] --> left_right["left right?"]
left_right["left right?"] --> Yes["Yes ↓"]
Yes["Yes ↓"] --> height_left_height_right["height(left) = height(right)?"]
height_left_height_right["height(left) = height(right)?"] --> Yes1["Yes →"]
Yes1["Yes →"] --> Update_leftMax["Update leftMax"]
Update_leftMax["Update leftMax"] --> Add_leftMax_height_left_to_t["Add (leftMax - height(left)) to total"]
Add_leftMax_height_left_to_t["Add (leftMax - height(left)) to total"] --> left["left++"]
left["left++"] --> No["No →"]
No["No →"] --> Update_rightMax["Update rightMax"]
Update_rightMax["Update rightMax"] --> Add_rightMax_height_right_to["Add (rightMax - height(right)) to total"]
Add_rightMax_height_right_to["Add (rightMax - height(right)) to total"] --> right["right--"]
right["right--"] --> Repeat["Repeat"]
Detailed Dry Run
Input:
height = [0, 1, 0, 2, 1, 0, 1, 3, 2, 1, 2, 1]
Initial state:
left = 0
right = 11
leftMax = 0
rightMax = 0
total = 0
Iteration 1
height[left] = 0
height[right] = 1
0 <= 1 → process left
leftMax = max(0, 0) = 0
water = 0 - 0 = 0
total = 0
left = 1
Iteration 2
height[left] = 1
height[right] = 1
1 <= 1 → process left
leftMax = max(0, 1) = 1
water = 1 - 1 = 0
total = 0
left = 2
Iteration 3
height[left] = 0
height[right] = 1
0 <= 1 → process left
leftMax = max(1, 0) = 1
water = 1 - 0 = 1
total = 1
left = 3
Iteration 4
height[left] = 2
height[right] = 1
2 > 1 → process right
rightMax = max(0, 1) = 1
water = 1 - 1 = 0
total = 1
right = 10
Iteration 5
height[left] = 2
height[right] = 2
2 <= 2 → process left
leftMax = max(1, 2) = 2
water = 2 - 2 = 0
total = 1
left = 4
Iteration 6
height[left] = 1
height[right] = 2
1 <= 2 → process left
leftMax = max(2, 1) = 2
water = 2 - 1 = 1
total = 2
left = 5
Iteration 7
height[left] = 0
height[right] = 2
0 <= 2 → process left
leftMax = max(2, 0) = 2
water = 2 - 0 = 2
total = 4
left = 6
Iteration 8
height[left] = 1
height[right] = 2
1 <= 2 → process left
leftMax = max(2, 1) = 2
water = 2 - 1 = 1
total = 5
left = 7
Iteration 9
height[left] = 3
height[right] = 2
3 > 2 → process right
rightMax = max(1, 2) = 2
water = 2 - 2 = 0
total = 5
right = 9
Iteration 10
height[left] = 3
height[right] = 1
3 > 1 → process right
rightMax = max(2, 1) = 2
water = 2 - 1 = 1
total = 6
right = 8
left = 7, right = 8 → left < right → one more step.
Iteration 11
height[left] = 3
height[right] = 2
3 > 2 → process right
rightMax = max(2, 2) = 2
water = 2 - 2 = 0
total = 6
right = 7
left = 7, right = 7 → loop ends.
Return:
6
Dry Run Table
| left | right | h[L] | h[R] | lMax | rMax | water added | total |
|---|---|---|---|---|---|---|---|
| 0 | 11 | 0 | 1 | 0 | 0 | 0 | 0 |
| 1 | 11 | 1 | 1 | 1 | 0 | 0 | 0 |
| 2 | 11 | 0 | 1 | 1 | 0 | 1 | 1 |
| 3 | 11 | 2 | 1 | 1 | 1 | 0 | 1 |
| 3 | 10 | 2 | 2 | 2 | 1 | 0 | 1 |
| 4 | 10 | 1 | 2 | 2 | 1 | 1 | 2 |
| 5 | 10 | 0 | 2 | 2 | 1 | 2 | 4 |
| 6 | 10 | 1 | 2 | 2 | 1 | 1 | 5 |
| 7 | 10 | 3 | 2 | 2 | 2 | 0 | 5 |
| 7 | 9 | 3 | 1 | 2 | 2 | 1 | 6 |
| 7 | 8 | 3 | 2 | 2 | 2 | 0 | 6 |
Return 6.
Visual Representation
height = [0, 1, 0, 2, 1, 0, 1, 3, 2, 1, 2, 1]
index 0 1 2 3 4 5 6 7 8 9 10 11
|
3 | |
2 | | | | | |
1 | | | | | | | | |
0 +--+--+--+--+--+--+--+--+--+--+--+--
Water trapped (shown as ~):
|
3 | |
2 | | ~~ | | | |
1 | | ~~ | ~~ | | | |
0 +--+--+--+--+--+--+--+--+--+--+--+--
Total = 6 units
Why the Two Pointer Approach Is Correct
Claim: When height[left] <= height[right], the water above height[left] is exactly leftMax - height[left].
Proof:
The water level above any bar is min(leftMax, rightMax).
When height[left] <= height[right]:
The right pointer has seen a bar of height >= height[left].
rightMax >= height[right] >= height[left] >= leftMax (since leftMax is built incrementally).
Therefore: min(leftMax, rightMax) = leftMax.
Water above left = leftMax - height[left].
No matter what bars lie between left and right, the right side is already proven to be at least as tall as leftMax.
The symmetric argument holds when height[right] < height[left].
Correctness Proof
Invariant:
At every step:
- Every index processed on the left has its exact water contribution computed.
- Every index processed on the right has its exact water contribution computed.
- The optimal pair
[left, right]is always within the current window.
Base case: Before the loop, leftMax = 0 and rightMax = 0. No water has been processed yet.
Inductive step:
- When
height[left] <= height[right]: the minimum boundary forleftisleftMax(proven above). The contributionleftMax - height[left]is exact.leftis advanced. - When
height[right] < height[left]: the minimum boundary forrightisrightMax(symmetric argument). The contributionrightMax - height[right]is exact.rightis advanced.
Termination: At each step, either left or right advances. The loop terminates when left == right.
Conclusion: All bars have been processed with exact water contributions. The total is correct.
Loop Invariant
At every iteration:
total = sum of exact water contributions
for all indices already processed
leftMax = max height seen from the left so far
rightMax = max height seen from the right so far
The unprocessed interval is [left, right].
When the loop ends:
left == right
All indices have been processed.
total is the answer.
Complexity Analysis
| Complexity | Value |
|---|---|
| Time | O(n) |
| Space | O(1) |
Each pointer moves at most n - 1 steps.
Only four integer variables are maintained — no arrays.
Why the Complexity Is Not O(n²)
The two pointers converge from both ends.
At each iteration, exactly one pointer advances.
Total iterations:
right - left ≤ n - 1 → O(n)
There is no nested scan.
Approach Comparison
| Approach | Time | Space | Notes |
|---|---|---|---|
| Brute Force | O(n²) | O(1) | Recomputes max for every bar |
| Prefix/Suffix Arrays | O(n) | O(n) | Three passes, two extra arrays |
| Two Pointer | O(n) | O(1) | Optimal — single pass, running maximums |
Production-Ready Implementation
import java.util.Objects;
public class TrappingRainWater {
/**
* Computes the total water trapped between bars.
*
* @param height non-null array of non-negative integers
* @return total units of trapped water
* @throws NullPointerException if height is null
*/
public int trap(int[] height) {
Objects.requireNonNull(height,
"height must not be null");
if (height.length < 3) {
return 0;
}
int left = 0;
int right = height.length - 1;
int leftMax = 0;
int rightMax = 0;
int total = 0;
while (left < right) {
if (height[left] <= height[right]) {
leftMax = Math.max(leftMax, height[left]);
total += leftMax - height[left];
left++;
} else {
rightMax = Math.max(rightMax, height[right]);
total += rightMax - height[right];
right--;
}
}
return total;
}
}
Edge Cases
Empty or Single Bar
Input
height = []
height = [5]
Output
0
No water can be trapped without at least one bar on each side.
Two Bars
Input
height = [3, 5]
Output
0
No bar between two walls — nothing to trap water above.
All Same Height (Flat)
Input
height = [3, 3, 3, 3]
Output
0
Water level equals bar height at every position. No water trapped.
Monotonically Increasing
Input
height = [1, 2, 3, 4, 5]
Output
0
The right wall is always taller. The left wall is always the constraint — but there is no depression to hold water.
Monotonically Decreasing
Input
height = [5, 4, 3, 2, 1]
Output
0
No bar can hold water — every bar's right side has no taller wall.
Single Valley
Input
height = [3, 0, 3]
Output
3
index 1: min(3, 3) - 0 = 3
Large Valley
Input
height = [4, 2, 0, 3, 2, 5]
Output
9
Left wall is 4, right wall is 5. Water fills from index 1 to 4.
Bars of Zero Height
Input
height = [0, 0, 0, 0]
Output
0
No walls — no water trapped.
Asymmetric Walls
Input
height = [1, 0, 5]
Output
1
index 1: min(1, 5) - 0 = 1
The shorter left wall limits the water level.
Common Interview Mistakes
Mistake 1 — Using height[left] as leftMax Without Updating
❌ total += height[left] - height[left]; // always 0
leftMax must track the running maximum, not the current height.
Mistake 2 — Not Taking max(0, ...) for Water
If a bar is taller than leftMax or rightMax:
leftMax - height[left] could be negative
In the Two Pointer approach, updating leftMax = max(leftMax, height[left]) before subtracting guarantees the result is always non-negative.
No explicit max(0, ...) is needed because leftMax >= height[left] always holds after the update.
Mistake 3 — Processing Both Pointers in the Same Iteration
❌ left++;
right--;
Only one pointer should move per iteration, depending on which side has the smaller maximum.
Mistake 4 — Starting Inner Scans at 0 and n-1 (Brute Force)
The first and last bars can never trap water — they have no wall on one side.
Skip indices 0 and n-1 in the brute force scan.
Mistake 5 — Confusing With Container With Most Water
Container With Most Water:
Area = min(height[left], height[right]) × width
Goal: maximize this area
Trapping Rain Water:
Water above bar i = min(leftMax, rightMax) - height[i]
Goal: sum of water above all bars
Different formula, different goal, different pointer movement logic.
Mistake 6 — Claiming O(n) for the Brute Force
The brute force has two nested loops — each bar scans all bars to its left and right.
Total inner scan steps ≈ n²
Mistake 7 — Not Handling Arrays Shorter Than 3
An array of length 1 or 2 cannot trap any water. Always return 0 early.
Mistake 8 — Updating leftMax After Adding Water
❌ total += leftMax - height[left];
leftMax = Math.max(leftMax, height[left]);
leftMax must be updated before computing the water contribution.
Correct:
leftMax = Math.max(leftMax, height[left]);
total += leftMax - height[left];
Follow-Up 1 — What Is the Maximum Possible Water?
Given constraints height[i] ≤ 100,000 and n ≤ 100,000:
Max water per bar ≈ 100,000
Max bars ≈ 100,000
Max total water ≈ 10^10
This exceeds int range (~2.1 × 10^9).
Use long for the total in production code to be safe:
long total = 0;
Follow-Up 2 — Return the Water Trapped Above Each Bar
Instead of summing, collect per-bar contributions:
public int[] waterPerBar(int[] height) {
int n = height.length;
int[] leftMax = new int[n];
int[] rightMax = new int[n];
int[] water = new int[n];
leftMax[0] = height[0];
for (int i = 1; i < n; i++) {
leftMax[i] = Math.max(leftMax[i - 1], height[i]);
}
rightMax[n - 1] = height[n - 1];
for (int i = n - 2; i >= 0; i--) {
rightMax[i] = Math.max(rightMax[i + 1], height[i]);
}
for (int i = 0; i < n; i++) {
water[i] = Math.max(
0,
Math.min(leftMax[i], rightMax[i]) - height[i]);
}
return water;
}
This uses the prefix/suffix approach for per-bar output.
Follow-Up 3 — What If Height Values Can Be Very Large?
Use long for intermediate computations:
long total = 0;
leftMax = Math.max(leftMax, height[left]);
total += (long) (leftMax - height[left]);
This prevents integer overflow when water volumes are large.
Follow-Up 4 — Solve Using a Monotonic Stack
The stack approach processes bars as they are encountered:
Maintain a stack of indexes in decreasing height order.
When a taller bar is encountered:
Pop the shorter bar (the valley floor).
Compute water between the new bar and the bar now on top of the stack.
Add to total.
Time → O(n)
Space → O(n)
The stack approach is more complex to implement but useful when you need to also identify which specific bars form each water pocket.
Follow-Up 5 — Streaming or Online Input
The Two Pointer approach requires the full array in memory.
For a streaming case, use the prefix/suffix approach with a two-pass algorithm:
Pass 1 (left to right): Build leftMax.
Pass 2 (right to left): Compute water using rightMax on the fly.
Or buffer the full stream before processing.
Production Applications
The rain water trapping principle appears in real engineering systems.
Examples include:
- Histogram-based capacity analysis (e.g., buffer pool, memory allocation)
- Network traffic shaping — identifying bandwidth valleys between peaks
- Reservoir simulation and watershed modelling
- Terrain analysis for drainage and flood modelling
- Financial chart analysis — identifying consolidation zones between price peaks
- CPU utilization gap analysis between load spikes
- Cloud auto-scaling — identifying compute capacity valleys in time-series data
The core insight — the water level at any point is constrained by the minimum of the two surrounding maximums — generalizes to many constraint-based capacity problems.
Real-World Example — Memory Buffer Pool Analysis
A memory allocator tracks free block sizes over time.
The "trapped water" analogy maps to available contiguous memory between large allocations.
public long analyseBufferCapacity(int[] blockSizes) {
if (blockSizes == null
|| blockSizes.length < 3) {
return 0L;
}
int left = 0;
int right = blockSizes.length - 1;
int leftMax = 0;
int rightMax = 0;
long total = 0L;
while (left < right) {
if (blockSizes[left]
<= blockSizes[right]) {
leftMax = Math.max(
leftMax, blockSizes[left]);
total += leftMax - blockSizes[left];
left++;
} else {
rightMax = Math.max(
rightMax, blockSizes[right]);
total += rightMax - blockSizes[right];
right--;
}
}
return total;
}
Real-World Example — Terrain Drainage Volume
Given elevation data as an array, compute the total volume of water retained after heavy rainfall:
elevation = [3, 1, 2, 4, 0, 1, 3, 2]
Total retained volume = trap(elevation)
This is directly the Trapping Rain Water problem applied to terrain data.
Senior-Level Discussion — Why Does This Work Without Storing Both Arrays?
The prefix/suffix approach requires both leftMax[i] and rightMax[i] to be known simultaneously for each bar.
The Two Pointer approach avoids this by using a critical observation:
When we process the left side, we only need leftMax.
We do not need the exact rightMax value because we already know:
height[right] >= height[left]
therefore rightMax >= height[right] >= leftMax
therefore min(leftMax, rightMax) = leftMax
The decision of which pointer to move gives us the exact water contribution without needing the other side's maximum.
This is a lazy evaluation strategy — compute exactly what you need, when you need it.
Senior-Level Discussion — Is This Dynamic Programming?
The prefix/suffix approach is a form of dynamic programming — it builds two tables bottom-up.
The Two Pointer approach is a greedy algorithm — it makes a locally optimal decision at each step (process the side with the smaller maximum) and provably produces the globally optimal answer.
Senior-Level Discussion — Can the Stack Approach Beat Two Pointers?
No.
Both the stack and two-pointer approaches achieve O(n) time.
The stack uses O(n) space; the two-pointer uses O(1) space.
The two-pointer approach is strictly better in space complexity.
The stack is preferred only when you need to identify the specific bar pairs forming each water pocket (for debugging or visualization).
Interview Explanation
A strong interview walkthrough:
graph TD
N_1_0_0["1"] --> N_2_1_1["2"]
N_1_0_0["1"] --> Walk_1_2["Walk"]
Explain_0_1["Explain"] --> through_1_3["through"]
Explain_0_1["Explain"] --> brute_1_4["brute"]
the_0_2["the"] --> force_1_5["force"]
the_0_2["the"] --> and_1_6["and"]
per_bar_0_3["per-bar"] --> explain_1_7["explain"]
per_bar_0_3["per-bar"] --> why_1_8["why"]
water_0_4["water"] --> it_1_9["it"]
water_0_4["water"] --> is_1_10["is"]
formula_0_5["formula"] --> O_1_11["O"]
formula_0_5["formula"] --> n_1_12["n²"]
N_2_1_1["2"] --> N_3_2_1["3"]
N_2_1_1["2"] --> Explain_2_2["Explain"]
Walk_1_2["Walk"] --> the_2_3["the"]
Walk_1_2["Walk"] --> prefix_2_4["prefix"]
through_1_3["through"] --> suffix_2_5["suffix"]
through_1_3["through"] --> optimization_2_6["optimization"]
brute_1_4["brute"] --> to_2_7["to"]
brute_1_4["brute"] --> O_2_8["O"]
force_1_5["force"] --> n_2_9["n"]
force_1_5["force"] --> time_2_10["time"]
and_1_6["and"] --> O_2_11["O"]
and_1_6["and"] --> n_2_12["n"]
explain_1_7["explain"] --> space_2_13["space"]
N_3_2_1["3"] --> N_4_3_1["4"]
N_3_2_1["3"] --> Explain_3_2["Explain"]
Explain_2_2["Explain"] --> why_3_3["why"]
Explain_2_2["Explain"] --> the_3_4["the"]
the_2_3["the"] --> Two_3_5["Two"]
the_2_3["the"] --> Pointer_3_6["Pointer"]
prefix_2_4["prefix"] --> approach_3_7["approach"]
prefix_2_4["prefix"] --> can_3_8["can"]
suffix_2_5["suffix"] --> avoid_3_9["avoid"]
suffix_2_5["suffix"] --> the_3_10["the"]
optimization_2_6["optimization"] --> extra_3_11["extra"]
optimization_2_6["optimization"] --> arrays_3_12["arrays"]
N_4_3_1["4"] --> N_5_4_1["5"]
N_4_3_1["4"] --> State_4_2["State"]
Explain_3_2["Explain"] --> the_4_3["the"]
Explain_3_2["Explain"] --> key_4_4["key"]
why_3_3["why"] --> invariant_4_5["invariant"]
why_3_3["why"] --> when_4_6["when"]
the_3_4["the"] --> height_4_7["height"]
the_3_4["the"] --> left_4_8["left"]
Two_3_5["Two"] --> height_4_9["height"]
Two_3_5["Two"] --> right_4_10["right"]
N_5_4_1["5"] --> the_5_1["the"]
N_5_4_1["5"] --> water_5_2["water"]
State_4_2["State"] --> level_5_3["level"]
State_4_2["State"] --> at_5_4["at"]
the_4_3["the"] --> left_5_5["left"]
the_4_3["the"] --> is_5_6["is"]
key_4_4["key"] --> exactly_5_7["exactly"]
key_4_4["key"] --> leftMax_5_8["leftMax"]
the_5_1["the"] --> N_6_6_1["6"]
the_5_1["the"] --> Walk_6_2["Walk"]
water_5_2["water"] --> through_6_3["through"]
water_5_2["water"] --> the_6_4["the"]
level_5_3["level"] --> dry_6_5["dry"]
level_5_3["level"] --> run_6_6["run"]
at_5_4["at"] --> step_6_7["step"]
at_5_4["at"] --> by_6_8["by"]
left_5_5["left"] --> step_6_9["step"]
N_6_6_1["6"] --> N_7_7_1["7"]
N_6_6_1["6"] --> State_7_2["State"]
Walk_6_2["Walk"] --> O_7_3["O"]
Walk_6_2["Walk"] --> n_7_4["n"]
through_6_3["through"] --> time_7_5["time"]
through_6_3["through"] --> and_7_6["and"]
the_6_4["the"] --> O_7_7["O"]
the_6_4["the"] --> N_1_7_8["1"]
dry_6_5["dry"] --> space_7_9["space"]
N_7_7_1["7"] --> N_8_8_1["8"]
N_7_7_1["7"] --> Mention_8_2["Mention"]
State_7_2["State"] --> edge_8_3["edge"]
State_7_2["State"] --> cases_8_4["cases"]
O_7_3["O"] --> fewer_8_5["fewer"]
O_7_3["O"] --> than_8_6["than"]
n_7_4["n"] --> N_3_8_7["3"]
n_7_4["n"] --> bars_8_8["bars"]
time_7_5["time"] --> flat_8_9["flat"]
time_7_5["time"] --> array_8_10["array"]
and_7_6["and"] --> single_8_11["single"]
and_7_6["and"] --> valley_8_12["valley"]
Interview Tips
Interviewers often ask:
- What is the water formula for a single bar?
- Why does the Two Pointer approach not need both leftMax and rightMax simultaneously?
- What is the loop invariant?
- How does this differ from Container With Most Water?
- Why start with the prefix/suffix approach before the Two Pointer?
- What is the space complexity of each approach?
- What happens when all bars are the same height?
- When would you prefer the stack approach over Two Pointer?
- How do you handle integer overflow for very large inputs?
- Can this be solved in a single pass from left to right only?
Be prepared to explain why moving the shorter-side pointer is safe before writing code.
Similar Problems
- LeetCode #11 — Container With Most Water — Maximize area; one rectangle; Two Pointer greedy.
- LeetCode #84 — Largest Rectangle in Histogram — Related bar-based area problem; uses a monotonic stack.
- LeetCode #407 — Trapping Rain Water II — 3D version; uses a min-heap (priority queue) BFS approach.
Trapping Rain Water (1D) and Container With Most Water are both Two Pointer problems but with fundamentally different formulas and goals:
| Problem | Formula | Goal |
|---|---|---|
| Container With Most Water | min(h[L], h[R]) × width |
Maximize |
| Trapping Rain Water | sum of min(leftMax, rightMax) - h[i] |
Sum all |
Unit Tests
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class TrappingRainWaterTest {
final TrappingRainWater solution =
new TrappingRainWater();
@Test
void example1() {
assertEquals(6,
solution.trap(
new int[]{0, 1, 0, 2, 1, 0, 1, 3, 2, 1, 2, 1}));
}
@Test
void example2() {
assertEquals(9,
solution.trap(
new int[]{4, 2, 0, 3, 2, 5}));
}
@Test
void example3() {
assertEquals(7,
solution.trap(
new int[]{3, 0, 2, 0, 4}));
}
@Test
void singleBar() {
assertEquals(0,
solution.trap(new int[]{5}));
}
@Test
void twoBars() {
assertEquals(0,
solution.trap(new int[]{3, 5}));
}
@Test
void flatArray() {
assertEquals(0,
solution.trap(new int[]{3, 3, 3, 3}));
}
@Test
void monotonicallyIncreasing() {
assertEquals(0,
solution.trap(new int[]{1, 2, 3, 4, 5}));
}
@Test
void monotonicallyDecreasing() {
assertEquals(0,
solution.trap(new int[]{5, 4, 3, 2, 1}));
}
@Test
void singleValley() {
assertEquals(3,
solution.trap(new int[]{3, 0, 3}));
}
@Test
void asymmetricWalls() {
assertEquals(1,
solution.trap(new int[]{1, 0, 5}));
}
@Test
void allZeros() {
assertEquals(0,
solution.trap(new int[]{0, 0, 0, 0}));
}
@Test
void nullInputReturnsZero() {
assertEquals(0,
solution.trap(null));
}
}
Summary
Trapping Rain Water is the canonical problem for understanding how two pointers can replace precomputed prefix and suffix arrays. The brute-force O(n²) approach recomputes left and right maximums for every bar. The prefix/suffix approach brings this to O(n) time but requires O(n) space. The Two Pointer approach achieves O(n) time and O(1) space by maintaining running maximums and processing whichever side has the smaller current maximum — because the other side is guaranteed to be at least as tall, making the water contribution exact without needing both maximums simultaneously. Mastering this problem provides the intuition for Trapping Rain Water II, Largest Rectangle in Histogram, and many other bar-based problems.
Key Takeaways
- Water above each bar equals
max(0, min(leftMax, rightMax) - height[i]). - Brute force is O(n²) — avoid recomputing maximums per bar.
- Prefix/suffix arrays give O(n) time but cost O(n) space.
- Two Pointer achieves O(n) time and O(1) space with running maximums.
- Process the side with the smaller current height — it determines the water level.
- Update
leftMaxorrightMaxbefore computing water — not after. - When
height[left] <= height[right], the water atleftis exactlyleftMax - height[left]. - Arrays shorter than 3 trap no water — return
0early. - Use
longfor the total when heights or array lengths are very large. - Distinguish this from Container With Most Water — different formula, different goal, different pointer logic.