Skip to content

Coding patternsBlind 75

Container With Most Water

Two lines hold water up to the shorter one. Solved two ways, brute force in O(n²) and two pointers in O(n), with a step-through, code in five languages and the Big O worked out from the code.

You are given an array height of n numbers. Each number is a vertical line: line i stands at position i and is height[i] tall.

Choose any two lines. Together with the floor, they form a container. Return the largest amount of water a container can hold. The container cannot be tilted, so the water stops at the shorter of the two lines.

Example 1

Input: height = [2, 5, 4, 1, 6, 3]

Output: 15

The lines of height 5 and 6 stand 3 positions apart. The water stops at the shorter one, so they hold 3 × 5 = 15.

height2051421364353 × 5 = 15

Example 2

Input: height = [3, 3]

Output: 3

There is only one pair. It is 1 position wide and 3 tall.

Example 3

Input: height = [4, 0, 0, 4]

Output: 12

The two outer lines are both 4 tall and 3 positions apart, so they hold 3 × 4 = 12. The zeros in between do not block the water.

  • 2 ≤ n ≤ 100,000
  • 0 ≤ height[i] ≤ 10,000
Is the array sorted?
No. And we must not sort it: sorting moves the lines and changes the distances.
Can a height be 0?
Yes. A line of height 0 holds no water. The formula already gives 0, so no special case.
Can we use the same line twice?
No. We need two different lines, so the width is always at least 1.
Do we return the lines or the amount?
Only the amount. If several pairs tie, it does not matter which one we find.
Can the answer overflow?
No. The largest answer is 10,000 × 99,999, just under 1,000,000,000. That fits in a 32-bit integer.

We pick a pair from an array, and the answer depends on both ends and the distance between them. That is the clue for two pointers: start one pointer at each end and move them toward each other. The hard part is knowing which pointer is safe to move.

This problem has two approaches. There is no useful step between them.

Optimal O(n)

Start with the widest container: the first and the last line. Every other container is narrower, so it can only win by being taller.

Now look at the shorter of the two lines. The water can never rise above it. If we keep it and move the other line inward, the container gets narrower and no taller. So the shorter line can never be part of a bigger container. We drop it and move that pointer inward. Then we measure again.

Why it never misses the answer: we only drop a line after showing that every container it could still make is no bigger than one we already measured.

height205142136435▲ left▲ right
best
0
  1. left, right = 0, len(height) - 1
  2. best = 0
  3. while left < right:
  4. water = min(height[left], height[right]) * (right - left)
  5. best = max(best, water)
  6. if height[left] < height[right]:
  7. left += 1
  8. else:
  9. right -= 1
  10. return best

Start with the widest container: the first line and the last line.

1 / 12
Solution.javasolution.pySolution.cssolution.gosolution.ts
class Solution {
// Two pointers: start with the widest container and drop the
// shorter line each step. Each line is dropped once, so n steps.
public int maxArea(int[] height) {
// The widest container. Any other one is narrower,
// so it can only win by being taller.
int left = 0, right = height.length - 1;
int best = 0;
while (left < right) {
// Water stops at the shorter line, so it sets the height.
int water = Math.min(height[left], height[right]) * (right - left);
best = Math.max(best, water);
// Drop the shorter line. Any container that still uses it
// is narrower and no taller, so it can never beat this one.
if (height[left] < height[right]) left++;
else right--;
}
return best;
}
}
Time O(n)Space O(1)Here is how we get these from the code, in four steps.
  1. 1 Find the work that repeats

    Each run of the loop measures one pair and moves one pointer. That is the same small job every time, so each run counts as 1 step. There is only one loop, and nothing inside it loops again.

    1. left, right = 0, len(height) - 1
    2. best = 0
    3. while left < right:
    4. water = min(height[left], height[right]) * (right - left)
    5. best = max(best, water)
    6. if height[left] < height[right]:
    7. left += 1
    8. else:
    9. right -= 1
    10. return best
  2. 2 Count the runs on Example 1

    Example 1 has 6 lines. The loop runs while left < right. The gap between them starts at 5. Every run moves one pointer inward, so the gap shrinks by exactly 1. It reaches 0 after 5 runs, and the loop stops.

    5 runs.

    run 1run 2run 3run 4run 5stop
    left011234
    right554444
    gap543210
  3. 3 Write the count for any n

    The gap starts at n − 1 and shrinks by 1 on every run, so the loop runs exactly:

    n − 1 times

    Check it with Example 1: 6 − 1 = 5.

  4. 4 Keep only the part that grows fastest

    Drop the − 1. One run more or less does not change how the work grows. When n doubles, n − 1 about doubles too. What is left is n.

    So the time is O(n).

  5. S Space: count the extra memory the same way

    The code creates left, right, water and best. That is 4 variables, whatever n is, so the space is O(1).

    leftrightwaterbest

2,000 random arrays of 2 to 12 heights, with many ties and zeros. The brute force and the two pointers gave the same answer every time.

Largest inputsResult
100,000 heights (the most allowed), each up to 10,000. Two pointers.0.01 s, and under 0.001 MB of extra memory: just the 4 variables.
2,000 heights. Brute force, about 2 million pairs.0.2 s. At 100,000 heights it would check 5 billion pairs, which is why the two pointers matter.