Learn/DSA Patterns
DSA PatternsArrays & Two Pointerseasy13 min read

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.

#suffix-sum#prefix-product#arrays#two-pass#beginner#interview
Table of contents

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 array output such that output[i] is the product of all elements except nums[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 leftoutput[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 rightoutput[3] = 6 × 1 = 6.   right = right × nums[3] = 1×4 = 4
i=2:  right = 4output[2] = 2 × 4 = 8.   right = 4×3 = 12
i=1:  right = 12output[1] = 1 × 12 = 12.  right = 12×2 = 24
i=0:  right = 24output[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 i holds the product of 0..i-1 (updated after recording output[i]).
  • The right variable at position i holds the product of i+1..n-1 (updated after multiplying into output[i]).

The "update after use" ordering is the key — and it's deliberate.


Why is it so fast?

PhaseWork
Left passO(n) — one multiplication per element
Right passO(n) — one multiplication per element
TotalO(n), two passes
Extra spaceO(1) (two variables)

The deeper idea — the two-pass framework

Prefix sum and suffix sum are the same concept at different scales:

PrefixSuffix
DirectionLeft → rightRight → left
RecordsRunning aggregate from the leftRunning aggregate from the right
At position i"Everything to my left""Everything to my right"
Combineprefix[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] = left first, then left *= nums[i]. Swapping those two lines includes nums[i] in its own product — the exact bug we're avoiding.
  • Same bug in the right pass. Write output[i] *= right first, then right *= nums[i].
  • Using two separate arrays. You can use a prefix[] and suffix[] array first to understand the idea, but the O(1)-space version re-uses output and 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, so output[i] = 0 for all except the zeros' positions — the edge cases multiply. The two-pass approach sidesteps all of this cleanly.
  • Forgetting to initialise output with 1s. If you initialise with 0, every multiplication in the right pass will multiply by 0. Multiplication's identity is 1.
BugFix
left *= nums[i] before output[i] = leftAlways assign then update: record first, update after
Using divisionUse the two-pass approach — handles zeros, handles the no-division constraint
output = [0] * nUse 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

ProblemDifficultyWhat to noticeLink
Product of Array Except SelfMediumTwo-pass; update after useLC #238
Trapping Rain WaterHardLeft-max pass + right-max pass; min to combineLC #42
CandyHardLeft-pass by left-neighbour; right-pass by right-neighbour; max to combineLC #135
Sum of Subarray MinimumsMediumLeft/right "nearest smaller" passes with monotonic stackLC #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.