Suffix Sum — Product of Array Except Self
Precompute from the right end, then combine with a left pass to answer each position's question in O(1). We build the idea from a recipe story, derive the classic no-division product trick, and master the two-pass framework.
Table of contents
- Before we start
- Picture this first (no code yet)
- The actual problem
- First, the slow way (so you feel the pain)
- The turning point
- The one idea to remember
- Watch it happen, frame by frame
- Now, the code — line by line
- Why does it never go wrong?
- Why is it so fast?
- The deeper idea — the two-pass framework
- When should I reach for this? (the trigger list)
- The same trick in three disguises
- Disguise 1 — Trapping Rain Water (revisited differently)
- Disguise 2 — Sum of Subarray Minimums (LC #907)
- Disguise 3 — Candy (LC #135)
- Traps that catch beginners
- Say it like a pro (interview one-liner)
- Check yourself
- Practice problems
Before we start
You just learned that a prefix precomputes from the left. This chapter adds the mirror image: precompute from the right — and then shows what happens when you combine the two. By the end you will be able to:
- See the left-pass / right-pass two-pass framework as one mental picture.
- Explain why the no-division product trick is equivalent to division, but safer.
- Recognise problems that need information from both sides of a position simultaneously.
Stop at every Pause & Think box. The insight there is what separates people who memorise this solution from people who understand it.
Picture this first (no code yet)
A real-life story
You are a baker. You have a long assembly line of ingredients laid out in order:
[flour, eggs, sugar, butter, vanilla, salt]
Each product you bake omits exactly one ingredient (for different dietary restrictions). A customer orders the "no-sugar" version — so you need the product of everything except sugar.
Here's the clever thing your head baker teaches you: instead of multiplying all six ingredients and dividing by sugar (which fails when sugar = 0), you split the work into two passes.
Left pass: Walk from left to right, and at each station, write down "the product of everything to my left."
- flour station: nothing to the left → 1
- eggs station: just flour → flour
- sugar station: flour × eggs
- butter station: flour × eggs × sugar
- ...
Right pass: Walk from right to left, and at each station, write down "the product of everything to my right."
- salt station: nothing to the right → 1
- vanilla station: just salt → salt
- butter station: salt × vanilla
- ...
For any ingredient, multiply its left product × right product — that's the product of everything except itself. No division. No zero problems. Two passes.
That two-pass assembly line is the pattern. Let's now see it solve a real problem.
The actual problem
Given an integer array
nums, return an arrayoutputsuch thatoutput[i]is the product of all elements exceptnums[i]. Do it in O(n) time, O(1) extra space (not counting the output), and without division.
nums = [1, 2, 3, 4]
output = [24, 12, 8, 6]
because:
output[0] = 2×3×4 = 24
output[1] = 1×3×4 = 12
output[2] = 1×2×4 = 8
output[3] = 1×2×3 = 6
The "no division" constraint rules out the obvious shortcut (total product ÷ nums[i]). More importantly, division breaks when any element is 0. The two-pass approach handles zeros naturally.
First, the slow way (so you feel the pain)
def product_except_self_slow(nums):
n = len(nums)
output = []
for i in range(n):
product = 1
for j in range(n):
if j != i:
product *= nums[j]
output.append(product)
return output
For each of the n positions, you multiply the remaining n-1 elements — O(n) work per position, O(n²) total.
n = 10 → ~100 multiplications
n = 10,000 → ~100,000,000 multiplications
n = 100,000 → ~10,000,000,000 multiplications (ten billion — painful)
The turning point
Pause & think
For position i, you need: nums[0] × nums[1] × … × nums[i-1] × nums[i+1] × … × nums[n-1].
You already know how to compute "the product of everything from 0 to i-1" with one left-to-right pass. That's just a prefix product — exactly like the prefix sum from the last chapter, but with multiplication.
What would you compute in a right-to-left pass to cover the "right half" (everything from i+1 to n-1)?
You'd compute a suffix product: suffix[i] = product of nums[i+1] through nums[n-1]. Then:
output[i] = prefix_product[i-1] × suffix_product[i+1]
= (everything to the left) × (everything to the right)
That's the whole algorithm. Two passes, no division.
The one idea to remember
The entire pattern in one sentence
For each position, multiply what's to its left (from a left-to-right pass) by what's to its right (from a right-to-left pass) — no element multiplies itself, so division is never needed.
Watch it happen, frame by frame
nums = [1, 2, 3, 4]
Left pass — fill output[i] with the product of everything LEFT of i:
i=0: nothing to the left → output[0] = 1
i=1: nums[0] → output[1] = 1
i=2: nums[0]×nums[1] → output[2] = 1×2 = 2
i=3: nums[0]×nums[1]×nums[2] → output[3] = 1×2×3 = 6
output after left pass: [1, 1, 2, 6]
Right pass — multiply each output[i] by the product of everything RIGHT of i:
We track a running right variable (starts at 1) as we scan from right to left.
i=3: nothing to the right → output[3] = 6 × 1 = 6. right = right × nums[3] = 1×4 = 4
i=2: right = 4 → output[2] = 2 × 4 = 8. right = 4×3 = 12
i=1: right = 12 → output[1] = 1 × 12 = 12. right = 12×2 = 24
i=0: right = 24 → output[0] = 1 × 24 = 24. right = 24×1 = 24
output after right pass: [24, 12, 8, 6] ✅
Pause & think
Cover the trace below. Apply the same two-pass approach to nums = [2, 3, 4, 5]. What does output look like after the left pass? After the right pass?
Check your trace
Left pass:
i=0: output[0] = 1
i=1: output[1] = 2
i=2: output[2] = 2×3 = 6
i=3: output[3] = 2×3×4 = 24
output = [1, 2, 6, 24]
Right pass (right starts at 1):
i=3: output[3] = 24×1 = 24, right = 5
i=2: output[2] = 6×5 = 30, right = 5×4 = 20
i=1: output[1] = 2×20 = 40, right = 20×3 = 60
i=0: output[0] = 1×60 = 60, right = 60×2 = 120
output = [60, 40, 30, 24] ✅
Verify: 3×4×5=60 ✓, 2×4×5=40 ✓, 2×3×5=30 ✓, 2×3×4=24 ✓
Now, the code — line by line
def productExceptSelf(nums):
n = len(nums)
output = [1] * n # initialise with 1s — multiplication's identity
# Left pass: output[i] = product of all elements to the LEFT of i
left = 1
for i in range(n):
output[i] = left # nothing to the left of i yet (left accumulates BEFORE update)
left *= nums[i] # update left to include nums[i] for the NEXT position
# Right pass: multiply output[i] by product of all elements to the RIGHT of i
right = 1
for i in range(n - 1, -1, -1):
output[i] *= right # fold in the right-side product
right *= nums[i] # update right to include nums[i] for the NEXT (leftward) position
return output
Mapping to the bakery:
output[i] = left— "Write down how much was multiplied on my left so far."left *= nums[i]— "Include this ingredient in the left product for the next station."output[i] *= right— "Fold in how much was multiplied on my right so far."right *= nums[i]— "Include this ingredient in the right product for the next (leftward) station."
Why O(1) extra space? We use only two integer variables (left, right). The output array itself doesn't count toward "extra" space — it's the required return value.
Why does it never go wrong?
For any index i, after both passes:
output[i]
= (product of nums[0..i-1]) ← set in the left pass
× (product of nums[i+1..n-1]) ← multiplied in the right pass
= product of everything except nums[i]
nums[i] is never included because:
- The left variable at position
iholds the product of0..i-1(updated after recordingoutput[i]). - The right variable at position
iholds the product ofi+1..n-1(updated after multiplying intooutput[i]).
The "update after use" ordering is the key — and it's deliberate.
Why is it so fast?
| Phase | Work |
|---|---|
| Left pass | O(n) — one multiplication per element |
| Right pass | O(n) — one multiplication per element |
| Total | O(n), two passes |
| Extra space | O(1) (two variables) |
The deeper idea — the two-pass framework
Prefix sum and suffix sum are the same concept at different scales:
| Prefix | Suffix | |
|---|---|---|
| Direction | Left → right | Right → left |
| Records | Running aggregate from the left | Running aggregate from the right |
| At position i | "Everything to my left" | "Everything to my right" |
| Combine | prefix[i] op suffix[i] = answer excluding position i |
The two-pass framework applies whenever:
- You need something from the left AND something from the right of each position simultaneously.
- You can't just loop twice naively (that's O(n²)).
- A single left-to-right pass followed by a single right-to-left pass gives you both pieces in O(n).
This framework shows up in: trapping rain water (prefix-max + suffix-max), minimum domino rotations, jump game variants, and many more.
When should I reach for this? (the trigger list)
Reach for the two-pass / suffix pattern when you notice:
- Each position's answer depends on elements to its left AND elements to its right.
- The naive approach would be O(n²) because of nested loops.
- The words "except itself," "all others," or "neighbours" appear.
- You've already used a left pass and realise you still need the right side — that's the signal.
- The problem explicitly says no division (your first instinct might be prefix product ÷ self — block it).
The same trick in three disguises
Disguise 1 — Trapping Rain Water (revisited differently)
Water trapped at position i = min(max_left, max_right) - height[i].
- Left pass: For each
i, record the maximum height seen so far from the left. - Right pass: For each
i, record the maximum height seen so far from the right. - Combine:
water[i] = min(left_max[i], right_max[i]) - height[i].
Same left-pass / right-pass / combine skeleton. Different operation (max instead of product).
Disguise 2 — Sum of Subarray Minimums (LC #907)
For each element, find the "span" of subarrays for which it is the minimum — done with a left pass (previous smaller element) and a right pass (next smaller element). Two-pass framework with monotonic stacks.
Disguise 3 — Candy (LC #135)
Assign minimum candies to children based on rating comparisons with neighbours:
- Left pass: Assign candies based on "is my rating higher than my left neighbour?"
- Right pass: Adjust candies based on "is my rating higher than my right neighbour?"
- Combine:
max(left_candy[i], right_candy[i]).
Identical skeleton — left pass, right pass, combine with an element-wise operation.
Traps that catch beginners
Watch out for these
- Updating before using. In the left pass, you must write
output[i] = leftfirst, thenleft *= nums[i]. Swapping those two lines includesnums[i]in its own product — the exact bug we're avoiding. - Same bug in the right pass. Write
output[i] *= rightfirst, thenright *= nums[i]. - Using two separate arrays. You can use a
prefix[]andsuffix[]array first to understand the idea, but the O(1)-space version re-usesoutputand two variables. Know both. - Attempting division. Division fails when any
nums[i] == 0(division by zero). There can also be two zeros, making total product = 0, sooutput[i] = 0for all except the zeros' positions — the edge cases multiply. The two-pass approach sidesteps all of this cleanly. - Forgetting to initialise
outputwith 1s. If you initialise with 0, every multiplication in the right pass will multiply by 0. Multiplication's identity is 1.
| Bug | Fix |
|---|---|
left *= nums[i] before output[i] = left | Always assign then update: record first, update after |
| Using division | Use the two-pass approach — handles zeros, handles the no-division constraint |
output = [0] * n | Use output = [1] * n — multiplication's identity |
Say it like a pro (interview one-liner)
"I'll use two passes. First, a left-to-right pass where I store the product of all elements to the left of each index. Then, a right-to-left pass where I multiply in the product of all elements to the right. Each index ends up with the product of everything except itself — O(n) time and O(1) extra space, with no division."
Remember this forever
Suffix Sum — Two-Pass Framework
Left pass: fill output[i] with the product (or sum, or max…) of everything to the LEFT.
Right pass: multiply (or combine) output[i] with the running aggregate from the RIGHT.
Assign then update — always record before you advance the running variable.
Cost: O(n) time, O(1) extra space · Trigger: each position needs something from both its left and its right simultaneously · Skeleton: left pass then right pass, combine in output
Key trap: Update the running variable after using it, not before.
Check yourself
Why must you write `output[i] = left` before `left *= nums[i]`?
left should hold the product of everything strictly to the left of i — that is, nums[0..i-1], not including nums[i] itself. If you updated left first, it would include nums[i], and output[i] would be the product of nums[0..i] — including the current element. That defeats the whole purpose.
Why doesn't division work when there's a zero in the array?
The division approach computes total_product / nums[i]. If nums[i] = 0, that's division by zero — undefined. If there are two zeros in the array, total_product = 0 for all positions, so you can't recover what the non-zero products are just by dividing. The two-pass approach avoids division entirely and handles any number of zeros naturally.
What operation replaces multiplication in the "Trapping Rain Water" application of this pattern?
max instead of ×. The left pass computes a running maximum (the tallest bar seen from the left); the right pass computes a running maximum from the right. At each position, water = min(left_max, right_max) - height. The framework — "record from the left, record from the right, combine" — is identical.
Practice problems
| Problem | Difficulty | What to notice | Link |
|---|---|---|---|
| Product of Array Except Self | Medium | Two-pass; update after use | LC #238 |
| Trapping Rain Water | Hard | Left-max pass + right-max pass; min to combine | LC #42 |
| Candy | Hard | Left-pass by left-neighbour; right-pass by right-neighbour; max to combine | LC #135 |
| Sum of Subarray Minimums | Medium | Left/right "nearest smaller" passes with monotonic stack | LC #907 |
Next up: Kadane's Algorithm, where we discover that you only need to remember one number — the best sum ending at the current position — to find the maximum subarray in a single pass.