Python Program for Prefix Sums and Range Sum Queries
Intermediate Data structures Time O(1) Space O(n)
Stores the running totals of a list in one O(n) pass, then answers each range sum query with one subtraction, in O(1).
A prefix sum is the total of a list's items up to a point: the first item, the first two, the first three and so on. Work them all out once, and the total of any stretch of the list, a range sum, takes one subtraction, however long the stretch is.
For [4, 7, 2, 9, 5, 3, 8] the prefix sums are [0, 4, 11, 13, 22, 27, 30, 38]: 0, then 4, 4 + 7 = 11,
11 + 2 = 13, and so on. The total of the items at indexes 2 to 4 (2, 9 and 5) is the total of the first five items
minus the total of the first two: 27 - 11 = 16. It's how a program answers many questions about one list that doesn't
change, such as the sales between any two dates.
Program
def prefix_sums(items):
"""sums[i] is the total of the first i items."""
sums = [0]
for item in items:
sums.append(sums[-1] + item)
return sums
def range_sum(sums, start, stop):
"""The total of items[start:stop]."""
return sums[stop] - sums[start]
numbers = [4, 7, 2, 9, 5, 3, 8]
sums = prefix_sums(numbers)
print("Prefix sums:", sums)
print("Total of numbers[2:5]:", range_sum(sums, 2, 5))
print("Total of numbers[0:4]:", range_sum(sums, 0, 4))
Open in the editor Download prefix_sums.py
Output
Prefix sums: [0, 4, 11, 13, 22, 27, 30, 38]
Total of numbers[2:5]: 16
Total of numbers[0:4]: 22
How it works
Step through the program line by line: each step below shows up as its line runs.
-
sumsstarts with 0, the total of no items. That makessums[i]the total of the firstiitems, and a range that starts at index 0 needs no special case. - Each new total is the one before it plus the next item (
sums[-1]is the last total so far), so one pass builds them all. The list it returns has one more entry thanitems. - The total of
items[start:stop]is the total of the firststopitems minus the total of the firststartitems: the items beforestartare in both totals, so they cancel out.startandstopwork as in a slice, so the item atstopisn't included. - The program builds the prefix sums of seven numbers, once, and prints them.
- It answers two range queries with one subtraction each: the items at indexes 2 to 4, and the first four items.
| i | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
|---|---|---|---|---|---|---|---|---|
numbers[i] |
4 | 7 | 2 | 9 | 5 | 3 | 8 | |
sums[i] |
0 | 4 | 11 | 13 | 22 | 27 | 30 | 38 |
| Query | Subtraction | Answer | Check |
|---|---|---|---|
numbers[2:5] |
sums[5] - sums[2] = 27 - 11 |
16 | 2 + 9 + 5 |
numbers[0:4] |
sums[4] - sums[0] = 22 - 0 |
22 | 4 + 7 + 2 + 9 |
Time and space complexity
| Time | O(1) |
|---|---|
| Extra space | O(n) |
Building the prefix sums visits each of the n items once: O(n) time, and O(n) space for the n + 1 totals. After
that, every query is one subtraction, O(1), whatever the size of the range. Adding up the range each time instead,
with sum(numbers[start:stop]), takes up to O(n) per query, so q queries cost O(n ร q) that way against O(n + q)
with prefix sums.
Prefix sums suit a list that doesn't change. Changing one item changes every total after it, and redoing them is O(n).
Other ways to write it
With itertools.accumulate()
accumulate() makes the running totals, and initial=0 puts the 0 in front.
from itertools import accumulate numbers = [4, 7, 2, 9, 5, 3, 8] sums = list(accumulate(numbers, initial=0)) print(sums, sums[5] - sums[2])
Adding up each range
sum() of a slice gives the same answers with no setup. It's fine for a few queries, but each one goes through every
item in its range.
numbers = [4, 7, 2, 9, 5, 3, 8] print(sum(numbers[2:5]), sum(numbers[0:4]))
Try it yourself
- Check each answer against
sum(numbers[start:stop]), and try a range of one item,range_sum(sums, 3, 4), and an empty one,range_sum(sums, 3, 3). - Use
range_sum()to find the average ofnumbers[1:6]. - Find which 3 items in a row have the largest total, with one
range_sum()call for each place they could start. - Count the even numbers in any range: build the prefix sums of a list that has 1 for each even number and 0 for each odd one, with a list comprehension.