Problem overview
The function maxProduct receives a slice nums of integers and returns the largest product of a subarray: a run of one or more elements that stand next to each other. The product is all the elements of the run multiplied together.
For nums = [2, 3, -2, 4] the answer is 6, from the subarray [2, 3]. The only negative number is -2, so every subarray that includes it has a negative product. For nums = [-2, 0, -1] the answer is 0. The two negatives would give 2, but [-2, -1] is not a subarray, because a 0 stands between them. For nums = [-2, 3, -4] the answer is 24: the whole slice, where the two negatives cancel out.
Approach: track the best and the worst product ending here
Trying every subarray takes O(n²) time, and the statement asks for O(n) time and O(1) extra space. The solution makes one pass and keeps, for the current element, two numbers:
maxHere: the largest product of a subarray that ends at the current element;minHere: the smallest product of a subarray that ends at the current element.
The smallest product matters because of negative numbers. Multiplying by a negative number flips the order: the most negative product becomes the largest. A subarray with a very negative product can turn into the winner when the next element is negative.
At each element x = nums[i], a subarray ending there either starts fresh at x or extends one that ends at the previous element. Starting fresh is how a product recovers after a 0, since a 0 turns every longer product into 0. So the new maxHere is the larger of x and maxHere * x, and the new minHere is the smaller of x and minHere * x. When x is negative, the roles of the two old values swap first: the old minimum times a negative number gives the new maximum candidate. The best answer so far, result, is the largest maxHere seen.
The trace for [-2, 3, -4] shows the flip. The values after each element are:
start maxHere -2 minHere -2 result -2
i=1, x=3 maxHere 3 minHere -6 result 3
i=2, x=-4 maxHere 24 minHere -12 result 24
At the last element, the old minHere of -6 times -4 gives the 24.
Step by step
- Create
maxHere := nums[0]. At the first element, the only subarray ending there is the element itself. - Create
minHere := nums[0]the same way. - Create
result := nums[0]: the largest product found so far. A subarray needs at least one element, so the first element alone is a valid start, even when it is negative. - Loop over the rest with
for i := 1; i < len(nums); i++. Element 0 is already counted. - Swap on a negative. If
nums[i] < 0, runmaxHere, minHere = minHere, maxHere. After the swap, each variable is the right one to multiply bynums[i]. - Update
maxHeretomax(nums[i], maxHere*nums[i]): start fresh or extend. - Update
minHeretomin(nums[i], minHere*nums[i]), the same two choices, keeping the smallest. - Update
resulttomax(result, maxHere). The best subarray ends at some element, so looking at the best one ending at each element finds it. - Return
resultafter the loop.
The solution in Go
func maxProduct(nums []int) int {
maxHere := nums[0]
minHere := nums[0]
result := nums[0]
for i := 1; i < len(nums); i++ {
if nums[i] < 0 {
maxHere, minHere = minHere, maxHere
}
maxHere = max(nums[i], maxHere*nums[i])
minHere = min(nums[i], minHere*nums[i])
result = max(result, maxHere)
}
return result
}
The Go details worth knowing:
- Tuple assignment.
maxHere, minHere = minHere, maxHereevaluates both right-hand values before it assigns either, so the two variables swap without a third one. Writing the swap as two separate statements,maxHere = minHereand thenminHere = maxHere, would leave both variables equal to the oldminHere. - Built-in
maxandmin. Since Go 1.21 these are built-in functions for ordered types, so no import or helper is needed. - Order of the updates.
minHereis updated aftermaxHere, and its formula uses onlyminHereandnums[i], not the newmaxHere. Both formulas use the values after the swap, so the order between these two lines does not matter. - Integer size.
intis 64 bits in Go, and the statement promises that every subarray product fits in 32 bits. The intermediate productsmaxHere*nums[i]are products of a subarray too, so they never overflow. - Empty slices.
nums[0]panics on an empty slice. The lesson's constraints guarantee at least one element.
Complexity
Time: O(n). The loop makes one pass, and each round does a constant amount of work: one comparison, one swap and a few multiplications.
Space: O(1). The function uses three integers, whatever the length of nums.
Edge cases and common mistakes
- A single negative element.
[-3]returns-3, not 0. That is whyresultstarts atnums[0]and not at 0. - A zero splits the problem.
[-2, 0, -1]returns0. At the 0, both totals become 0, and the next element starts a fresh run. - Two negatives.
[-2, -3, -4]returns12, from the last two elements.[-1, -1]returns1. - A negative in the middle.
[3, -1, 4]returns4: the negative element is better left out.[2, -5, -2, -4, 3]returns24. - Keeping only the maximum. Tracking just
maxHerefails on[-2, 3, -4], where the winner comes from a negative product that the next negative turns around. - Forgetting the fresh start. Without
nums[i]as an option inmax, a 0 would force every later product to stay 0. - Skipping the swap. With a negative
nums[i], multiplyingmaxHereby it would give a small number, and the real maximum candidate,minHere * nums[i], would never be tried. - Starting
resultat 0. It returns a wrong answer whenever every element is negative.
Testing
nums = [2, 3, -2, 4]gives6.nums = [-2, 0, -1]gives0.nums = [-2, 3, -4]gives24.nums = [-3]gives-3.nums = [0, 2]gives2.nums = [-2, -3, -4]gives12.nums = [3, -1, 4]gives4.nums = [2, -5, -2, -4, 3]gives24.
