Find the contiguous subarray with the largest sum.

Medium
7 years ago

You are given an array of integers. Write a function to find the contiguous subarray with the largest sum and return that sum.

For example:

  • Given the array [-2, 1, -3, 4, -1, 2, 1, -5, 4], the contiguous subarray [4, -1, 2, 1] has the largest sum = 6.
  • Given the array [1, 2, 3, 4, 5], the contiguous subarray [1, 2, 3, 4, 5] has the largest sum = 15.
  • Given the array [-1, -2, -3], the contiguous subarray [-1] has the largest sum = -1.

Your function should be efficient and handle arrays with both positive and negative numbers. Explain the time complexity of your solution. Can you implement this using dynamic programming? Are there other approaches? Discuss the tradeoffs of the various potential solutions.

Sample Answer

Maximum Subarray Sum

This question asks us to find the contiguous subarray within a given array of integers that has the largest sum. Let's explore different approaches to solve this problem, analyze their time and space complexities, and discuss their tradeoffs.

1. Brute Force Approach

A simple approach is to consider all possible subarrays and calculate the sum of each subarray. We can then keep track of the maximum sum encountered so far.

def max_subarray_brute_force(nums):
    max_sum = float('-inf')  # Initialize with negative infinity
    n = len(nums)
    for i in range(n):
        for j in range(i, n):
            current_sum = 0
            for k in range(i, j + 1):
                current_sum += nums[k]
            max_sum = max(max_sum, current_sum)
    return max_sum

# Example usage
nums = [-2, 1, -3, 4, -1, 2, 1, -5, 4]
print(f"Maximum subarray sum (brute force): {max_subarray_brute_force(nums)}")

Time Complexity: O(n^3)

  • The outer loop iterates n times.
  • The middle loop iterates n - i times in the worst case, which is still O(n).
  • The inner loop iterates j - i + 1 times in the worst case, which is also O(n).
  • Therefore, the overall time complexity is O(n * n * n) = O(n^3).

Space Complexity: O(1)

  • The algorithm uses a constant amount of extra space, regardless of the input size.

2. Improved Brute Force Approach

We can optimize the brute force approach by calculating the subarray sum more efficiently. Instead of recomputing the sum for each subarray, we can reuse the sum from the previous iteration.

def max_subarray_improved_brute_force(nums):
    max_sum = float('-inf')
    n = len(nums)
    for i in range(n):
        current_sum = 0
        for j in range(i, n):
            current_sum += nums[j]
            max_sum = max(max_sum, current_sum)
    return max_sum

# Example usage
nums = [-2, 1, -3, 4, -1, 2, 1, -5, 4]
print(f"Maximum subarray sum (improved brute force): {max_subarray_improved_brute_force(nums)}")

Time Complexity: O(n^2)

  • The outer loop iterates n times.
  • The inner loop iterates n - i times in the worst case, which is still O(n).
  • Therefore, the overall time complexity is O(n * n) = O(n^2).

Space Complexity: O(1)

  • The algorithm still uses a constant amount of extra space.

3. Kadane's Algorithm (Dynamic Programming)

Kadane's algorithm is an efficient dynamic programming approach to solve this problem. It maintains two variables:

  • max_so_far: Stores the maximum sum found so far.
  • current_max: Stores the maximum sum ending at the current position.

The algorithm iterates through the array, updating current_max by choosing the maximum between the current element and the sum of the current element and the previous current_max. max_so_far is updated with the maximum value between the current max_so_far and current_max.

def max_subarray_kadane(nums):
    max_so_far = float('-inf')
    current_max = 0
    for i in range(len(nums)):
        current_max = max(nums[i], current_max + nums[i])
        max_so_far = max(max_so_far, current_max)
    return max_so_far

# Example usage
nums = [-2, 1, -3, 4, -1, 2, 1, -5, 4]
print(f"Maximum subarray sum (Kadane's algorithm): {max_subarray_kadane(nums)}")

Time Complexity: O(n)

  • The algorithm iterates through the array once.
  • Therefore, the time complexity is O(n).

Space Complexity: O(1)

  • The algorithm uses a constant amount of extra space.

4. Divide and Conquer Approach

Another approach is to use divide and conquer. The main idea is to divide the array into two halves, recursively find the maximum subarray sum in each half, and then find the maximum subarray sum that crosses the midpoint.

def max_crossing_subarray(nums, low, mid, high):
    left_sum = float('-inf')
    current_sum = 0
    for i in range(mid, low - 1, -1):
        current_sum += nums[i]
        left_sum = max(left_sum, current_sum)

    right_sum = float('-inf')
    current_sum = 0
    for i in range(mid + 1, high + 1):
        current_sum += nums[i]
        right_sum = max(right_sum, current_sum)

    return left_sum + right_sum


def max_subarray_divide_and_conquer(nums, low, high):
    if low == high:
        return nums[low]

    mid = (low + high) // 2

    return max(
        max_subarray_divide_and_conquer(nums, low, mid),
        max_subarray_divide_and_conquer(nums, mid + 1, high),
        max_crossing_subarray(nums, low, mid, high),
    )


def max_subarray_dc(nums):
    return max_subarray_divide_and_conquer(nums, 0, len(nums) - 1)

# Example usage
nums = [-2, 1, -3, 4, -1, 2, 1, -5, 4]
print(f"Maximum subarray sum (divide and conquer): {max_subarray_dc(nums)}")

Time Complexity: O(n log n)

  • The recurrence relation for this algorithm is T(n) = 2T(n/2) + O(n), which solves to O(n log n) using the Master Theorem.

Space Complexity: O(log n)

  • The space complexity is determined by the depth of the recursion, which is O(log n).

Tradeoffs

  • Brute Force: Simple to understand but inefficient for large arrays.
  • Improved Brute Force: Slightly better but still not optimal.
  • Kadane's Algorithm: Most efficient with linear time complexity and constant space complexity. Preferred for its simplicity and performance.
  • Divide and Conquer: More complex to implement and less efficient than Kadane's algorithm, but it's a good example of a divide and conquer strategy.
ApproachTime ComplexitySpace ComplexityAdvantagesDisadvantages
Brute ForceO(n^3)O(1)Simple to understandInefficient for large arrays
Improved Brute ForceO(n^2)O(1)Better than brute forceStill not optimal
Kadane's AlgorithmO(n)O(1)Most efficient, simpleNone
Divide and ConquerO(n log n)O(log n)Illustrates divide and conquerMore complex, less efficient than Kadane's

Edge Cases

  1. Empty Array: If the input array is empty, the maximum subarray sum should be 0.
  2. All Negative Numbers: If all numbers in the array are negative, the maximum subarray sum is the largest negative number (i.e., the least negative number).

Kadane's algorithm handles both of these cases correctly.

Conclusion

Kadane's algorithm provides the most efficient solution for finding the maximum contiguous subarray sum, with a time complexity of O(n) and a space complexity of O(1). It's easy to implement and understand, making it the preferred choice for this problem.