Skip to main content

Maximum Subarray Problem Solution

Complete Java implementation of Kadane's algorithm for finding maximum subarray sum with O(n) time and space complexity analysis.

The maximum subarray problem seeks to find the contiguous subarray with the largest sum in a given array of integers. This classic algorithmic challenge demonstrates the power of dynamic programming and optimal substructure properties in array processing.

1. Problem Restatement

Clarifying questions to ask the interviewer:

  1. What are the constraints on input size? (e.g., is it up to 10^5 elements?)
  2. Are duplicates allowed in the input array?
  3. Should I return just the maximum sum or also the subarray indices?
  4. Can I mutate the input array, or must I work with a copy?
  5. What's the target time complexity? (e.g., O(n) is preferred over O(n²))

Restatement in my own words: Given an array of integers, find the contiguous subarray with the largest sum. This is a classic dynamic programming problem where we need to identify the optimal contiguous segment that maximizes the total sum.

Clarification of inputs, outputs, and constraints:

  • Input: An array of integers (can contain negative numbers)
  • Output: The maximum sum of any contiguous subarray
  • Constraints: Array length can be up to 10^5 elements, values can range from -10^4 to 10^4
  • The subarray must be contiguous (elements must appear consecutively in the array)

Assumptions I'm making:

  • The input array is non-null (we can handle empty arrays as a special case)
  • We're looking for the maximum sum, not the actual subarray elements
  • The problem allows for negative numbers in the array

Data shape and hidden structure: The input is a 1D array of integers with no specific ordering. The problem has a hidden greedy structure where we can make locally optimal choices at each position to build up the global optimum. The key insight is that at any point, we can decide whether to extend the current subarray or start fresh from the current element.

This problem exhibits the classic "maximum subarray" pattern that appears in many algorithmic interviews and is solvable efficiently using Kadane's algorithm.

2. Pattern Recognition

The problem asks us to find the contiguous subarray with the largest sum, which is a classic dynamic programming problem. The key indicators that signal this pattern are:

  1. "Contiguous subarray" - This tells us we're looking for a continuous segment of the array, not just any elements
  2. "Maximum sum" - This suggests we're optimizing for a value, which is typical in DP problems
  3. "Subarray" - The requirement for a continuous sequence points to sliding window or prefix sum approaches

The core insight is that at each position, we must decide whether to:

  • Extend the existing subarray by including the current element, or
  • Start a new subarray from the current element

This decision-making process is characteristic of Kadane's Algorithm pattern, where we maintain a running maximum and make local optimal choices that lead to global optimum.

The hidden structure here is that we're essentially tracking two values at each step:

  1. The maximum sum ending at the current position (local optimum)
  2. The overall maximum sum seen so far (global optimum)

This pattern appears in many variants of subarray optimization problems, making it a fundamental algorithmic pattern to recognize and apply.

The problem fits this pattern because:

  • It has optimal substructure (the maximum ending at position i depends on the maximum ending at position i-1)
  • It has overlapping subproblems (we compute maximum sums for multiple positions)
  • The optimal solution can be built from solutions to subproblems

The core insight is that we don't need to check all possible subarrays (which would be O(n²)), but can compute the optimal solution in one pass by making locally optimal decisions at each step.

3. Approach Evolution

My first instinct is to consider a brute force approach where I check all possible subarrays and calculate their sums, but that would be O(n³) time complexity because for each starting position, I'd need to iterate through all ending positions and sum the elements in between. That's clearly not optimal.

Let me think about how to optimize this. I notice that if I maintain a running sum and keep track of the maximum sum seen so far, I can reduce it to O(n) time by using Kadane's algorithm. The key insight is that at each position, I need to decide whether to extend the current subarray or start a new one. If the current running sum becomes negative, it's better to discard it and start fresh.

I'm thinking of the classic Kadane's algorithm where I maintain two variables: one for the maximum sum ending at the current position, and another for the overall maximum seen so far. This is a classic dynamic programming problem where we're making local optimal choices that lead to global optimality.

The key invariant I need to maintain is that at each step, currentMax represents the maximum sum of a subarray ending at the current position, and globalMax tracks the overall maximum seen so far. When I encounter a negative running sum, it's better to restart with the current element rather than extend the previous subarray.

This approach works because we're essentially asking: "At each position, what's the best subarray ending here?" and we can answer that by either extending the previous subarray or starting fresh. The global maximum is just the best of all these local optimal choices.

The algorithm will process each element once, making it linear time. Space complexity is O(1) since we only need a few variables to track our state.

The approach is clean, optimal, and handles all edge cases naturally - whether we have all negative numbers, mixed positive/negative, or just positive numbers.

4. Data Structure Choice

For the maximum subarray problem, I'll use a combination of primitive variables and the input array itself.

Primary data structures:

  • int[] nums - The input array, which we'll read from but not modify (as per typical FAANG interview constraints)
  • int variables for tracking current sum, maximum sum, and indices (when needed)

Why each data structure is needed:

  • The input array is essential as our source of values to process
  • We need only a few integer variables to maintain state: current sum and maximum sum found so far
  • No additional collections like lists or hash maps are required since we're looking for a contiguous subarray and can process elements sequentially

Time and space impact:

  • Time complexity remains O(n) as we iterate once through the array
  • Space complexity is O(1) - constant extra space beyond input, since we only use a few variables to track state
  • The solution doesn't require any auxiliary data structures that grow with input size

Helper structure:

  • No helper structures are needed beyond the basic variables to track running sums and maximum values
  • The algorithm is purely iterative with state tracking, so no complex data structures like stacks or queues are required

This minimal approach aligns with Kadane's algorithm's efficiency - we don't need to store intermediate subarrays or their sums, just the running maximums.

5. Algorithm Walkthrough

Let me trace through the algorithm with the example array [-2,1,-3,4,-1,2,1,-5,4]:

Step 1: Initialize maxSoFar = -2, maxEndingHere = -2

Step 2: Process nums[1] = 1

  • maxEndingHere = max(1, -2 + 1) = max(1, -1) = 1
  • maxSoFar = max(-2, 1) = 1

Step 3: Process nums[2] = -3

  • maxEndingHere = max(-3, 1 + (-3)) = max(-3, -2) = -2
  • maxSoFar = max(1, -2) = 1

Step 4: Process nums[3] = 4

  • maxEndingHere = max(4, -2 + 4) = max(4, 2) = 4
  • maxSoFar = max(1, 4) = 4

Step 5: Process nums[4] = -1

  • maxEndingHere = max(-1, 4 + (-1)) = max(-1, 3) = 3
  • maxSoFar = max(4, 3) = 4

Step 6: Process nums[5] = 2

  • maxEndingHere = max(2, 3 + 2) = max(2, 5) = 5
  • maxSoFar = max(4, 5) = 5

Step 7: Process nums[6] = 1

  • maxEndingHere = max(1, 5 + 1) = max(1, 6) = 6
  • maxSoFar = max(5, 6) = 6

Step 8: Process nums[7] = -5

  • maxEndingHere = max(-5, 6 + (-5)) = max(-5, 1) = 1
  • maxSoFar = max(6, 1) = 6

Step 9: Process nums[8] = 4

  • maxEndingHere = max(4, 1 + 4) = max(4, 5) = 5
  • maxSoFar = max(6, 5) = 6

The algorithm correctly identifies that the subarray [4,-1,2,1] gives the maximum sum of 6.

The key invariants maintained:

  • maxEndingHere always represents the maximum sum of subarray ending at current position
  • maxSoFar always tracks the global maximum seen so far
  • At each step, we decide whether to extend the previous subarray or start fresh from current element

Edge cases handled:

  • Single element arrays work correctly
  • All negative numbers return the least negative number
  • Mixed positive/negative arrays find optimal subarray efficiently

6. Java Solution

public class MaximumSubarray {
    /**
     * Finds the contiguous subarray with the largest sum using Kadane's algorithm.
     * 
     * @param nums Array of integers
     * @return Maximum sum of contiguous subarray
     */
    public int maxSubArray(int[] nums) {
        // Handle edge case of null or empty array
        if (nums == null || nums.length == 0) {
            throw new IllegalArgumentException("Input array cannot be null or empty");
        }
        
        // Initialize variables to track maximum sum and current sum
        int maxSum = nums[0];  // Global maximum
        int currentSum = nums[0];  // Current subarray sum
        
        // Iterate through the array starting from index 1
        for (int i = 1; i < nums.length; i++) {
            // Either extend the existing subarray or start a new one
            currentSum = Math.max(nums[i], currentSum + nums[i]);
            
            // Update the global maximum if current sum is larger
            maxSum = Math.max(maxSum, currentSum);
        }
        
        return maxSum;
    }
}

This implementation uses Kadane's algorithm, which is optimal for this problem. The key insight is that at each position, we decide whether to extend the previous subarray or start fresh from the current element. The algorithm maintains two variables: one for the maximum sum seen so far and another for the current subarray sum. This approach ensures we never need to revisit previous elements, making it both time and space efficient.

7. Complexity Analysis

Time Complexity: O(n)

The algorithm makes a single pass through the input array exactly once. At each element, we perform constant-time operations: comparing values, updating variables, and making a simple decision. Since each of the n elements is processed exactly once with O(1) work per element, the overall time complexity is O(n).

Space Complexity: O(1)

The solution uses only a constant amount of extra space regardless of input size. We maintain just two integer variables (maxSoFar and maxEndingHere) to track the necessary state information. No additional data structures like arrays, stacks, or hash maps are required. The space usage remains constant at O(1) as the input size grows.

Why These Bounds Are Correct

The time bound is optimal for this problem because we must examine each element at least once to determine the maximum subarray sum. Any algorithm that skips elements would risk missing the true maximum. The O(n) linear time complexity matches the information-theoretic lower bound for this problem.

The space bound is optimal because we only need to store the essential state information at any point in time. The maximum subarray sum can be computed using only the current element and the best sums seen so far, requiring no additional storage beyond a few variables.

Optimality

This solution is optimal in both time and space complexity. It achieves the best possible performance characteristics for this problem, with linear time and constant space usage. No better complexity bounds are theoretically achievable for this problem since we must process each element and only need constant state information.

8. Edge Cases & Pitfalls

Empty and Minimal Inputs

  • Empty array: Should return 0 or throw an exception depending on requirements. The problem typically assumes non-empty arrays.
  • Single element: Should handle correctly - if the element is positive, it's the answer; if negative, the optimal subarray might be empty (0) or just that element.

Duplicate Values and Repeated States

  • All negative numbers: The algorithm must correctly identify that the maximum subarray is the single largest element, not an empty subarray.
  • Mixed positive/negative: Duplicates can cause confusion in tracking running sums and maximums.

Boundary Conditions

  • All zeros: The maximum subarray should be 0 (empty or single zero element).
  • Large negative numbers: The algorithm must properly handle cases where including a negative number reduces the sum.
  • Integer overflow: When dealing with very large numbers, ensure that intermediate sums don't overflow.

Common Implementation Mistakes

  • Incorrect initialization: Setting maxSoFar to 0 instead of Integer.MIN_VALUE or the first element can lead to wrong results for all-negative arrays.
  • Not updating max correctly: Forgetting to update maxSoFar when a new maximum is found.
  • Off-by-one errors: Incorrectly handling array indices, especially when comparing current element with running sum.
  • Misunderstanding the problem: Confusing maximum subarray (contiguous) with maximum sum of any elements (non-contiguous).

Key Implementation Tips

  1. Always validate input constraints before processing.
  2. Initialize tracking variables properly based on problem requirements.
  3. Handle the case where all elements are negative by returning the maximum single element.
  4. Use Math.max() carefully to ensure we're tracking the global maximum correctly throughout the iteration.
  5. Consider that a subarray can be of size 0 (empty), but in most interpretations, we want the maximum sum of a contiguous subsequence.

9. Alternative Approaches

There are several alternative approaches to solving the maximum subarray problem, each with different trade-offs:

Brute Force Approach: The most straightforward alternative is to check all possible subarrays using nested loops. For each starting index, we compute the sum of all subarrays beginning at that index, keeping track of the maximum. This approach has O(n³) time complexity due to computing sums repeatedly, and O(1) space complexity. While simple to implement, it's inefficient for large inputs.

Divide and Conquer: This approach recursively divides the array into halves, computing three possible maximum subarrays: one entirely in the left half, one entirely in the right half, and one crossing the midpoint. The recurrence relation T(n) = 2T(n/2) + O(n) gives us O(n log n) time complexity. This approach is more complex to implement correctly and requires careful handling of the crossing subarray case, but it demonstrates a different algorithmic paradigm.

Dynamic Programming with Space Optimization: While our chosen solution uses Kadane's algorithm which is optimal, we could implement a 2D DP approach where dp[i][j] represents the maximum subarray sum in range [i,j]. However, this would require O(n²) space and is unnecessarily complex for this problem.

Kadane's Algorithm Variants: The standard Kadane's algorithm can be modified to return the actual subarray indices instead of just the sum, or to handle multiple maximum subarrays if duplicates exist. These variants maintain the same O(n) time complexity but may increase space usage slightly.

The chosen solution using Kadane's algorithm is preferred because it provides optimal O(n) time and O(1) space complexity with clean, readable implementation that's easy to verify for correctness. The alternatives either have worse time complexity or unnecessary complexity for this specific problem.

10. Test Cases

public class MaximumSubarray {
    public int maxSubArray(int[] nums) {
        if (nums == null || nums.length == 0) return 0;
        
        int maxSoFar = nums[0];
        int maxEndingHere = nums[0];
        
        for (int i = 1; i < nums.length; i++) {
            maxEndingHere = Math.max(nums[i], maxEndingHere + nums[i]);
            maxSoFar = Math.max(maxSoFar, maxEndingHere);
        }
        
        return maxSoFar;
    }
}

Sample Test Cases

Basic case with mixed positive/negative numbers:

int[] nums1 = {-2, 1, -3, 4, -1, 2, 1, -5, 4};
// Expected: 6 (subarray [4, -1, 2, 1])

All negative numbers:

int[] nums2 = {-2, -3, -1, -5};
// Expected: -1 (single element subarray with max value)

All positive numbers:

int[] nums3 = {1, 2, 3, 4, 5};
// Expected: 15 (entire array)

Single element:

int[] nums4 = {5};
// Expected: 5

Empty array (edge case):

int[] nums5 = {};
// Expected: 0 (or handle as invalid input)

Two elements with one negative:

int[] nums6 = {-1, 2};
// Expected: 2 (just the second element)

Large negative followed by positive:

int[] nums7 = {-100000, 100000};
// Expected: 100000 (just the second element)

11. Final Summary

The core idea is to efficiently find the contiguous subarray with the largest sum using Kadane's algorithm, which maintains a running maximum by comparing the current element with the accumulated sum so far. This greedy approach works because at each position, we decide whether to extend the existing subarray or start fresh, ensuring we never miss a better solution. The algorithm is optimal with O(n) time and O(1) space complexity, making it both time and space efficient for this problem.

Was this article helpful?

Topics in this article

More from DSA Problems

Stay in the Loop

Get curated DSA patterns, coding problems, frontend and backend LLD/HLD, and AI/ML System Design insights delivered straight to your inbox. No spam, just focused interview and engineering content.