Codepath

Two Pointer

The two pointer method is a helpful technique to keep in mind when working with strings and arrays. It's a clever optimization that can help reduce time complexity with no added space complexity (a win-win!) by utilizing extra pointers to avoid repetitive operations.

This approach is best demonstrated through a walkthrough, as done below.

Problem: Minimum size subarray sum

Given an array of n positive integers and a positive integer s, find the minimal length of a contiguous subarray of which the sum ≥ s. If there isn't one, return 0 instead.

Example:

>>> min_sub_array_length([2,3,1,2,4,3], 7)
2

Explanation: the subarray [4,3] has the minimal length under the problem constraint.

Solution #1: Brute Force

Approach

When first given a problem, if an optimal solution is not immediately clear, it's better to have any solution that works than be stuck. With this problem, a brute force solution would be to generate all possible subarrays and find the length of the shortest subarray that sums up to a sum that is greater than or equal to the given number.

Implementation

def min_sub_array_length(nums, sum):
    min_length = float("inf")
    for start_idx in range(len(nums)):
        for end_idx in range(start_idx, len(nums)):
            subarray_sum = get_sum(nums, start_idx, end_idx)
            if subarray_sum >= sum:
                min_length = min(min_length, end_idx - start_idx + 1)
    return min_length if min_length != float("inf") else 0

def get_sum(nums, start_index, end_index):
    result = 0
    for i in range(start_index, end_index + 1):
        result += nums[i]
    return result

Time and space complexity

The time complexity of this solution would be O(n3). The double for loop results in O(n2) calls to get_sum and each call to get_sum has a worst case run time of O(n), which results in a O(n2 * n) = O(n3) runtime.

The space complexity would be O(1) because the solution doesn't create new data structures.

Improvements

Optimization #1:

Keep track of a running sum instead of running get_sum in each iteration of the inner end_idx for loop

In the brute solution, a lot of repetitive calculations are done in the inner end_idx for loop with the get_sum function. Instead of recalculating the sum from elements start_idx to end_idx in every iteration of the end_idx loop, we can store a subarray_sum variable to save calculations from previous iterations and simply add to it in each iteration of the end_idx loop.

def min_sub_array_length(nums, sum):
    min_length = float("inf")
    for start_idx in range(len(nums)):
        subarray_sum = 0
        for end_idx in range(start_idx, len(nums)):
            subarray_sum += nums[end_idx]
            if subarray_sum >= sum:
                min_length = min(min_length, end_idx - start_idx + 1)
    return min_length if min_length != float("inf") else 0

This optimization reduces the time complexity from O(N3) to O(N2) with the addition of a variable to store the accumulating sum.

Optimization #2:

Reduce number of calculations by terminating the inner end_idx for loop early

With the improved solution, we can further reduce the number of iterations in the inner for loop by terminating it early. Once we have a subarray_sum that is equal to or greater than the target sum, we can simply move to the next iteration of the outer for loop. This is because the questions asks for minimum length subarray and any further iterations of the inner for loop would only cause an increase in the subarray length.

def min_sub_array_length(nums, sum):
    min_length = float("inf")
    for start_idx in range(len(nums)):
        subarray_sum = 0
        for end_idx in range(start_idx, len(nums)):
            subarray_sum += nums[end_idx]
            if subarray_sum >= sum:
                min_length = min(min_length, end_idx - start_idx + 1)
                continue
    return min_length if min_length != float("inf") else 0

This is a minor time complexity improvement and this solution will still have a worst case runtime of O(n2). The improvement is nice, but to reduce the runtime from O(n2) to O(n), we would need to somehow eliminate the inner for loop.

Solution #2: Two pointer approach

Approach:

The optimal, two pointer approach to this problem utilizing the observations we made in the previous section. The main idea of this approach is that we grow and shrink an interval as we loop through the list, while keeping a running sum that we update as we alter the interval.

There will be two pointers, one to track the start of the interval and the other to track the end. They will both start at the beginning of the list and move dynamically to the right until they hit the end of the list.

First, we grow the interval to the right until it exceeds the minimum sum. Once we find that interval, we move the start pointer right as much as we can to shrink the interval until it sums to a number that is smaller than the target sum.

Then, we move the end pointer to once again to try and hit the sum with new intervals. If growing the interval by moving the end pointer leads to an interval that sums up to at least the target sum, we need to repeat the process of trying to shrink the interval again by moving the start pointer before further moving the end pointer.

As we utilize these two pointers to determine which intervals to evaluate, we have a variable to keep track of the current sum of the interval as we go along to avoid recalculating it every time one of the pointers moves to the right, and another variable to store the length of the shortest interval that sums up to >= the target sum.

This push and pull of the end and start pointer will continue until we finish looping through the list.

Implementation

def min_sub_array_length(nums, sum):
    start_idx = 0
    min_length, subarray_sum = float('inf'), 0

    for end_idx in range(len(nums)):
        subarray_sum += nums[end_idx]
        while subarray_sum >= sum:
            min_length = min(min_length, end_idx - start_idx + 1)
            subarray_sum -= nums[start_idx]
            start_idx += 1
    if min_length == float('inf'):
        return 0
    return min_length

Time and space complexity

The time complexity of this solution is O(n) because each element is visited at most twice. In the worst case scenario, all elements will be visited once by the start pointer and another time by the end pointer.

The space complexity would be O(1) because the solution doesn't create new data structures.

Walkthrough

Take the example of min_sub_array_length([2,3,1,2,4,3], 7). The left pointer starts at 0 and the right doesn't exist yet.

As we start looping through the list, our first interval is [2]. We won't fulfill the while loop condition until the list reaches [2, 3, 1, 2] whose sum, 8 is >= 7. We then set the min_length to 4.

Now, we shrink the interval to [3, 1, 2] by increasing start_idx by 1. This new interval sums up to less than the target sum, 7 so we need to grow the interval. In the next iteration, we grow the interval to [3, 1, 2, 4], which has a sum of 10 and once again, we satisfy the while loop condition.

We then shrink the interval to [1, 2, 4]. This is the shortest interval we've come across that sums up to at least the target sum, so we update the min_length to 3.

We now move the end_idx pointer and it hits the end of the list, with interval [2, 4, 3]. Then shrink the interval to [4, 3], which sums up to 7, the target sum. This is the shortest interval we've come across that sums up to at least the target sum, so we update the min_length to 2. This is the final result that is returned.

Takeaways

This optimization can often be applied to improve solutions that involve the use of multiple for loops, as demonstrated in the example above. If you have an approach that utilizes multiple for loops, analyze the actions performed in those for loops to determine if repetitive calculations can be removed through strategic movements of multiple pointers.

Note: Though this walkthrough demonstrated applying the two pointer approach to an arrays problem, this approach is commonly utilized to solve string problems as well.

Fork me on GitHub