How to solve the container with most water problem on LeetCode
Imagine standing on the Cahill Expressway lookout in Sydney, watching the blue of the harbour squeeze between two piers. The wider the gap and the deeper the walls, the more water the inlet seems to hold. That visual is exactly the intuition behind LeetCode's famous container with most water problem: given a series of vertical lines drawn on a number line, pick the two that trap the largest possible volume between them and the x-axis.
The puzzle looks deceptively simple at first glance. Each line has a fixed height and a fixed position, and the container's volume equals the distance between the two chosen lines multiplied by the smaller of the two heights. The catch is that there is no obvious shortcut, and a naive approach runs straight into a wall of time-limit-exceeded errors once the input grows past a few hundred elements.
This is the kind of problem Australian engineering students often tackle during late-night sessions in university libraries around Melbourne and Brisbane, or while commuting on the Sydney Metro between stops at Central and Redfern. It also appears regularly in coding interviews for roles at Atlassian, Canva, and the big banks in Barangaroo, so practising it pays off well beyond the leaderboard.
The good news is that the optimal solution requires only a handful of lines and runs in linear time. The rest of this article walks through the problem statement, the brute force baseline, the elegant two-pointer insight, and a clean Python implementation.
Reading the problem statement carefully
The prompt gives an array of non-negative integers where each value represents the height of a vertical line located at its index. The task is to choose two indices i and j such that the area (j - i) * min(height[i], height[j]) is maximised. The output is a single integer.
It is worth pausing on the word "container". Water cannot climb the walls; it rises only as high as the shorter of the two sides. This single observation is the key to the entire solution, because it tells us that the taller line is the only one that can ever contribute more area after we move a pointer.
Another subtle point is that the array is not sorted, nor is it required to be. Sorting the heights would change the positions of the lines and therefore the distances between them, which would change the problem entirely. The two lines must remain at their original indices, even when the order of heights looks irregular.
For Australian readers preparing for take-home tests, this is also a good moment to remember the Privacy Act and the principles of the Australian Cyber Security Centre: when you share screenshots of your working solution in a public Slack channel, scrub out the test cases first.
The brute force approach and why it stalls
The most direct mental model is to compare every possible pair of lines and keep track of the largest area found. In pseudocode, that means something resembling for i in range(n): for j in range(i+1, n): update best. The total cost is roughly n * (n-1) / 2 operations.
For an array of ten thousand elements, that is almost fifty million comparisons. Most online judges, including LeetCode's standard harness, will accept this on a warm afternoon in Adelaide but reject it under heavier load. Even when it passes, the runtime reveals nothing about the elegance of the structure hiding inside the data.
Brute force is still useful, however. It is the right place to start when you are writing pseudocode on a whiteboard in a Canva office or sketching on the back of a tram ticket. It confirms your understanding of the metric and gives you a baseline to beat. A quick implementation in plain Python looks like:
def max_area_brute(heights):
best = 0
for i in range(len(heights)):
for j in range(i + 1, len(heights)):
best = max(best, (j - i) * min(heights[i], heights[j]))
return best
That is enough to verify the logic; it is not enough to call the problem solved.
Why the two-pointer technique works
The smarter approach starts with one pointer at each end of the array and walks them towards the centre. At every step we compute the current area, update the maximum, then move the pointer that points to the shorter line. The reasoning is that the area is limited by the shorter height, so keeping it fixed cannot improve the result.
This greedy step is what makes the algorithm linear. Each pointer moves at most n times in total, so the total number of operations is bounded by a small constant times n. There is no recursion, no extra arrays, no sorting, just two indices and a running maximum.
The technique is sometimes called the "left-right squeeze" in Australian bootcamp circles, where mentors in Surry Hills and South Bank regularly walk cohorts through it before tackling harder sliding-window problems. It also relies on the same instinct used in other tutorials on the same site: when you cannot improve on a constraint, abandon it and pivot to a less constrained choice.
It is worth double-checking the edge cases. If all heights are equal, the widest pair gives the answer. If the array is monotonically increasing or decreasing, the answer comes from the first and last element.
A step-by-step walkthrough
Let us trace through a small example so the movement of the pointers feels natural. Suppose the heights are [1, 8, 6, 2, 5, 4, 8, 3, 7]. The indices run from 0 to 8. Start with left = 0 (height 1) and right = 8 (height 7). The width is 8 and the limiting height is 1, so the area is 8. The shorter line is on the left, so we move left to index 1.
Now left = 1 (height 8) and right = 8 (height 7). Width 7, limiting height 7, area 49. This is our new best. The shorter line is now on the right, so we move right down to 7. Continue in the same fashion: each comparison updates the best, and each move drops the smaller of the two heights. The maximum area we eventually compute is 49, achieved by the pair (1, 8).
A clean pseudocode sketch captures the loop in a few lines:
left, right = 0, len(heights) - 1
best = 0
while left < right:
width = right - left
height = min(heights[left], heights[right])
best = max(best, width * height)
if heights[left] < heights[right]:
left += 1
else:
right -= 1
return best
That is the entire algorithm, and the same skeleton reappears in problems about trapping rainwater, comparing string widths, and sliding-window variants used in take-home tests for fintechs in Sydney's CBD.
Putting it into practice with Python
For readers who want a fully working script, the move from pseudocode to idiomatic Python is short. The only real decision is whether to use a while loop or a for loop with a sentinel; both are fine.
A tested implementation looks like this:
def max_area(height):
left, right = 0, len(height) - 1
best = 0
while left < right:
best = max(best, (right - left) * min(height[left], height[right]))
if height[left] < height[right]:
left += 1
else:
right -= 1
return best
If you are new to Python and want to build solid foundations, the Python programming tutorials on hello ML are a sensible place to start. They cover exactly the kind of detail that separates a clean submission from one that crashes on an off-by-one error.
When you run the function against the example above, the answer is 49. Against an array of two elements such as [1, 1], the answer is 1. Against an empty list, the function returns 0 without raising an exception, which is the behaviour LeetCode expects.
Complexity, variations and where to take it next
The two-pointer solution runs in O(n) time and O(1) auxiliary space. That is the best you can do for this specific problem, because any correct algorithm must examine at least one of the heights to be sure of the answer, and the linear scan covers each element at most twice.
It is helpful to keep a small checklist of debugging questions ready whenever you revisit the algorithm:
- Have I handled the empty list and the single-element list?
- Does the inner comparison use the smaller of the two heights, not the larger?
- Does my loop terminate, or can
leftandrightcross without stopping? - Did I update
bestbefore moving the pointer, not after? - Did I cast the width to the right integer type in statically typed languages?
- Did I cover the case where the array contains zeros?
Variations worth exploring once the basic solution feels automatic include the 3D version, the trapped-rainwater problem, and several interview staples that combine the two-pointer idea with a sliding window. Each of these will reinforce the muscle memory built here.
A second useful list compares the approaches you have just seen:
- Brute force:
O(n²)time,O(1)space, easy to write, slow on large inputs. - Two-pointer:
O(n)time,O(1)space, requires the greedy insight, scales well. - Sorted array trick:
O(n log n)time if you sort heights with their original indices. - Divide and conquer: possible but offers no advantage over two pointers.
- Library call: most languages do not ship a "max water" function, so write your own.
If you would like to see how a similar pattern shows up in scheduling, the editorial walk-through of the critical path method on the same site is a surprisingly rewarding detour. And for the curious reader who wants to know who keeps the tutorials current, the about page lists the contributors behind hello ML.
Mastering this problem is less about memorising a snippet and more about internalising the argument that justifies each pointer movement. Once that argument is yours, the rest is simply typing it up.