Maximum Product Subarray (LC 152)
On this page
Pattern: Array / Dynamic Tracking of Min & Max
Difficulty: Medium
Key Concept: A negative number can turn a small (most negative) running product into the largest product, so track both the max and min product ending at each index.
Problem Statement
Given an integer array nums, find a contiguous non-empty subarray that has the largest product, and return that product.
Input
nums— integer array
Output
int— maximum product over all contiguous non-empty subarrays
Example
nums = [2, 3, -2, 4]→6(subarray[2, 3]).
1. Algorithm & Pseudocode
Brute force
- Set
besttonums[0]. - For each start
ifrom0ton - 1: - Set
prod = 1. - For each end
jfromiton - 1: - Multiply:
prod = prod * nums[j]. - If
prod > best, setbest = prod. - Return
best.
Optimal
- Initialize
maxSoFar = minSoFar = best = nums[0]. - For
kfrom1ton - 1: - Let
x = nums[k]. - If
x < 0, swapmaxSoFarandminSoFar(because multiplying flips order: large×negative becomes small, small×negative becomes large). - Update
maxSoFar = max(x, maxSoFar * x). - Update
minSoFar = min(x, minSoFar * x). best = max(best, maxSoFar).- Return
best.
2. Step-by-Step Analysis (Beginner-Friendly)
Why brute force is correct
Every contiguous subarray is tried; products are exact (within Java int range—LeetCode assumes results fit in 32-bit signed int).
Why brute force is slow
(O(n^2)) subarrays—too slow for large (n).
Why “max only” (like Kadane for sum) fails
Multiplication by a negative flips the sign. A running product that looks terrible (very negative) can become the best after one more negative number. Example: in [-2, 3, -4], the subarray [-2, 3, -4] has product 24, which you only discover if you remember that -2 * 3 = -6 was a useful “small” intermediate, not just garbage to discard forever.
Why track minSoFar
minSoFar holds the smallest (most negative) product ending here. When x is negative, minSoFar * x can become a huge positive candidate for the answer.
Why swap on negative x
Multiplying both running extremes by a negative reverses their ordering: the old max becomes the “more negative” chain and the old min becomes the “more positive” chain. Swapping aligns variables so the same max(x, prevMax * x) formulas still work.
ASCII — sign flip intuition
... maxSoFar = 6, minSoFar = -2, next x = -3
Without care: 6 * -3 = -18, (-2) * -3 = 6 ← the "min" path wins
After swap (conceptually): treat the old min as the chain that extends to large positive after * negative
3. The Dry Run
Input: nums = [2, 3, -2, 4]
k |
x |
swap? | maxSoFar after |
minSoFar after |
best |
|---|---|---|---|---|---|
| 0 | 2 | — | 2 | 2 | 2 |
| 1 | 3 | no | max(3,6)=6 | min(3,6)=3 | 6 |
| 2 | -2 | yes | max(-2,-6)=-2 | min(-2,-12)=-12 | 6 |
| 3 | 4 | no | max(4,-8)=4 | min(4,-48)=-48 | 6 |
Result: 6.
Zeros: When x == 0, the updates maxSoFar = max(0, …) and minSoFar = min(0, …) effectively reset the running product chain to 0, which matches “start a new subarray here.” LeetCode’s tests include zeros; the listed optimal code handles them without a separate branch.
4. Java Solution
Brute Force
class Solution {
public int maxProduct(int[] nums) {
int n = nums.length;
int best = nums[0];
for (int i = 0; i < n; i++) {
int prod = 1;
for (int j = i; j < n; j++) {
prod *= nums[j];
if (prod > best) {
best = prod;
}
}
}
return best;
}
}
Time: (O(n^2)).
Space: (O(1)).
Optimal
class Solution {
public int maxProduct(int[] nums) {
int maxSoFar = nums[0];
int minSoFar = nums[0];
int best = nums[0];
for (int k = 1; k < nums.length; k++) {
int x = nums[k];
if (x < 0) {
int tmp = maxSoFar;
maxSoFar = minSoFar;
minSoFar = tmp;
}
maxSoFar = Math.max(x, maxSoFar * x);
minSoFar = Math.min(x, minSoFar * x);
best = Math.max(best, maxSoFar);
}
return best;
}
}
Time: (O(n)).
Space: (O(1)).
Note: The swap-on-negative formulation assumes maxSoFar and minSoFar already summarize the previous step; when x == 0, maxSoFar * x and minSoFar * x are both 0, and max(0,0)=0, min(0,0)=0, so the chain resets naturally—best still updates via max(best, 0) when zeros appear. Verify with nums = [-2, 0, -1].
5. The “Java vs. Others” Edge
Math.max/Math.minonintavoid importing anything else; nolongneeded unless you extend to “product fits in long.”- Swapping with a temporary
intis clear and avoids XOR tricks on signed values. - Unlike sum Kadane, watch integer overflow in interviews; LeetCode 152 stays within
intby problem guarantee.
6. Complexity Summary
| Approach | Time | Space | Notes |
|---|---|---|---|
| Brute Force | (O(n^2)) | (O(1)) | Enumerates all subarrays. |
| Optimal | (O(n)) | (O(1)) | Tracks max/min product ending at each index. |