Product of Array Except Self #
Problem Statement #
Given an array nums
of n integers where n > 1, return an array output
such that output[i]
is equal to the product of all the elements of nums
except nums[i]
.
Constraint: It’s guaranteed that the product of the elements of any prefix or suffix of the array (including the whole array) fits in a 32-bit integer.
Note: Please solve it without division and in O(n) time complexity.
Example #
Input: [1,2,3,4]
Output: [24,12,8,6]
Explanation:
- Index 0: 2 × 3 × 4 = 24
- Index 1: 1 × 3 × 4 = 12
- Index 2: 1 × 2 × 4 = 8
- Index 3: 1 × 2 × 3 = 6
Initial Intuition (The Forbidden Approach) #
If we were allowed to use division, this problem would be straightforward:
- Calculate the product of all numbers in the array
- For each index i, divide the total product by
nums[i]
This would give us an O(n) solution with minimal space complexity. However, the constraint explicitly forbids division, forcing us to think more creatively.
The Core Insight #
The key observation is understanding what we’re computing at each position. For any index i
, we need:
result[i] = nums[0] × nums[1] × ... × nums[i-1] × nums[i+1] × ... × nums[n-1]
Notice this can be split into two parts:
- Left product: All elements before index i
- Right product: All elements after index i
So: result[i] = leftProduct[i] × rightProduct[i]
This insight leads us to a solution that avoids repeated calculations by preprocessing the left and right products.
Solution Approach #
Step 1: Calculate Prefix Products #
Create an array where each element contains the product of all elements before it (not including itself).
For input [1,2,3,4]
:
- Index 0: 1 (no elements before)
- Index 1: 1 (only 1 before)
- Index 2: 1 × 2 = 2
- Index 3: 1 × 2 × 3 = 6
Prefix array: [1, 1, 2, 6]
let pre = [];
for (let i = 0; i < nums.length; i++) {
pre[i] = (pre[i-1] === undefined ? 1 : pre[i-1]) * nums[i];
}
What this creates: pre[i]
contains the product of nums[0]
through nums[i]
(inclusive).
Step 2: Calculate Suffix Products #
Create an array where each element contains the product of all elements after it (not including itself).
For input [1,2,3,4]
:
- Index 3: 1 (no elements after)
- Index 2: 4
- Index 1: 3 × 4 = 12
- Index 0: 2 × 3 × 4 = 24
Suffix array: [24, 12, 4, 1]
let post = [];
for (let i = nums.length - 1; i >= 0; i--) {
post[i] = (post[i+1] === undefined ? 1 : post[i+1]) * nums[i];
}
What this creates: post[i]
contains the product of nums[i]
through nums[n-1]
(inclusive).
Step 3: Combine Results #
Now we have:
pre[i-1]
: Product of all elements before index ipost[i+1]
: Product of all elements after index i
Multiply these together to get the product excluding nums[i]
:
let result = [];
for (let i = 0; i < nums.length; i++) {
result[i] = (pre[i-1] === undefined ? 1 : pre[i-1]) *
(post[i+1] === undefined ? 1 : post[i+1]);
}
Visual Walkthrough #
For input [1, 2, 3, 4]
:
Index: 0 1 2 3
nums: 1 2 3 4
pre: 1 2 6 24 (cumulative product left-to-right)
post: 24 12 4 1 (cumulative product right-to-left)
For each index i, we need:
result[i] = pre[i-1] × post[i+1]
Index 0: pre[-1] × post[1] = 1 × 12 = 12... wait, that's wrong!
Let me recalculate:
Index 0: (1) × post[1] = 1 × 12 = 12...
Actually, let me trace through more carefully with the actual values:
nums = [1, 2, 3, 4]
After pre loop:
pre[0] = 1 * 1 = 1
pre[1] = 1 * 2 = 2
pre[2] = 2 * 3 = 6
pre[3] = 6 * 4 = 24
pre = [1, 2, 6, 24]
After post loop:
post[3] = 1 * 4 = 4
post[2] = 4 * 3 = 12
post[1] = 12 * 2 = 24
post[0] = 24 * 1 = 24
post = [24, 24, 12, 4]
For result:
result[0] = (undefined → 1) * post[1] = 1 * 24 = 24 ✓
result[1] = pre[0] * post[2] = 1 * 12 = 12 ✓
result[2] = pre[1] * post[3] = 2 * 4 = 8 ✓
result[3] = pre[2] * (undefined → 1) = 6 * 1 = 6 ✓
result = [24, 12, 8, 6]
Complete Solution #
var productExceptSelf = function(nums) {
// Create array of cumulative products from left to right
let pre = [];
for (let i = 0; i < nums.length; i++) {
pre[i] = (pre[i-1] === undefined ? 1 : pre[i-1]) * nums[i];
}
// Create array of cumulative products from right to left
let post = [];
for (let i = nums.length - 1; i >= 0; i--) {
post[i] = (post[i+1] === undefined ? 1 : post[i+1]) * nums[i];
}
// Combine left and right products
let result = [];
for (let i = 0; i < nums.length; i++) {
result[i] = (pre[i-1] === undefined ? 1 : pre[i-1]) *
(post[i+1] === undefined ? 1 : post[i+1]);
}
return result;
};
Complexity Analysis #
Time Complexity: O(n) #
- First loop: O(n) to build prefix product array
- Second loop: O(n) to build suffix product array
- Third loop: O(n) to combine results
- Total: O(3n) = O(n)
Space Complexity: O(n) #
pre
array: O(n)post
array: O(n)result
array: O(n) (required for output)- Total auxiliary space (not counting output): O(2n) = O(n)
Why the Undefined Checks? #
The conditional (pre[i-1] === undefined ? 1 : pre[i-1])
handles edge cases:
- At index 0: There’s no element before it (
pre[-1]
is undefined), so we use 1 (multiplicative identity) - At index n-1: There’s no element after it (
post[n]
is undefined), so we use 1
Using 1 as the default ensures these boundary cases don’t affect the multiplication.
Alternative Approaches #
Space-Optimized Solution (O(1) Extra Space) #
We can reduce space complexity by building the result array in-place:
var productExceptSelf = function(nums) {
const n = nums.length;
const result = new Array(n);
// Build prefix products directly into result
result[0] = 1;
for (let i = 1; i < n; i++) {
result[i] = result[i-1] * nums[i-1];
}
// Build suffix products and multiply with prefix
let rightProduct = 1;
for (let i = n - 1; i >= 0; i--) {
result[i] *= rightProduct;
rightProduct *= nums[i];
}
return result;
};
This achieves O(1) extra space (not counting the output array) while maintaining O(n) time complexity.
Key Takeaways #
- Pattern Recognition: Breaking the problem into prefix and suffix products is the key insight
- Avoid Redundancy: Pre-computing products eliminates repeated calculations
- Edge Cases: Handle boundaries carefully with appropriate default values
- Space-Time Tradeoffs: Multiple solutions exist with different space complexities
- Constraints Matter: The “no division” constraint forces creative problem-solving
Common Pitfalls #
- Off-by-one errors: Be careful with
i-1
andi+1
indices - Forgetting edge cases: Always handle the first and last elements specially
- Integer overflow: The problem guarantees no overflow, but be aware in similar problems
- Modifying input: Some variations ask for in-place modification; read requirements carefully
Practice Tips #
To master this problem:
- Draw out the arrays visually for small examples
- Trace through each loop iteration manually
- Understand why we need both forward and backward passes
- Practice the space-optimized version
- Consider what changes if division were allowed
This problem teaches fundamental techniques applicable to many array manipulation challenges, particularly those involving cumulative operations and the prefix-suffix pattern.