195 lines
8.0 KiB
YAML
195 lines
8.0 KiB
YAML
title: Product of Array Except Self
|
||
slug: product-of-array-except-self
|
||
difficulty: medium
|
||
leetcode_id: 238
|
||
leetcode_url: https://leetcode.com/problems/product-of-array-except-self/
|
||
categories:
|
||
- arrays
|
||
patterns:
|
||
- slug: prefix-sum
|
||
is_optimal: true
|
||
|
||
function_signature: "def product_except_self(nums: list[int]) -> list[int]:"
|
||
|
||
test_cases:
|
||
visible:
|
||
- input: { nums: [1, 2, 3, 4] }
|
||
expected: [24, 12, 8, 6]
|
||
- input: { nums: [-1, 1, 0, -3, 3] }
|
||
expected: [0, 0, 9, 0, 0]
|
||
hidden:
|
||
- input: { nums: [2, 3] }
|
||
expected: [3, 2]
|
||
- input: { nums: [1, 1, 1, 1] }
|
||
expected: [1, 1, 1, 1]
|
||
- input: { nums: [0, 0] }
|
||
expected: [0, 0]
|
||
- input: { nums: [5, -2, 4] }
|
||
expected: [-8, 20, -10]
|
||
- input: { nums: [1, 2, 3, 4, 5] }
|
||
expected: [120, 60, 40, 30, 24]
|
||
- input: { nums: [0, 1, 2, 3] }
|
||
expected: [6, 0, 0, 0]
|
||
- input: { nums: [-1, -1, -1] }
|
||
expected: [1, 1, 1]
|
||
|
||
description: |
|
||
Given an integer array `nums`, return an array `answer` such that `answer[i]` is equal to the product of all the elements of `nums` except `nums[i]`.
|
||
|
||
The product of any prefix or suffix of `nums` is **guaranteed** to fit in a **32-bit** integer.
|
||
|
||
You must write an algorithm that runs in **O(n)** time and **without using the division operation**.
|
||
|
||
constraints: |
|
||
- `2 <= nums.length <= 10^5`
|
||
- `-30 <= nums[i] <= 30`
|
||
- The product of any prefix or suffix of `nums` is guaranteed to fit in a 32-bit integer
|
||
|
||
examples:
|
||
- input: "nums = [1,2,3,4]"
|
||
output: "[24,12,8,6]"
|
||
explanation: "answer[0] = 2×3×4 = 24, answer[1] = 1×3×4 = 12, answer[2] = 1×2×4 = 8, answer[3] = 1×2×3 = 6"
|
||
- input: "nums = [-1,1,0,-3,3]"
|
||
output: "[0,0,9,0,0]"
|
||
explanation: "Any position except index 2 includes the zero, making the product 0. Position 2 excludes the zero: (-1)×1×(-3)×3 = 9"
|
||
|
||
explanation:
|
||
intuition: |
|
||
For each position `i`, we need the product of everything **except** `nums[i]`. If division were allowed, we could compute the total product and divide by `nums[i]`. But division is forbidden (and would fail with zeros anyway).
|
||
|
||
Think of it differently: the product of "everything except position i" equals:
|
||
- (product of everything **left** of i) × (product of everything **right** of i)
|
||
|
||
This is the **prefix-suffix** insight! For each position:
|
||
- **Prefix product**: multiply all elements from the start up to (but not including) position i
|
||
- **Suffix product**: multiply all elements from position i+1 to the end
|
||
|
||
If we can compute both efficiently, we just multiply them together.
|
||
|
||
The clever optimisation: we can use the output array itself to store prefix products in a left-to-right pass, then multiply in suffix products with a right-to-left pass using a single running variable.
|
||
|
||
approach: |
|
||
We solve this using **Two-Pass Prefix/Suffix Products**:
|
||
|
||
**Step 1: Initialise the answer array**
|
||
|
||
- Create `answer` of length n, initialised to 1
|
||
- This array will hold our final results
|
||
|
||
|
||
|
||
**Step 2: Left pass — compute prefix products**
|
||
|
||
- Track `left_product = 1` (product of elements to the left)
|
||
- For each `i` from 0 to n-1:
|
||
- Set `answer[i] = left_product` (product of all elements before i)
|
||
- Update `left_product *= nums[i]` (include current element for next position)
|
||
|
||
|
||
|
||
**Step 3: Right pass — multiply by suffix products**
|
||
|
||
- Track `right_product = 1` (product of elements to the right)
|
||
- For each `i` from n-1 down to 0:
|
||
- Multiply `answer[i] *= right_product` (now contains prefix × suffix)
|
||
- Update `right_product *= nums[i]` (include current element for next position)
|
||
|
||
|
||
|
||
**Step 4: Return the answer**
|
||
|
||
- Each `answer[i]` now contains the product of all elements except `nums[i]`
|
||
|
||
|
||
|
||
Example walkthrough with `[1,2,3,4]`:
|
||
- After left pass: `[1, 1, 2, 6]` (prefix products)
|
||
- After right pass: `[24, 12, 8, 6]` (prefix × suffix)
|
||
|
||
common_pitfalls:
|
||
- title: Using Division
|
||
description: |
|
||
The problem explicitly forbids the division operator. Even if allowed, division fails when the array contains zeros (division by zero).
|
||
|
||
The prefix-suffix approach works without division and handles zeros naturally.
|
||
wrong_approach: "total_product // nums[i]"
|
||
correct_approach: "Prefix product × suffix product"
|
||
|
||
- title: Using Extra Space for Prefix and Suffix Arrays
|
||
description: |
|
||
A straightforward approach creates separate `prefix[]` and `suffix[]` arrays, using O(n) extra space.
|
||
|
||
The optimal solution reuses the output array for prefix products and uses a single variable for the running suffix product — O(1) extra space.
|
||
wrong_approach: "prefix = []; suffix = [] — O(n) extra space"
|
||
correct_approach: "Use output array for prefix, single variable for suffix"
|
||
|
||
- title: Off-by-One in Product Accumulation
|
||
description: |
|
||
The key insight: update `answer[i]` **before** multiplying `nums[i]` into the running product. This ensures `nums[i]` itself is excluded from the product at position i.
|
||
|
||
If you multiply first, you'll include `nums[i]` in its own product.
|
||
wrong_approach: "left_product *= nums[i]; answer[i] = left_product"
|
||
correct_approach: "answer[i] = left_product; left_product *= nums[i]"
|
||
|
||
key_takeaways:
|
||
- "**Prefix/suffix products**: A powerful pattern for 'exclude current element' problems"
|
||
- "**Two-pass technique**: Build partial results in one direction, complete them in the other"
|
||
- "**Space optimisation**: The output array can store intermediate results, achieving O(1) extra space"
|
||
- "**No division needed**: This approach naturally handles zeros without special cases"
|
||
|
||
time_complexity: "O(n). Two passes through the array, each doing O(1) work per element."
|
||
space_complexity: "O(1). Only two variables (`left_product` and `right_product`) beyond the output array. The output array doesn't count as extra space per the problem statement."
|
||
|
||
solutions:
|
||
- approach_name: Two-Pass with O(1) Space
|
||
is_optimal: true
|
||
code: |
|
||
def product_except_self(nums: list[int]) -> list[int]:
|
||
n = len(nums)
|
||
answer = [1] * n
|
||
|
||
# Left pass: answer[i] = product of all elements to the LEFT of i
|
||
left_product = 1
|
||
for i in range(n):
|
||
answer[i] = left_product
|
||
left_product *= nums[i] # Include nums[i] for positions after i
|
||
|
||
# Right pass: multiply by product of all elements to the RIGHT of i
|
||
right_product = 1
|
||
for i in range(n - 1, -1, -1):
|
||
answer[i] *= right_product
|
||
right_product *= nums[i] # Include nums[i] for positions before i
|
||
|
||
return answer
|
||
explanation: |
|
||
**Time Complexity:** O(n) — Two linear passes.
|
||
|
||
**Space Complexity:** O(1) — Only two extra variables (output array doesn't count).
|
||
|
||
First pass stores prefix products in the answer array. Second pass multiplies each position by the suffix product. The result is the product of all elements except the current one.
|
||
|
||
- approach_name: Separate Prefix/Suffix Arrays
|
||
is_optimal: false
|
||
code: |
|
||
def product_except_self(nums: list[int]) -> list[int]:
|
||
n = len(nums)
|
||
|
||
# Build prefix products: prefix[i] = product of nums[0..i-1]
|
||
prefix = [1] * n
|
||
for i in range(1, n):
|
||
prefix[i] = prefix[i - 1] * nums[i - 1]
|
||
|
||
# Build suffix products: suffix[i] = product of nums[i+1..n-1]
|
||
suffix = [1] * n
|
||
for i in range(n - 2, -1, -1):
|
||
suffix[i] = suffix[i + 1] * nums[i + 1]
|
||
|
||
# Combine: answer[i] = prefix[i] * suffix[i]
|
||
return [prefix[i] * suffix[i] for i in range(n)]
|
||
explanation: |
|
||
**Time Complexity:** O(n) — Three linear passes.
|
||
|
||
**Space Complexity:** O(n) — Two extra arrays for prefix and suffix products.
|
||
|
||
This version is easier to understand. We compute prefix and suffix products separately, then combine them. The optimal version merges these steps to save space.
|