← Back to DevBytes

Solving Container With Most Water in JavaScript: Step-by-Step Guide

Introduction to the Container With Most Water Problem

The "Container With Most Water" problem is one of the most popular algorithmic challenges you'll encounter in coding interviews, particularly on platforms like LeetCode. It tests your ability to recognize patterns, optimize brute-force solutions, and apply the two-pointer technique effectively. In this tutorial, we'll walk through everything you need to know to solve this problem in JavaScript, from understanding the problem statement to writing an optimal solution.

What Is the Container With Most Water Problem?

Imagine you have an array of non-negative integers, where each integer represents the height of a vertical line drawn at that index on the x-axis. When you pick any two lines, they form a container with the x-axis, and the container can hold water up to the height of the shorter line. Your goal is to find two lines that, together with the x-axis, form a container that holds the maximum amount of water.

Formally, given an array height of length n, find indices i and j such that the area min(height[i], height[j]) * (j - i) is maximized. The area is determined by two factors: the distance between the two lines (width) and the height of the shorter line (height).

Why This Problem Matters

This problem is more than just an interview exercise. It teaches several fundamental concepts that are crucial for any developer working with algorithms and data structures:

Mastering this problem builds the intuition needed for similar challenges involving sliding windows, pointer manipulation, and optimization problems in general.

Understanding the Problem With an Example

Let's consider the input array [1, 8, 6, 2, 5, 4, 8, 3, 7]. Each value represents a vertical line's height at that index. If we pick the lines at index 1 (height 8) and index 8 (height 7), the width between them is 8 - 1 = 7, and the height is limited by the shorter line, which is 7. The area is therefore 7 * 7 = 49, which happens to be the maximum possible area for this input.

Notice that even though there are taller lines available, the combination of width and height at indices 1 and 8 produces the largest container. This illustrates the core trade-off: a taller pair of lines closer together might hold less water than a slightly shorter pair that is farther apart.

The Brute-Force Approach

Before jumping to the optimal solution, it's valuable to understand the brute-force approach. This method checks every possible pair of lines and computes the area for each, keeping track of the maximum found.

function maxAreaBruteForce(height) {
  let maxArea = 0;
  
  for (let i = 0; i < height.length; i++) {
    for (let j = i + 1; j < height.length; j++) {
      const width = j - i;
      const minHeight = Math.min(height[i], height[j]);
      const area = width * minHeight;
      maxArea = Math.max(maxArea, area);
    }
  }
  
  return maxArea;
}

// Example usage
const heights = [1, 8, 6, 2, 5, 4, 8, 3, 7];
console.log(maxAreaBruteForce(heights)); // Output: 49

While this solution is correct, it has a time complexity of O(n²) because of the nested loops. For large arrays, this becomes prohibitively slow. The space complexity is O(1) since we only use a few variables.

The Optimal Two-Pointer Solution

The key insight for optimization is that we don't need to check every pair. By using two pointers—one starting at the beginning of the array and one at the end—we can systematically narrow down the search space while always keeping track of the maximum area.

How the Two-Pointer Technique Works Here

We start with the widest possible container, with one pointer at index 0 and the other at the last index. At each step, we calculate the area formed by the two lines at the pointers. Then, we move the pointer pointing to the shorter line inward. The reasoning is simple: moving the pointer at the taller line would never increase the area, because the height is limited by the shorter line, and the width is decreasing. The only chance to find a larger area is to move the shorter line's pointer inward, hoping to find a taller line that compensates for the reduced width.

function maxArea(height) {
  let left = 0;
  let right = height.length - 1;
  let maxArea = 0;
  
  while (left < right) {
    const width = right - left;
    const currentHeight = Math.min(height[left], height[right]);
    const area = width * currentHeight;
    maxArea = Math.max(maxArea, area);
    
    // Move the pointer pointing to the shorter line
    if (height[left] < height[right]) {
      left++;
    } else {
      right--;
    }
  }
  
  return maxArea;
}

// Example usage
const heights = [1, 8, 6, 2, 5, 4, 8, 3, 7];
console.log(maxArea(heights)); // Output: 49

This solution runs in O(n) time because each element is visited at most once, and the two pointers move toward each other until they meet. The space complexity remains O(1), making it highly efficient.

Step-by-Step Walkthrough

Let's trace through the algorithm with the input [1, 8, 6, 2, 5, 4, 8, 3, 7] to see exactly how it works:

As you can see, the algorithm efficiently narrows down the search space while always preserving the possibility of finding a larger area.

Edge Cases to Consider

When implementing this solution, it's important to handle edge cases properly to ensure robustness:

function maxArea(height) {
  // Edge case: array with fewer than 2 elements cannot form a container
  if (!height || height.length < 2) {
    return 0;
  }
  
  let left = 0;
  let right = height.length - 1;
  let maxArea = 0;
  
  while (left < right) {
    const width = right - left;
    const currentHeight = Math.min(height[left], height[right]);
    const area = width * currentHeight;
    maxArea = Math.max(maxArea, area);
    
    if (height[left] < height[right]) {
      left++;
    } else {
      right--;
    }
  }
  
  return maxArea;
}

// Test cases
console.log(maxArea([1, 1]));           // Output: 1
console.log(maxArea([4, 3, 2, 1, 4]));  // Output: 16
console.log(maxArea([1, 2, 1]));        // Output: 2
console.log(maxArea([]));               // Output: 0
console.log(maxArea([5]));              // Output: 0

These test cases cover scenarios such as minimal arrays, descending and ascending heights, empty inputs, and single-element inputs. Always validate your solution against these before considering it production-ready.

Best Practices and Optimization Tips

1. Always Start With Brute Force

In an interview setting, start by explaining the brute-force solution. This demonstrates that you understand the problem before attempting to optimize. Once you've established the baseline, you can discuss why it's inefficient and how the two-pointer approach improves upon it.

2. Use Descriptive Variable Names

While left and right are clear, avoid single-letter variable names in production code. Descriptive names improve readability and maintainability, especially when the logic becomes more complex.

3. Avoid Redundant Calculations

In the optimal solution, we compute Math.min(height[left], height[right]) once per iteration. Avoid recalculating values that haven't changed, as this can add unnecessary overhead in tight loops.

4. Test With Large Inputs

The O(n) solution should handle arrays with tens of thousands of elements without issue. If you're unsure, test with a large generated array to confirm performance:

// Performance test with a large array
const largeArray = Array.from({ length: 100000 }, (_, i) => 
  Math.floor(Math.random() * 1000)
);

console.time('maxArea');
const result = maxArea(largeArray);
console.timeEnd('maxArea');
console.log('Result:', result);

5. Understand Why Moving the Shorter Pointer Works

The correctness of this algorithm hinges on a crucial observation: when you move the pointer at the taller line, the width decreases, and the height cannot increase (it's still bounded by the shorter line). Therefore, the area can only stay the same or decrease. By moving the shorter pointer, you give the algorithm a chance to find a taller line that might produce a larger area despite the reduced width. This greedy choice is what makes the solution both correct and efficient.

Common Mistakes to Avoid

Variations and Related Problems

Once you've mastered the Container With Most Water problem, you can apply similar techniques to related challenges:

Practicing these variations will deepen your understanding of when and how to apply two-pointer strategies effectively.

Conclusion

The Container With Most Water problem is a classic example of how a clever algorithmic insight can transform a slow brute-force solution into an elegant, linear-time algorithm. By understanding the trade-off between width and height, and by applying the two-pointer technique with greedy reasoning, you can solve this problem efficiently in JavaScript. Remember to start with the brute-force approach to build your understanding, then optimize using the two-pointer method, and always test your solution against edge cases. With this knowledge in your toolkit, you'll be well-prepared to tackle this problem in interviews and apply the same principles to a wide range of related algorithmic challenges.

— Ad —

Google AdSense will appear here after approval

← Back to all articles