11 Maximum Subarray
Rank 9 · Pattern: Kadane’s algorithm / compressed dynamic programming · Target: \(O(n)\) time, \(O(1)\) space
11.1 Problem definition
Given an integer array nums, return the greatest sum among all of its nonempty contiguous subarrays.
11.2 Clarifying questions
- Is the input guaranteed to contain at least one value?
- Should I return only the maximum sum or also the subarray’s boundaries?
- Is an empty subarray a valid choice?
- What numeric range should I use for accumulated sums?
11.3 Sample answers
- Yes.
numsis non-null and nonempty. - Return only the sum.
- No. The chosen subarray must be nonempty, so an all-negative input returns its largest element rather than
0. - Assume every relevant sum fits in a signed 32-bit
int.
11.4 State and recurrence
Enumerating all start/end pairs costs \(O(n^2)\) even with prefix sums. The key is that only one fact about the previous position matters.
Define endingHere as the maximum sum of a nonempty subarray ending exactly at the current index. At value x, either:
- start a new subarray at
x, or - extend the best subarray ending at the previous index.
So:
\[ endingHere_i = \max(nums_i, endingHere_{i-1} + nums_i) \]
best is the maximum endingHere seen anywhere.
Invariant: after processing index i, endingHere is optimal among subarrays ending at i, and best is optimal among all subarrays contained in 0..i.
For [-2,1,-3,4,-1,2,1,-5,4]:
| value | ending here | best |
|---|---|---|
| -2 | -2 | -2 |
| 1 | 1 | 1 |
| -3 | -2 | 1 |
| 4 | 4 | 4 |
| -1 | 3 | 4 |
| 2 | 5 | 5 |
| 1 | 6 | 6 |
| -5 | 1 | 6 |
| 4 | 5 | 6 |
11.5 Minimal solution
import java.util.*;
import org.junit.*;
import org.junit.runner.*;
import static org.junit.Assert.*;
public class MaximumSubarraySolution {
static int maxSubarray(int[] nums) {
int endingHere = nums[0];
int best = nums[0];
for (int i = 1; i < nums.length; i++) {
endingHere = Math.max(nums[i], endingHere + nums[i]);
best = Math.max(best, endingHere);
}
return best;
}
@Test public void findsBestRange() {
assertEquals(6, maxSubarray(new int[] {-2, 1, -3, 4, -1, 2, 1, -5, 4}));
}
@Test public void handlesAllNegativeValues() {
assertEquals(-2, maxSubarray(new int[] {-8, -2, -5}));
}
@Test public void handlesSingleValue() {
assertEquals(7, maxSubarray(new int[] {7}));
}
@Test public void mayUseTheEntireArray() {
assertEquals(6, maxSubarray(new int[] {1, 2, 3}));
}
@Test public void startsNewSubarrayAfterLargeLoss() {
assertEquals(13, maxSubarray(new int[] {5, -100, 6, 7}));
}
@Test public void keepsBestEarlierSubarray() {
assertEquals(5, maxSubarray(new int[] {5, -6, 4}));
}
@Test public void handlesZeros() {
assertEquals(0, maxSubarray(new int[] {0, -1, 0}));
}
public static void main(String[] args) {
JUnitCore.main("MaximumSubarraySolution");
}
}import unittest
def max_subarray(nums):
ending_here = nums[0]
best = nums[0]
for value in nums[1:]:
ending_here = max(value, ending_here + value)
best = max(best, ending_here)
return best
class MaximumSubarrayTests(unittest.TestCase):
def test_finds_best_range(self):
self.assertEqual(6, max_subarray([-2, 1, -3, 4, -1, 2, 1, -5, 4]))
def test_handles_all_negative_values(self):
self.assertEqual(-2, max_subarray([-8, -2, -5]))
def test_handles_single_value(self):
self.assertEqual(7, max_subarray([7]))
def test_may_use_entire_array(self):
self.assertEqual(6, max_subarray([1, 2, 3]))
def test_starts_new_subarray_after_large_loss(self):
self.assertEqual(13, max_subarray([5, -100, 6, 7]))
def test_keeps_best_earlier_subarray(self):
self.assertEqual(5, max_subarray([5, -6, 4]))
def test_handles_zeros(self):
self.assertEqual(0, max_subarray([0, -1, 0]))
if __name__ == "__main__":
unittest.main()pub fn max_subarray(nums: &[i32]) -> i32 {
let mut ending_here = nums[0];
let mut best = nums[0];
for &value in &nums[1..] {
ending_here = value.max(ending_here + value);
best = best.max(ending_here);
}
best
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn finds_best_range() {
assert_eq!(6, max_subarray(&[-2, 1, -3, 4, -1, 2, 1, -5, 4]));
}
#[test]
fn handles_all_negative_values() {
assert_eq!(-2, max_subarray(&[-8, -2, -5]));
}
#[test]
fn handles_single_value() {
assert_eq!(7, max_subarray(&[7]));
}
#[test]
fn may_use_entire_array() {
assert_eq!(6, max_subarray(&[1, 2, 3]));
}
#[test]
fn starts_new_subarray_after_large_loss() {
assert_eq!(13, max_subarray(&[5, -100, 6, 7]));
}
#[test]
fn keeps_best_earlier_subarray() {
assert_eq!(5, max_subarray(&[5, -6, 4]));
}
}Initializing from nums[0] rather than zero is what makes all-negative inputs correct.
11.6 High-value tests
| Input | Expected | Purpose |
|---|---|---|
[7] |
7 | one value |
[-8,-2,-5] |
-2 | all negative |
[1,2,3] |
6 | entire array |
[-2,1,-3,4,-1,2,1,-5,4] |
6 | interior range |
[0,-1,0] |
0 | zeros |
11.7 Complexity and correctness
The array is scanned once, giving \(O(n)\) time and \(O(1)\) working space.
Any optimal subarray ending at i either contains the previous position or starts at i; there is no third form. If it contains the previous position, extending the best such prior subarray is never worse than extending another. The recurrence therefore computes the exact local optimum, and best takes the maximum of all possible ending positions.
11.8 If the interviewer pushes
- Return indices by tracking the tentative start whenever a new subarray begins, and save the best boundaries.
- Use
longif sums may exceed theintrange. - For a circular array, combine Kadane’s maximum with total sum minus a minimum-subarray result, handling the all-negative case separately.
- A divide-and-conquer solution is \(O(n\log n)\) and demonstrates a different pattern but is not an improvement here.
11.9 TDD interview script
Confirm that input is non-null and nonempty, the chosen subarray must be nonempty and contiguous, only the sum is returned, and relevant sums fit in int. Then write this test list:
- One value
- All positive values
- Forced restart
- Earlier best that must be retained
- All negative values
- Zeros
- Standard mixed example
Add one focused test per red–green–refactor cycle. Verify the failure, introduce the smallest recurrence or state change, rerun the complete example, and refactor only while green. Tests assert the result, not the internal dynamic-programming variables.
From book/src, run:
./java/run.sh MaximumSubarraySolution./python/run.sh maximum_subarray_solution./rust/run.sh maximum_subarray_solutionExplain the two pieces of state before generalizing: endingHere answers a constrained question about subarrays ending at the current index, while best remembers the unconstrained maximum over every ending position seen so far.
11.9.1 Pseudocode
endingHere = nums[0]
best = nums[0]
for each x after nums[0]:
endingHere = max(x, endingHere + x)
best = max(best, endingHere)
return best
The invariant is: after index i, endingHere is the greatest sum of a nonempty subarray ending exactly at i, and best is the greatest sum of any subarray within 0..i.
11.9.2 TDD sequence
| Step | Test code added | Production code added or changed |
|---|---|---|
| 1. Required element | handlesSingleValue |
Initialize both state variables from nums[0], preserving the nonempty-subarray contract. |
| 2. Extend a winner | mayUseTheEntireArray |
Iterate from index one and extend the previous ending sum when doing so is beneficial. |
| 3. Restart after loss | startsNewSubarrayAfterLargeLoss |
Set endingHere to max(x, endingHere + x) so a new subarray can begin at the current value. |
| 4. Preserve global best | keepsBestEarlierSubarray |
Track best separately from endingHere; a later local result must not erase an earlier maximum. |
| 5. No empty escape | handlesAllNegativeValues |
Triangulate initialization from the first element rather than zero, so the least-negative value wins. |
| 6. Zero boundary | handlesZeros |
Verify that zero can be the optimal nonempty sum without special-case logic. |
| 7. Mixed acceptance case | findsBestRange |
Exercise several extensions, restarts, and best updates in one standard example. |
The sequence derives Kadane’s recurrence from two choices at each position—start or extend—and uses focused regressions to keep the local and global optima distinct.