Skip to content

Prefix Sums and Difference Arrays

If a problem asks the same question — “what’s the sum of this range?” — over and over on a fixed array, recomputing the sum each time is wasted work. Precompute once, answer forever: that’s the entire idea behind a prefix sum, and it’s one of the highest-leverage O(n)O(n)-setup tricks in all of interview coding.

What you’ll learn

  • Building a prefix sum array in O(n)O(n), then answering any range-sum query in O(1)O(1).
  • Extending prefix sums to 2D for rectangle-sum queries over a matrix.
  • The difference array — the mirror-image trick for applying many range updates efficiently.
  • The hashmap + prefix sum combo that turns “subarray sums equal to k” from O(n2)O(n^2) into O(n)O(n).

The pattern: precompute once, query in O(1)

A prefix sum array prefixprefix stores running totals, with prefix[0] = 0prefix[0] = 0 and prefix[i] = nums[0] + nums[1] + ... + nums[i-1]prefix[i] = nums[0] + nums[1] + ... + nums[i-1]. The sum of any range [left, right][left, right] (inclusive) is then just a subtraction:

sum(left,right)=prefix[right+1]prefix[left]\text{sum}(left, right) = \text{prefix}[right + 1] - \text{prefix}[left]
range_sum_query.py
class NumArray:
    def __init__(self, nums):
        self.prefix = [0] * (len(nums) + 1)
        for i, x in enumerate(nums):
            self.prefix[i + 1] = self.prefix[i] + x   # O(n) build, once
 
    def sum_range(self, left, right):
        return self.prefix[right + 1] - self.prefix[left]   # O(1) per query
 
 
arr = NumArray([-2, 0, 3, -5, 2, -1])
print(arr.sum_range(0, 2))   # expect 1
print(arr.sum_range(2, 5))   # expect -1
print(arr.sum_range(0, 5))   # expect -3
range_sum_query.py
class NumArray:
    def __init__(self, nums):
        self.prefix = [0] * (len(nums) + 1)
        for i, x in enumerate(nums):
            self.prefix[i + 1] = self.prefix[i] + x   # O(n) build, once
 
    def sum_range(self, left, right):
        return self.prefix[right + 1] - self.prefix[left]   # O(1) per query
 
 
arr = NumArray([-2, 0, 3, -5, 2, -1])
print(arr.sum_range(0, 2))   # expect 1
print(arr.sum_range(2, 5))   # expect -1
print(arr.sum_range(0, 5))   # expect -3

The +1+1 offset is what makes sum_rangesum_range subtraction-only, with no special case for left == 0left == 0: prefix[0] = 0prefix[0] = 0 acts as “the sum of nothing before the array starts.”

How it works

diagram Prefix sums: nums vs. the running-total array mermaid

Each prefix[i]prefix[i] folds in one more element than the last — once that chain is built, any range sum is just two array lookups and a subtraction, no matter how wide the range.

Worked example: Subarray Sum Equals K

Count subarrays summing to exactly kk. The brute-force way checks every (left, right)(left, right) pair, O(n2)O(n^2). The prefix-sum trick: a subarray nums[i+1..j]nums[i+1..j] sums to kk exactly when prefix[j] - prefix[i] == kprefix[j] - prefix[i] == k — so for every new running sum, look up how many earlier prefix sums are exactly kk less, using a hashmap instead of a nested loop.

subarray_sum_equals_k.py
def subarray_sum_equals_k(nums, k):
    count = 0
    prefix_sum = 0
    seen = {0: 1}   # empty prefix (sum 0) has been "seen" once, before the array starts
 
    for x in nums:
        prefix_sum += x
        count += seen.get(prefix_sum - k, 0)          # how many earlier prefixes make this a valid subarray?
        seen[prefix_sum] = seen.get(prefix_sum, 0) + 1  # record the current running sum
 
    return count
 
 
print(subarray_sum_equals_k([1, 1, 1], 2))   # expect 2
print(subarray_sum_equals_k([1, 2, 3], 3))   # expect 2
subarray_sum_equals_k.py
def subarray_sum_equals_k(nums, k):
    count = 0
    prefix_sum = 0
    seen = {0: 1}   # empty prefix (sum 0) has been "seen" once, before the array starts
 
    for x in nums:
        prefix_sum += x
        count += seen.get(prefix_sum - k, 0)          # how many earlier prefixes make this a valid subarray?
        seen[prefix_sum] = seen.get(prefix_sum, 0) + 1  # record the current running sum
 
    return count
 
 
print(subarray_sum_equals_k([1, 1, 1], 2))   # expect 2
print(subarray_sum_equals_k([1, 2, 3], 3))   # expect 2

The same seenseen hashmap trick (running value -> how many times seen) solves Contiguous Array too: treat each 00 as -1-1 and each 11 as +1+1, then the longest subarray with equal 00s and 11s is the longest gap between two indices sharing the same running sum.

2D prefix sums: rectangle-sum queries

The same idea extends one dimension further. prefix[r][c]prefix[r][c] holds the sum of every cell in the rectangle from (0, 0)(0, 0) to (r-1, c-1)(r-1, c-1). Building it uses inclusion-exclusion to avoid double-counting the overlapping corner; querying a rectangle does the same subtraction in reverse.

range_sum_query_2d.py
class NumMatrix:
    def __init__(self, matrix):
        rows, cols = len(matrix), len(matrix[0])
        self.prefix = [[0] * (cols + 1) for _ in range(rows + 1)]
 
        for r in range(rows):
            for c in range(cols):
                self.prefix[r + 1][c + 1] = (
                    matrix[r][c]
                    + self.prefix[r][c + 1]      # sum above
                    + self.prefix[r + 1][c]      # sum to the left
                    - self.prefix[r][c]           # remove double-counted corner
                )
 
    def sum_region(self, row1, col1, row2, col2):
        p = self.prefix
        return (
            p[row2 + 1][col2 + 1]
            - p[row1][col2 + 1]
            - p[row2 + 1][col1]
            + p[row1][col1]
        )
 
 
matrix = [
    [3, 0, 1, 4, 2],
    [5, 6, 3, 2, 1],
    [1, 2, 0, 1, 5],
    [4, 1, 0, 1, 7],
    [1, 0, 3, 0, 5],
]
nm = NumMatrix(matrix)
print(nm.sum_region(2, 1, 4, 3))   # expect 8
print(nm.sum_region(1, 1, 2, 2))   # expect 11
range_sum_query_2d.py
class NumMatrix:
    def __init__(self, matrix):
        rows, cols = len(matrix), len(matrix[0])
        self.prefix = [[0] * (cols + 1) for _ in range(rows + 1)]
 
        for r in range(rows):
            for c in range(cols):
                self.prefix[r + 1][c + 1] = (
                    matrix[r][c]
                    + self.prefix[r][c + 1]      # sum above
                    + self.prefix[r + 1][c]      # sum to the left
                    - self.prefix[r][c]           # remove double-counted corner
                )
 
    def sum_region(self, row1, col1, row2, col2):
        p = self.prefix
        return (
            p[row2 + 1][col2 + 1]
            - p[row1][col2 + 1]
            - p[row2 + 1][col1]
            + p[row1][col1]
        )
 
 
matrix = [
    [3, 0, 1, 4, 2],
    [5, 6, 3, 2, 1],
    [1, 2, 0, 1, 5],
    [4, 1, 0, 1, 7],
    [1, 0, 3, 0, 5],
]
nm = NumMatrix(matrix)
print(nm.sum_region(2, 1, 4, 3))   # expect 8
print(nm.sum_region(1, 1, 2, 2))   # expect 11

The plus/minus pattern in sum_regionsum_region is the 2D inclusion-exclusion principle: adding back p[row1][col1]p[row1][col1] corrects for subtracting the top-left overlap region twice.

The mirror image: difference arrays for range updates

Prefix sums answer “what’s the sum over this range?” fast. A difference array answers the opposite question fast: “add valval to every element in this range”, applied many times, each in O(1)O(1) — by marking only the range’s two endpoints, then reconstructing the final array with a single prefix-sum pass at the very end.

difference_array_range_updates.py
def apply_range_updates(n, updates):
    diff = [0] * (n + 1)
 
    for left, right, val in updates:
        diff[left] += val          # start adding val from `left` onward
        diff[right + 1] -= val     # cancel that addition after `right`
 
    result = [0] * n
    running = 0
    for i in range(n):
        running += diff[i]         # this IS a prefix sum over the diff array
        result[i] = running
 
    return result
 
 
# Add +2 to indices [1, 3], and +1 to indices [0, 2].
print(apply_range_updates(5, [(1, 3, 2), (0, 2, 1)]))   # expect [1, 3, 3, 2, 0]
difference_array_range_updates.py
def apply_range_updates(n, updates):
    diff = [0] * (n + 1)
 
    for left, right, val in updates:
        diff[left] += val          # start adding val from `left` onward
        diff[right + 1] -= val     # cancel that addition after `right`
 
    result = [0] * n
    running = 0
    for i in range(n):
        running += diff[i]         # this IS a prefix sum over the diff array
        result[i] = running
 
    return result
 
 
# Add +2 to indices [1, 3], and +1 to indices [0, 2].
print(apply_range_updates(5, [(1, 3, 2), (0, 2, 1)]))   # expect [1, 3, 3, 2, 0]

Each update touches only 2 positions instead of the entire range, no matter how wide it is — mm updates cost O(m)O(m) total, then one final O(n)O(n) pass reconstructs the array. A difference array is quite literally a prefix sum array run in reverse.

Time and space complexity

StructureBuildQuery / UpdateSpace
1D prefix sumO(n)O(n)O(1)O(1) range-sum queryO(n)O(n)
2D prefix sumO(rowscols)O(rows \cdot cols)O(1)O(1) rectangle-sum queryO(rowscols)O(rows \cdot cols)
Difference arrayO(1)O(1) per updateO(n)O(n) final reconstructionO(n)O(n)
Hashmap + prefix sum (subarray sum = k)O(n)O(n) totalO(n)O(n)

When to use it

  • Many range-sum queries on a fixed array or matrix — prefix sums turn each query from O(n)O(n) into O(1)O(1) after one O(n)O(n) (or O(rowscols)O(rows \cdot cols)) build.
  • Many range updates (add a value to every element in [l, r][l, r]) followed by reading the final array once — a difference array does each update in O(1)O(1) instead of O(range length)O(\text{range length}).
  • “Count subarrays/substrings matching some sum/parity condition” — pair a running prefix value with a hashmap of “seen so far” counts.
  • Note the array must stay fixed between queries for a pure prefix sum to help; if elements change frequently between range-sum queries, a Fenwick tree / segment tree (outside this pattern) is the right upgrade.

Practice — real LeetCode problems

Each exercise is the actual LeetCode problem with its real method signature and LeetCode’s own examples as the test. Write the body, press Run, and match the expected output — then paste the same code into leetcode.com.

LC 303 — Range Sum Query, Immutable · Easy

Problem. Design NumArrayNumArray supporting sumRange(left, right)sumRange(left, right), returning the inclusive sum of nums[left..right]nums[left..right]. The array never changes, and there may be many queries.

Constraints. 1 <= len(nums) <= 10^41 <= len(nums) <= 10^4, -10^5 <= nums[i] <= 10^5-10^5 <= nums[i] <= 10^5, up to 10^410^4 calls.

Examples. For [-2,0,3,-5,2,-1][-2,0,3,-5,2,-1]: sumRange(0,2)sumRange(0,2) gives 11, sumRange(2,5)sumRange(2,5) gives -1-1, sumRange(0,5)sumRange(0,5) gives -3-3

Editorial — approach, complexity, follow-ups

Precompute cumulative sums once, then every range query is a single subtraction.

Construction O(n)O(n). Each query O(1)O(1). Space O(n)O(n).

The leading 00 is the whole design decision. With prefix[i]prefix[i] defined as “the sum of the first ii elements”, prefix[0] = 0prefix[0] = 0 is the empty prefix, and sumRange(left, right) = prefix[right + 1] - prefix[left]sumRange(left, right) = prefix[right + 1] - prefix[left] works uniformly — including when left == 0left == 0. Without it you need an if left == 0if left == 0 branch, which is exactly the sort of special case that becomes a bug in the 2D version.

The + 1+ 1 on the right index is what makes the range inclusive; sumRange(3, 3)sumRange(3, 3) returning -5-5 (a single element) is the test that pins it down.

Follow-ups you should expect:

  • “What if the array can be updated (LC 307)?” Prefix sums cost O(n)O(n) per update. Use a Fenwick tree or a segment tree for O(logn)O(\log n) on both operations.
  • “2D version (LC 304)?” A 2D prefix table with inclusion-exclusion: total - top - left + toplefttotal - top - left + topleft.
  • “Count subarrays summing to k (LC 560)?” Prefix sums plus a hash map — see Prefix Sum with HashMap.
  • “Memory too tight for the prefix array?” You could recompute per query at O(n)O(n); the whole point here is trading space for query speed.

LC 238 — Product of Array Except Self · Medium

Problem. Return an array where answer[i]answer[i] is the product of every element except nums[i]nums[i]. You must not use division, and it must run in O(n)O(n).

Constraints. 2 <= len(nums) <= 10^52 <= len(nums) <= 10^5, -30 <= nums[i] <= 30-30 <= nums[i] <= 30, and the answer is guaranteed to fit in a 32-bit integer.

Examples. [1,2,3,4][1,2,3,4] gives [24,12,8,6][24,12,8,6] · [-1,1,0,-3,3][-1,1,0,-3,3] gives [0,0,9,0,0][0,0,9,0,0]

Editorial — approach, complexity, follow-ups

answer[i]answer[i] is the prefix product before ii times the suffix product after it. Two sweeps compute both without ever storing separate arrays: the first writes prefixes into the output, the second multiplies suffixes in as it walks back.

Time O(n)O(n). Space O(1)O(1) extra — the output does not count, per the problem’s own note.

Why the no-division rule matters. The obvious solution divides the total product by each element, but a single 00 makes that undefined, and two zeros make every answer 00 in a way division cannot express. The prefix/suffix method handles zeros with no special case at all: [-1,1,0,-3,3][-1,1,0,-3,3] gives [0,0,9,0,0][0,0,9,0,0], where only the zero’s own position gets a non-zero answer. [0,0][0,0] gives [0,0][0,0].

Getting that behaviour for free is the point of the technique — a division-with-zero-counting solution needs to branch on whether there are zero, one, or many zeros.

The overflow note in the constraints (“the answer fits in 32 bits”) is a hint for fixed-width languages; Python is unaffected.

Follow-ups you should expect: “With division allowed?” — count the zeros and branch; be ready to enumerate the three cases. “Prefix and suffix arrays instead?” — clearer to explain at O(n)O(n) extra space; a fine first answer before optimising. “Sum instead of product?” — ordinary prefix sums. “Product of a range?” — prefix products work only when no zeros exist; otherwise reset at zeros, as in LC 1352.

LC 1109 — Corporate Flight Bookings · Medium

Problem. Given nn flights labelled 1..n1..n and bookings [first, last, seats][first, last, seats] meaning seatsseats were reserved on every flight from firstfirst to lastlast inclusive, return the total seats reserved on each flight.

Constraints. 1 <= n <= 2 * 10^41 <= n <= 2 * 10^4, 1 <= len(bookings) <= 2 * 10^41 <= len(bookings) <= 2 * 10^4, 1 <= first <= last <= n1 <= first <= last <= n, 1 <= seats <= 10^41 <= seats <= 10^4.

Examples. bookings = [[1,2,10],[2,3,20],[2,5,25]], n = 5bookings = [[1,2,10],[2,3,20],[2,5,25]], n = 5 gives [10,55,45,25,25][10,55,45,25,25] · bookings = [[1,2,10],[2,2,15]], n = 2bookings = [[1,2,10],[2,2,15]], n = 2 gives [10,25][10,25]

Editorial — approach, complexity, follow-ups

A difference array inverts the prefix-sum relationship. Prefix sums make range queries O(1)O(1) after O(n)O(n) setup; difference arrays make range updates O(1)O(1), with one O(n)O(n) sweep at the end to read the final values.

Time O(bookings+n)O(\text{bookings} + n). Space O(n)O(n).

The naive alternative loops over each booking’s range, which is O(bookings×n)O(\text{bookings} \times n) — up to 4×1084 \times 10^8 operations at these constraints. Two writes per booking instead of a loop is the whole optimisation.

Two indexing details:

  • delta[first - 1]delta[first - 1] converts the 1-indexed flight number to a 0-indexed slot.
  • delta[last]delta[last], not delta[last + 1]delta[last + 1]. Because lastlast is a 1-indexed inclusive end, its 0-indexed slot is last - 1last - 1, so the cancellation belongs at lastlast. Sizing the array n + 1n + 1 means a booking ending at flight nn can write at index nn without going out of bounds.

[[1,1,5]][[1,1,5]] giving [5][5] is the minimal case: +5+5 at index 0, -5-5 at index 1 (the spare slot), and the running sum yields [5][5].

Follow-ups you should expect: “Range queries as well as updates?” — a Fenwick tree or segment tree gives O(logn)O(\log n) for both. “2D range updates?” — a 2D difference array with four corner writes. “Range Addition (LC 370)?” — the same technique, stated directly. “Why not a sweep line?” — it is essentially the same idea; see Sweep Line and Event Counting.

LeetCode problem set

#ProblemDifficultyThe twist
303Range Sum Query - ImmutableEasyThe exact 1D prefix sum template
560Subarray Sum Equals KMediumThe hashmap + prefix sum combo, as above
525Contiguous ArrayMediumMap 0 -> -10 -> -1, 1 -> +11 -> +1, then find the longest span between two equal running sums
304Range Sum Query 2D - ImmutableMediumThe 2D prefix sum with inclusion-exclusion
238Product of Array Except SelfMediumNot a sum, but the same “precompute running totals from both directions” idea, using running products instead of running sums

Recap

  • A prefix sum array turns repeated range-sum queries into O(1)O(1) lookups after one O(n)O(n) build; the +1+1 offset avoids special-casing left == 0left == 0.
  • The same inclusion-exclusion idea extends to 2D for rectangle sums over a matrix.
  • A difference array is the mirror image: many range updates, each O(1)O(1), reconstructed into the real array with one final prefix-sum pass.
  • Pairing a running prefix sum with a hashmap of “seen so far” counts solves subarray-sum-matching questions in O(n)O(n) instead of O(n2)O(n^2).

You’ve now covered the core interview-pattern toolkit — top-k heaps, k-way merges, binary search on the answer, monotonic stacks, and prefix sums — the five patterns that recur across the widest range of DSA interview questions.

If this helped you, consider buying me a coffee ☕

Buy me a coffee

Was this page helpful?

Let us know how we did