Problem overview
The method maxSumSubarray receives an Array[Int] called nums, which may hold positive and negative numbers. It returns the largest sum that any subarray of nums can have. A subarray is a run of neighbouring elements, from some index i to some index j with i <= j.
For nums = Array(-2, -5, 6, -2, -3, 1, 5, -6) the answer is 7, from the subarray 6, -2, -3, 1, 5. The statement allows O(n^2) time and O(1) extra space.
Approach: try every subarray, but sum smartly
The brute-force idea is to try every candidate. A subarray is fixed by its first index i and its last index j, so two nested loops can visit every pair. For each pair, compare the subarray's sum with the best sum found so far.
Computing each sum from scratch with a third loop would make the whole thing O(n^3). The trick that brings it down to O(n^2) is to notice that, for a fixed i, each step of j just adds one element to the subarray. So keep a running sum: reset it to 0 when i changes, and add nums(j) each time j moves. No extra array of prefix sums is needed, which keeps the extra space constant.
Faster approaches exist, and this course revisits the problem with them. This lesson sticks to the quadratic one.
Tracing one start index
In the example, take i = 2, where nums(2) is 6. As j goes from 2 to 7 the running sum takes the values 6, 4, 1, 2, 7 and 1. The largest of those is 7, reached at j = 6. No other start index does better, so 7 is the answer.
Step by step
- Declare
maxSumwithvarand initialize it tonums(0). Don't start at 0: if every number is negative, the best subarray has a negative sum, and 0 would be larger than any real answer. A single element is itself a subarray, sonums(0)is a sum that really exists. Usevarbecause it changes. - Loop over each start index with
for (i <- nums.indices).nums.indicesis the range from 0 tonums.length - 1. - Declare
var sum = 0inside that loop. It belongs to one start index, so it is created fresh each time and is not visible outside the loop. Starting from 0 means an empty subarray, which is what we have before adding any element. - Loop over each end index with
for (j <- i until nums.length). The end can't be before the start, anduntilexcludes its upper bound, sojcoversithroughnums.length - 1. - Add the new element:
sum += nums(j), inside the inner loop. - Update the best:
maxSum = math.max(maxSum, sum), which keeps the larger of the two in one line. - Finish with
maxSumon its own line. It is the last expression of the method, so it is the returned value.
The solution in Scala
class Solution {
def maxSumSubarray(nums: Array[Int]): Int = {
var maxSum = nums(0)
for (i <- nums.indices) {
var sum = 0
for (j <- i until nums.length) {
sum += nums(j)
maxSum = math.max(maxSum, sum)
}
}
maxSum
}
}
The Scala details worth a closer look:
- Ranges.
nums.indicesandi until nums.lengthare both ranges ofInt.0 until 3contains 0, 1 and 2;1 to 3would contain 1, 2 and 3. Wheniequalsnums.length,i until nums.lengthis empty, so the inner loop simply doesn't run. - The last expression is the result. There is no
return. If a line after the loops were missing, the method would end with the loop itself, whose value isUnit, and the compiler would reject it because the method promises anInt. math.max. It is available without an import and works onIntvalues, returning anInt.- Array indexing.
nums(j)uses parentheses. An index outside the array throws anArrayIndexOutOfBoundsException. Intoverflow. The sum is a 32-bitInt, and overflow wraps silently instead of raising an error. ForArray(Int.MaxValue, 1)the running sum wraps to a negative number, so the method returns 2147483647 rather than the true sum of 2147483648. The lesson's inputs are small enough that this doesn't matter, but useLongif sums can grow that large.
Complexity
Time: O(n^2). For n elements, the inner loop runs n times for i = 0, n - 1 times for i = 1, and so on down to once. That adds up to n(n + 1) / 2 iterations, each of which does constant work.
Space: O(1). The method uses maxSum, sum and the two loop indexes, whatever the size of nums. The ranges are not stored as lists; they produce their values one at a time.
Edge cases and common mistakes
- All negative numbers.
Array(-3, -1, -2)returns -1: the best subarray is the single element -1. InitializingmaxSumto 0 would return 0, a sum no subarray has. - One element.
Array(4)returns 4. - All positive numbers.
Array(1, 2, 3)returns 6, the whole array. - A negative gap worth crossing.
Array(5, -10, 5)returns 5. Crossing the -10 would cost more than the second 5 gives back. - Forgetting to reset
sum. Ifsumis declared outside the outer loop, each start index inherits the previous total and the results are wrong. - Starting
jat 0. The inner loop must start ati. Otherwise it adds elements that are not part of the subarray beginning ati. - Empty array.
nums(0)would throw, so the method expects at least one element.
Testing
Array(-2, -5, 6, -2, -3, 1, 5, -6)gives7.Array(-3, -1, -2)gives-1.Array(4)gives4.Array(1, 2, 3)gives6.Array(5, -10, 5)gives5.
