Range Sum Query - Immutable (LeetCode 303)
An integer array nums is given once, and it never changes. After that, many questions arrive. Each question names two indices, left and right, with left <= right, and asks for the total of the elements from index left to index right, both ends included. That total is the sum below.
nums[left] + nums[left + 1] + ... + nums[right]
Your class is NumArray. Its constructor, NumArray(nums), receives the array. Its method sumRange(left, right) answers one question and returns that total as an integer.
Worked Examples
["NumArray", "sumRange", "sumRange", "sumRange"]
[[[-2, 0, 3, -5, 2, -1]], [0, 2], [2, 5], [0, 5]][null, 1, -1, -3]NumArray([-2, 0, 3, -5, 2, -1]) receives the array and returns null. sumRange(0, 2) is -2 + 0 + 3 = 1. sumRange(2, 5) is 3 + (-5) + 2 + (-1) = -1. sumRange(0, 5) is the whole array: -2 + 0 + 3 + (-5) + 2 + (-1) = -3.⚖️Formal Constraints & Bounds
1 <= nums.length <= 104-105 <= nums[i] <= 1050 <= left <= right < nums.lengthsumRangeis called at most104times.
Core Insight
self.prefix[i] holds the sum of the first i elements, with self.prefix[0] = 0. The first right + 1 elements are the first left elements followed by nums[left..right]. So every range sum is one subtraction of two stored sums.
Real-World Scenario & Production Applications
A dashboard shows totals over many date ranges of the same daily figures, such as sales per day or requests per hour. Past days do not change. So the running totals from the first day can be computed once, and the total of any date range is then one subtraction.
Subproblems & Recurrence Decomposition2 Phases
self.prefix gets one slot more than nums. Slot 0 is the sum of no elements, 0, and each next slot adds one element, so self.prefix[i] is the sum of the first i elements. This is one pass, O(N).
self.prefix = [0] * (len(nums) + 1)
for i, num in enumerate(nums):
self.prefix[i + 1] = self.prefix[i] + numStep-by-Step Execution Trace Table
The debugger's first preset is Example 1, nums = [-2, 0, 3, -5, 2, -1]. __init__ fills self.prefix one slot at a time:
i | num | self.prefix[i + 1] = self.prefix[i] + num | self.prefix after the step |
|---|---|---|---|
| (start) | — | — | [0, 0, 0, 0, 0, 0, 0] |
| 0 | -2 | 0 + (-2) = -2 | [0, -2, 0, 0, 0, 0, 0] |
| 1 | 0 | -2 + 0 = -2 | [0, -2, -2, 0, 0, 0, 0] |
| 2 | 3 | -2 + 3 = 1 | [0, -2, -2, 1, 0, 0, 0] |
| 3 | -5 | 1 + (-5) = -4 | [0, -2, -2, 1, -4, 0, 0] |
| 4 | 2 | -4 + 2 = -2 | [0, -2, -2, 1, -4, -2, 0] |
| 5 | -1 | -2 + (-1) = -3 | [0, -2, -2, 1, -4, -2, -3] |
Then each call reads two slots: sumRange(0, 2) = self.prefix[3] - self.prefix[0] = 1 - 0 = 1, sumRange(2, 5) = self.prefix[6] - self.prefix[2] = -3 - (-2) = -1, and sumRange(0, 5) = self.prefix[6] - self.prefix[0] = -3 - 0 = -3.
The Math, Step by Step
1. The number the code stores
Call the prefix sum at : the total of the first elements of nums. So , because it adds no elements. is nums[0], and is nums[0] + nums[1]. An array of elements has prefix sums, from to .
The code stores in self.prefix[i]. Every question is then answered with one formula:
2. Computing every prefix sum in one pass
Each prefix sum is the one before it plus one more element:
So the code needs one pass from left to right. It starts with and adds one element per step. There are no cases to tell apart. The same line runs for every index.
3. Why one subtraction gives the range total
Split the first elements into two parts. The first part is the first elements. The second part is nums[left] up to nums[right]. Written as totals:
Move to the other side, and you get the formula from part 1. The proof only splits one sum into two parts. It never needs the values to be positive, and it never needs to grow. So the formula also holds when nums has negative values, as in the example below.
4. The + 1 and the zero at the start
The two indices of the formula are not symmetric. includes nums[right], because it counts elements. excludes nums[left], because it counts only the elements before it. If you write self.prefix[right] - self.prefix[left], the result leaves out nums[right].
The extra entry handles ranges that start at index 0. For the formula reads . That is the total of the first elements minus nothing. Without , a range starting at index 0 would need its own if.
5. Example 1 by hand
nums = [-2, 0, 3, -5, 2, -1]. The constructor builds the prefix sums one step at a time:
nums[i] | ||
|---|---|---|
| start | ||
| 0 | ||
| 1 | ||
| 2 | ||
| 3 | ||
| 4 | ||
| 5 |
The picture shows the seven prefix sums to for Example 1, with the index of each one under its cell. The two marked entries answer the question sumRange(2, 5), where and . is the total of all six elements. is the total of the first two elements, nums[0] and nums[1]. Their difference is the total of nums[2] to nums[5].
The three questions of Example 1 each take one subtraction:
| Question | Answer | ||
|---|---|---|---|
sumRange(0, 2) | |||
sumRange(2, 5) | |||
sumRange(0, 5) |
The answers , and are the expected output. A direct check of the second one: .
6. From the formulas to the code
| Formula | In NumArray |
|---|---|
| , and entries | self.prefix = [0] * (len(nums) + 1) |
self.prefix[i + 1] = self.prefix[i] + num | |
return self.prefix[right + 1] - self.prefix[left] |
__init__ runs the pass of part 2 once, in time. After that, sumRange reads two stored values and subtracts them, in time per question.
| 1 | self.prefix = [0] * (len(nums) + 1) |
| 2 | for i, num in enumerate(nums): |
| 3 | self.prefix[i + 1] = <the slot before it, plus num> |
| 4 | return self.prefix[right + 1] - self.prefix[left] |
Conceptual Narrative
🧭 Conceptual Foundation & Pattern Intuition
Adding up nums[left..right] on every call reads up to n elements each time. With 10^4 elements and 10^4 calls, that is up to 10^8 additions. But the array never changes, so the calls repeat the same additions. Prefix Sums does the adding once. self.prefix[i] stores the sum of the first i elements. Any range is then the difference of two stored sums, self.prefix[right + 1] - self.prefix[left].
🛣️ The Analogy: Distance Markers on a Road
A road has a marker every kilometer, and each marker shows the distance from the start of the road. To find the distance from marker 42 to marker 78, you do not measure the road again. You subtract: 78 - 42 = 36 kilometers. self.prefix is the list of markers, and self.prefix[0] = 0 is the start of the road.
🔑 Why It Works
After __init__, self.prefix[i] is the sum of nums[0..i-1] for every i from 0 to n. The first right + 1 elements are the first left elements followed by nums[left..right]. So the two sums differ by exactly the sum of the range. The values may be negative. The formula still holds, because it never needs the totals to increase.
Writing
self.prefix[right]instead ofself.prefix[right + 1]Dropping the leading 0 and reading
self.prefix[left - 1]Adding up the range inside
sumRange
4-Phase Thought Process Model
Pattern Recognition Signals
The 10-second spot
The statement says that the array is given once and never changes. Then sumRange is called up to 10^4 times, on up to 10^4 elements. A fixed array plus many questions about the sum of a given range is the signal for Prefix Sums. You pay once to build the sums, and each question becomes one subtraction.
Formulating the Predicate & Invariants
Turning intuition into a boolean rule
Invariant: after __init__, self.prefix[i] is the sum of the first i elements, nums[0..i-1], for every i from 0 to n, and self.prefix[0] = 0. The first right + 1 elements are the first left elements followed by the range, so sumRange returns self.prefix[right + 1] - self.prefix[left].
Silent Failure Traps & Edge Cases
Where confident candidates still lose points
Read
self.prefix[right + 1], notself.prefix[right].self.prefix[i]stops just beforenums[i], soself.prefix[right]leavesnums[right]out. On[-2, 0, 3, -5, 2, -1],sumRange(0, 2)would return -2 instead of 1.Fill
self.prefix[i + 1]fromself.prefix[i], the slot just before it, not fromself.prefix[i - 1]. Ati = 0,self.prefix[i - 1]isself.prefix[-1], the last slot, which is still 0. Fromi = 1on, each slot addsnumto the slot two places back, so it leaves one element out. On[-2, 0, 3, -5, 2, -1],sumRange(2, 5)then returns -6 instead of -1.
The 60-Second Interview Pitch
Say this out loud before you type a single line
I'd use Prefix Sums. The array never changes and there are many range questions, so I do the adding once. In the constructor I build prefix with n plus 1 slots. prefix at 0 is 0, and each next slot is the one before it plus one element. So prefix at i is the sum of the first i elements. For sumRange of left and right, the first right plus 1 elements are the first left elements followed by my range. So I return prefix at right plus 1, minus prefix at left. The leading 0 lets a range that starts at index 0 use the same formula. The trap is the index shift: prefix at right stops just before nums at right, so I must read prefix at right plus 1. Building takes
O(N)time andO(N)space, and each query is one subtraction,O(1).
Complexity & Mathematical Proof
O(N) to build, O(1) per sumRange
Look at the code. self.prefix = [0] * (len(nums) + 1) writes N + 1 zeros. The for loop runs N times and does one addition each time. So __init__ costs O(N). sumRange reads two slots of self.prefix and subtracts once, whatever the length of the range. So each call costs O(1), and Q calls cost O(Q).
O(N)
self.prefix holds N + 1 integers, so O(N). sumRange keeps no extra memory. There is no recursion, so the call stack stays the same size.
T(N, Q) = O(N) to build + Q · O(1) for the calls = O(N + Q)
Derivation Progression
O(N + 1)
[0] * (len(nums) + 1) writes one zero per slot.
N · O(1)
self.prefix[i + 1] = self.prefix[i] + num: one addition per element.
O(1)
self.prefix[right + 1] - self.prefix[left]: two reads and one subtraction, for any range length.
O(N + Q)
One pass over the array, then constant work per call.
Variable Definitions
The number of elements, len(nums)
The number of sumRange calls
Memory Architecture & Bounds
O(1): no recursion
O(N): self.prefix, N + 1 integers
O(1): one integer per call
Boundary Best / Worst Cases
per call: a one-element range such as sumRange(3, 3) reads two slots
per call
per call: the whole array, sumRange(0, N - 1), also reads only two slots
Senior SWE Deconstruction & Hardware Caveats
Triggers: "many questions arrive" and "sumRange is called at most 104 times", on an array that is given once and never changes. At scale the same shape is read-heavy data: written once, then queried by range many times, such as daily totals in a report.
values of absolute size up to , so every prefix total lies between and (). That fits in a 32-bit signed integer, whose largest value is . On larger inputs the totals grow with , so production code stores them as 64-bit integers.
The table is correct only while the data does not change. One changed element changes every prefix total after it, so an update costs O(N) to rebuild. When updates and range questions alternate, a Fenwick tree answers both in O(log N). With floating-point data, each prefix total is rounded, and subtracting two large rounded totals can lose the digits of a small range sum.
Core Algorithmic State Invariants
- Negative Values Need No Special Case
The totals in self.prefix can go up and down. For [-2, 0, 3, -5, 2, -1] they are [0, -2, -2, 1, -4, -2, -3]. The formula only uses the fact that the first right + 1 elements are the first left elements plus the range. It never needs the totals to increase, so negative values need no special case.
- One Table Answers Every Range
An array of n elements has n * (n + 1) / 2 ranges, and self.prefix answers each one from two of its n + 1 slots. For the 6 elements of Example 1, that is 21 ranges from 7 slots.
Range Sum Query - Immutable (LeetCode 303)
An integer array nums is given once, and it never changes. After that, many questions arrive. Each question names two indices, left and right, with left <= right, and asks for the total of the elements from index left to index right, both ends included. That total is the sum below.
nums[left] + nums[left + 1] + ... + nums[right]
Your class is NumArray. Its constructor, NumArray(nums), receives the array. Its method sumRange(left, right) answers one question and returns that total as an integer.
Worked Examples
["NumArray", "sumRange", "sumRange", "sumRange"]
[[[-2, 0, 3, -5, 2, -1]], [0, 2], [2, 5], [0, 5]][null, 1, -1, -3]NumArray([-2, 0, 3, -5, 2, -1]) receives the array and returns null. sumRange(0, 2) is -2 + 0 + 3 = 1. sumRange(2, 5) is 3 + (-5) + 2 + (-1) = -1. sumRange(0, 5) is the whole array: -2 + 0 + 3 + (-5) + 2 + (-1) = -3.⚖️Formal Constraints & Bounds
1 <= nums.length <= 104-105 <= nums[i] <= 1050 <= left <= right < nums.lengthsumRangeis called at most104times.
Core Insight
self.prefix[i] holds the sum of the first i elements, with self.prefix[0] = 0. The first right + 1 elements are the first left elements followed by nums[left..right]. So every range sum is one subtraction of two stored sums.
Real-World Scenario & Production Applications
A dashboard shows totals over many date ranges of the same daily figures, such as sales per day or requests per hour. Past days do not change. So the running totals from the first day can be computed once, and the total of any date range is then one subtraction.
Subproblems & Recurrence Decomposition2 Phases
self.prefix gets one slot more than nums. Slot 0 is the sum of no elements, 0, and each next slot adds one element, so self.prefix[i] is the sum of the first i elements. This is one pass, O(N).
self.prefix = [0] * (len(nums) + 1)
for i, num in enumerate(nums):
self.prefix[i + 1] = self.prefix[i] + numStep-by-Step Execution Trace Table
The debugger's first preset is Example 1, nums = [-2, 0, 3, -5, 2, -1]. __init__ fills self.prefix one slot at a time:
i | num | self.prefix[i + 1] = self.prefix[i] + num | self.prefix after the step |
|---|---|---|---|
| (start) | — | — | [0, 0, 0, 0, 0, 0, 0] |
| 0 | -2 | 0 + (-2) = -2 | [0, -2, 0, 0, 0, 0, 0] |
| 1 | 0 | -2 + 0 = -2 | [0, -2, -2, 0, 0, 0, 0] |
| 2 | 3 | -2 + 3 = 1 | [0, -2, -2, 1, 0, 0, 0] |
| 3 | -5 | 1 + (-5) = -4 | [0, -2, -2, 1, -4, 0, 0] |
| 4 | 2 | -4 + 2 = -2 | [0, -2, -2, 1, -4, -2, 0] |
| 5 | -1 | -2 + (-1) = -3 | [0, -2, -2, 1, -4, -2, -3] |
Then each call reads two slots: sumRange(0, 2) = self.prefix[3] - self.prefix[0] = 1 - 0 = 1, sumRange(2, 5) = self.prefix[6] - self.prefix[2] = -3 - (-2) = -1, and sumRange(0, 5) = self.prefix[6] - self.prefix[0] = -3 - 0 = -3.
The Math, Step by Step
1. The number the code stores
Call the prefix sum at : the total of the first elements of nums. So , because it adds no elements. is nums[0], and is nums[0] + nums[1]. An array of elements has prefix sums, from to .
The code stores in self.prefix[i]. Every question is then answered with one formula:
2. Computing every prefix sum in one pass
Each prefix sum is the one before it plus one more element:
So the code needs one pass from left to right. It starts with and adds one element per step. There are no cases to tell apart. The same line runs for every index.
3. Why one subtraction gives the range total
Split the first elements into two parts. The first part is the first elements. The second part is nums[left] up to nums[right]. Written as totals:
Move to the other side, and you get the formula from part 1. The proof only splits one sum into two parts. It never needs the values to be positive, and it never needs to grow. So the formula also holds when nums has negative values, as in the example below.
4. The + 1 and the zero at the start
The two indices of the formula are not symmetric. includes nums[right], because it counts elements. excludes nums[left], because it counts only the elements before it. If you write self.prefix[right] - self.prefix[left], the result leaves out nums[right].
The extra entry handles ranges that start at index 0. For the formula reads . That is the total of the first elements minus nothing. Without , a range starting at index 0 would need its own if.
5. Example 1 by hand
nums = [-2, 0, 3, -5, 2, -1]. The constructor builds the prefix sums one step at a time:
nums[i] | ||
|---|---|---|
| start | ||
| 0 | ||
| 1 | ||
| 2 | ||
| 3 | ||
| 4 | ||
| 5 |
The picture shows the seven prefix sums to for Example 1, with the index of each one under its cell. The two marked entries answer the question sumRange(2, 5), where and . is the total of all six elements. is the total of the first two elements, nums[0] and nums[1]. Their difference is the total of nums[2] to nums[5].
The three questions of Example 1 each take one subtraction:
| Question | Answer | ||
|---|---|---|---|
sumRange(0, 2) | |||
sumRange(2, 5) | |||
sumRange(0, 5) |
The answers , and are the expected output. A direct check of the second one: .
6. From the formulas to the code
| Formula | In NumArray |
|---|---|
| , and entries | self.prefix = [0] * (len(nums) + 1) |
self.prefix[i + 1] = self.prefix[i] + num | |
return self.prefix[right + 1] - self.prefix[left] |
__init__ runs the pass of part 2 once, in time. After that, sumRange reads two stored values and subtracts them, in time per question.
| 1 | self.prefix = [0] * (len(nums) + 1) |
| 2 | for i, num in enumerate(nums): |
| 3 | self.prefix[i + 1] = <the slot before it, plus num> |
| 4 | return self.prefix[right + 1] - self.prefix[left] |
Conceptual Narrative
🧭 Conceptual Foundation & Pattern Intuition
Adding up nums[left..right] on every call reads up to n elements each time. With 10^4 elements and 10^4 calls, that is up to 10^8 additions. But the array never changes, so the calls repeat the same additions. Prefix Sums does the adding once. self.prefix[i] stores the sum of the first i elements. Any range is then the difference of two stored sums, self.prefix[right + 1] - self.prefix[left].
🛣️ The Analogy: Distance Markers on a Road
A road has a marker every kilometer, and each marker shows the distance from the start of the road. To find the distance from marker 42 to marker 78, you do not measure the road again. You subtract: 78 - 42 = 36 kilometers. self.prefix is the list of markers, and self.prefix[0] = 0 is the start of the road.
🔑 Why It Works
After __init__, self.prefix[i] is the sum of nums[0..i-1] for every i from 0 to n. The first right + 1 elements are the first left elements followed by nums[left..right]. So the two sums differ by exactly the sum of the range. The values may be negative. The formula still holds, because it never needs the totals to increase.
Writing
self.prefix[right]instead ofself.prefix[right + 1]Dropping the leading 0 and reading
self.prefix[left - 1]Adding up the range inside
sumRange
4-Phase Thought Process Model
Pattern Recognition Signals
The 10-second spot
The statement says that the array is given once and never changes. Then sumRange is called up to 10^4 times, on up to 10^4 elements. A fixed array plus many questions about the sum of a given range is the signal for Prefix Sums. You pay once to build the sums, and each question becomes one subtraction.
Formulating the Predicate & Invariants
Turning intuition into a boolean rule
Invariant: after __init__, self.prefix[i] is the sum of the first i elements, nums[0..i-1], for every i from 0 to n, and self.prefix[0] = 0. The first right + 1 elements are the first left elements followed by the range, so sumRange returns self.prefix[right + 1] - self.prefix[left].
Silent Failure Traps & Edge Cases
Where confident candidates still lose points
Read
self.prefix[right + 1], notself.prefix[right].self.prefix[i]stops just beforenums[i], soself.prefix[right]leavesnums[right]out. On[-2, 0, 3, -5, 2, -1],sumRange(0, 2)would return -2 instead of 1.Fill
self.prefix[i + 1]fromself.prefix[i], the slot just before it, not fromself.prefix[i - 1]. Ati = 0,self.prefix[i - 1]isself.prefix[-1], the last slot, which is still 0. Fromi = 1on, each slot addsnumto the slot two places back, so it leaves one element out. On[-2, 0, 3, -5, 2, -1],sumRange(2, 5)then returns -6 instead of -1.
The 60-Second Interview Pitch
Say this out loud before you type a single line
I'd use Prefix Sums. The array never changes and there are many range questions, so I do the adding once. In the constructor I build prefix with n plus 1 slots. prefix at 0 is 0, and each next slot is the one before it plus one element. So prefix at i is the sum of the first i elements. For sumRange of left and right, the first right plus 1 elements are the first left elements followed by my range. So I return prefix at right plus 1, minus prefix at left. The leading 0 lets a range that starts at index 0 use the same formula. The trap is the index shift: prefix at right stops just before nums at right, so I must read prefix at right plus 1. Building takes
O(N)time andO(N)space, and each query is one subtraction,O(1).
Complexity & Mathematical Proof
O(N) to build, O(1) per sumRange
Look at the code. self.prefix = [0] * (len(nums) + 1) writes N + 1 zeros. The for loop runs N times and does one addition each time. So __init__ costs O(N). sumRange reads two slots of self.prefix and subtracts once, whatever the length of the range. So each call costs O(1), and Q calls cost O(Q).
O(N)
self.prefix holds N + 1 integers, so O(N). sumRange keeps no extra memory. There is no recursion, so the call stack stays the same size.
T(N, Q) = O(N) to build + Q · O(1) for the calls = O(N + Q)
Derivation Progression
O(N + 1)
[0] * (len(nums) + 1) writes one zero per slot.
N · O(1)
self.prefix[i + 1] = self.prefix[i] + num: one addition per element.
O(1)
self.prefix[right + 1] - self.prefix[left]: two reads and one subtraction, for any range length.
O(N + Q)
One pass over the array, then constant work per call.
Variable Definitions
The number of elements, len(nums)
The number of sumRange calls
Memory Architecture & Bounds
O(1): no recursion
O(N): self.prefix, N + 1 integers
O(1): one integer per call
Boundary Best / Worst Cases
per call: a one-element range such as sumRange(3, 3) reads two slots
per call
per call: the whole array, sumRange(0, N - 1), also reads only two slots
Senior SWE Deconstruction & Hardware Caveats
Triggers: "many questions arrive" and "sumRange is called at most 104 times", on an array that is given once and never changes. At scale the same shape is read-heavy data: written once, then queried by range many times, such as daily totals in a report.
values of absolute size up to , so every prefix total lies between and (). That fits in a 32-bit signed integer, whose largest value is . On larger inputs the totals grow with , so production code stores them as 64-bit integers.
The table is correct only while the data does not change. One changed element changes every prefix total after it, so an update costs O(N) to rebuild. When updates and range questions alternate, a Fenwick tree answers both in O(log N). With floating-point data, each prefix total is rounded, and subtracting two large rounded totals can lose the digits of a small range sum.
Core Algorithmic State Invariants
- Negative Values Need No Special Case
The totals in self.prefix can go up and down. For [-2, 0, 3, -5, 2, -1] they are [0, -2, -2, 1, -4, -2, -3]. The formula only uses the fact that the first right + 1 elements are the first left elements plus the range. It never needs the totals to increase, so negative values need no special case.
- One Table Answers Every Range
An array of n elements has n * (n + 1) / 2 ranges, and self.prefix answers each one from two of its n + 1 slots. For the 6 elements of Example 1, that is 21 ranges from 7 slots.