Range Sum Query - Mutable (LeetCode 307)
You get an integer array nums, followed by a mix of two kinds of requests in any order: change one element, or report the sum of a contiguous stretch of elements.
Build the NumArray class:
NumArray(nums)stores the array.update(index, val)setsnums[index]toval.sumRange(left, right)returnsnums[left] + nums[left + 1] + ... + nums[right], both ends included (left <= right).
Worked Examples
["NumArray", "sumRange", "update", "sumRange"]
[[[1, 3, 5]], [0, 2], [1, 2], [0, 2]][null, 9, null, 8][1, 3, 5], so sumRange(0, 2) is 1 + 3 + 5 = 9. update(1, 2) makes it [1, 2, 5], and the next sumRange(0, 2) is 1 + 2 + 5 = 8.⚖️Formal Constraints & Bounds
1 <= nums.length <= 3 * 104-100 <= nums[i] <= 1000 <= index < nums.length-100 <= val <= 1000 <= left <= right < nums.lengthAt most
3 * 104calls will be made toupdateandsumRange.
Core Insight
Store sums of blocks whose length is the index's lowest set bit: a prefix is the sum of O(log N) disjoint blocks, and a changed position belongs to O(log N) blocks, so both operations walk one block per bit.
Real-World Scenario & Production Applications
Live dashboards and leaderboards that keep running totals while individual values keep changing: bucketed counters (requests per second, sales per day) where a single bucket is corrected and a range total is read on every page load. A plain prefix array would need an O(N) rebuild after every change.
Step-by-Step Execution Trace Table
nums = [1, 3, 5] (n = 3). Each value is added at i = index + 1; i & (-i) decides the jump.
| Call | Walk of i | tree[1..3] after | Returns |
|---|---|---|---|
build: update(1, 1) | 1 → 2 → 4 (stop, > 3) | [1, 1, 0] | |
build: update(2, 3) | 2 → 4 (stop) | [1, 4, 0] | |
build: update(3, 5) | 3 → 4 (stop) | [1, 4, 5] | |
sumRange(0, 2) = query(3) - query(0) | query(3): tree[3] + tree[2] = 5 + 4 (3 → 2 → 0) | [1, 4, 5] | 9 |
update(1, 2): add 2 - 3 = -1 at i = 2 | 2 → 4 (stop) | [1, 3, 5] | null |
sumRange(0, 2) | query(3): 5 + 3 = 8 | [1, 3, 5] | 8 |
The Math, Step by Step
1. The range each tree entry covers
The Fenwick tree numbers the array positions from 1, so position index of nums is . Write for the value of the lowest 1 bit of . For example, is 110 in binary, so .
Entry tree[i] holds the sum of values: the positions from to . Written as a half-open range, it covers .
| in binary | = i & -i | tree[i] covers | |
|---|---|---|---|
| 1 | 0001 | 1 | |
| 2 | 0010 | 2 | |
| 3 | 0011 | 1 | |
| 4 | 0100 | 4 | |
| 5 | 0101 | 1 | |
| 6 | 0110 | 2 | |
| 7 | 0111 | 1 | |
| 8 | 1000 | 8 |
2. Why i & -i is the lowest 1 bit
In two's complement, is computed as "flip every bit of , then add 1". Say ends with a 1 followed by zeros. Flipping gives a 0 followed by ones. Adding 1 turns those ones back into zeros and the 0 into a 1. So has the same lowest 1 bit as , with the same zeros after it. Every bit to the left of it is flipped.
& keeps a bit only where both numbers have a 1. Only the lowest 1 bit is 1 in both. So i & -i equals . For : in 8 bits, is 00000110 and is 11111010, and their & is 00000010, which is 2.
3. Why a query and an update each touch about entries
A prefix sum walks down. query(i) adds tree[i], which covers . Then it sets and repeats. The next range ends exactly where the last one started. So the ranges fit together without gaps or overlaps, and they cover . Each step clears the lowest 1 bit of , so the loop runs once per 1 bit, at most times. For the ranges are , and .
An update walks up. A change at position must reach every entry whose range contains . update(i, delta) adds delta to tree[i], then sets and repeats. Adding the lowest 1 bit moves to the next larger entry whose range still reaches down to position . Each step makes the lowest 1 bit at least twice as large, so the loop runs at most times. With , a change at position 3 visits entries 3, 4 and 8. The table of part 1 shows that these are exactly the entries that cover position 3.
A range sum is the difference of two prefix sums, as for a fixed array:
with 1-based positions. sumRange adds 1 to both indices first.
4. The 1-based positions and the size of the change
Position 0 does not work in a Fenwick tree. , so update(0, ...) would add 0 to forever. That is why the code stores position index at , and why tree has entries with tree[0] unused. For query(0), the loop while i > 0 runs zero times and returns 0, which is the sum of no elements.
The tree stores sums, so it must receive the change of a value, not the new value. NumArray.update sends val - self.nums[index], and then stores val in self.nums for the next change. Sending val itself would add the new value on top of the old one.
5. Example 1 by hand
nums = [1, 3, 5], so . The constructor calls update once per value:
| Call | Entries visited | tree[1..3] after |
|---|---|---|
update(1, 1) | 1, then | |
update(2, 3) | 2 (next would be 4, past ) | |
update(3, 5) | 3 (next would be 4, past ) |
So tree[2] = 4 holds positions 1 and 2, and tree[3] = 5 holds position 3 alone.
sumRange(0, 2) computes query(3) - query(0). query(3) adds tree[3] = 5, moves to , adds tree[2] = 4, and moves to . It returns . query(0) returns 0. The answer is , the expected output.
update(1, 2) changes nums[1] from 3 to 2, so it sends the change to position 2. Entry 2 becomes . The next entry would be , past , so the update stops.
The second sumRange(0, 2) adds tree[3] = 5 and tree[2] = 3, and returns , the expected output.
6. From the math to the code
| Math | In the code |
|---|---|
positions , tree[0] unused | self.tree = [0] * (n + 1) and index + 1 |
i & (-i) | |
| walk up to every entry that covers | while i <= self.n: with i += i & (-i) |
| walk down over ranges that tile | while i > 0: with i -= i & (-i) |
return self.query(right) - self.query(left - 1) | |
| send the change, not the new value | self.fenwick.update(index + 1, val - self.nums[index]) |
Both walks take steps. The constructor calls update times, so it takes time. The tree and the copy of nums take space.
| 1 | `tree[i]` holds the sum of the `i & (-i)` positions that end at `i`, so any prefix is a few whole blocks and any position sits in a few blocks. |
| 2 | `query(i)` adds `tree[i]` and steps `i -= i & (-i)` down to 0; `update(i, delta)` adds `delta` to `tree[i]` and steps `i += i & (-i)` up past `n`. |
| 3 | `NumArray`: keep `self.nums`, build by calling `update(index + 1, val)` for each value, `update` adds `val - self.nums[index]`, `sumRange` returns `range_query(left + 1, right + 1)`. |
| 4 | The trap: the tree is 1-indexed. `i & (-i)` is 0 when `i` is 0, so `update(0, ...)` never moves and loops forever; shift every index up by one. |
Conceptual Narrative
🧭 Conceptual Foundation & Pattern Intuition
A prefix-sum array answers range sums in but needs an rebuild after every change; a plain array updates in but sums in . A Fenwick tree (Binary Indexed Tree) meets in the middle: both operations take . Slot tree[i] stores the sum of a block of positions that ends at i and is i & (-i) long (the value of i's lowest set bit).
🏟️ The Analogy: Receipts Filed by Month, Quarter and Year
An accountant keeps receipts in folders of different sizes: some cover one month, some two, some four, some eight, each ending at a certain month. To total January to November, you add a handful of folders that together cover exactly those months. When a March receipt is corrected, you fix every folder that contains March: a handful again.
🔑 Why It Works
Removing the lowest set bit lands just before the block tree[i] covers, so the blocks query adds never overlap and cover exactly 1..i. Adding it reaches the next bigger block that contains i. Both loops touch one block per bit: .
Using index 0 in the tree:
i & (-i)is0wheniis0, soupdate(0, delta)adds forever without moving. Shift every LeetCode index by one:update(index + 1, ...).Adding
valinstead of the change:update(index, val)sets a value, but the tree can only add. Addval - self.nums[index]and then storeval, or every repeated update double-counts.Off-by-one in
sumRange: with 1-indexed slots the sum ofnums[left..right]isquery(right + 1) - query(left), which isrange_query(left + 1, right + 1).Rebuilding a prefix array on every update: correct but
O(N)per update; with3 * 104calls on3 * 104elements that is about 10^9 steps.
4-Phase Thought Process Model
Pattern Recognition Signals
The 10-second spot
"Change one element" mixed with "sum of a contiguous stretch", many times and in any order: a prefix array alone rebuilds in O(N) per change, and a plain array sums in O(N). Point updates plus prefix-based range sums are the exact job of a Fenwick Tree.
Formulating the Predicate & Invariants
Turning intuition into a boolean rule
self.tree[i] always equals the sum of the values at positions i - (i & -i) + 1 through i; update keeps it true by walking i += i & (-i), and query(i) adds disjoint blocks walking i -= i & (-i).
Silent Failure Traps & Edge Cases
Where confident candidates still lose points
This problem's traps are in Common Pitfalls above.
The 60-Second Interview Pitch
Say this out loud before you type a single line
I'd use a Fenwick Tree, because I need point updates and range sums mixed together. Slot i of the tree stores the sum of a block ending at i whose length is i's lowest set bit. A prefix sum walks down by removing that bit each time, adding whole blocks that never overlap; an update walks up by adding the bit, fixing every block that contains the position. Each walk touches one block per bit, so both are
O(log N). In NumArray I keep the current values so an update can add val minus the old value, since the tree only adds, and a range sum is the prefix at right plus one minus the prefix at left. The trap is indexing: the tree is 1-indexed, because at zero the lowest set bit is zero and the loop would never move, so I shift every index up by one. Building costsO(N log N)and space isO(N).
Complexity & Mathematical Proof
O(log N) update & query
Look at the loops: update runs i += i & (-i) until i > n; each step moves the lowest set bit of i at least one place higher, so it runs at most about log2(N) + 1 times. query runs i -= i & (-i) until i == 0; each step removes one set bit, so it runs at most log2(N) + 1 times. sumRange is two queries and NumArray.update is one tree update: O(log N) each. Building NumArray makes N tree updates: O(N log N) once.
O(N)
self.tree has N + 1 slots and self.nums keeps N current values: O(N). The loops are iterative, so there is no recursion stack.
T(update) = T(query) = O(log N); T(sumRange) = 2 · O(log N); build = N · O(log N)
Derivation Progression
N × O(log N)
__init__ calls self.fenwick.update(index + 1, val) once per value.
O(log N)
i += i & (-i) climbs at most one step per bit of N.
O(log N)
i -= i & (-i) clears one set bit per step.
2 × O(log N)
range_query is query(right) - query(left - 1).
Variable Definitions
Length of nums
The lowest set bit of i: the length of the block tree[i] covers
Memory Architecture & Bounds
O(1): both walks are loops
O(N): tree (N + 1 slots) and nums
O(1) per call
Boundary Best / Worst Cases
steps when i is a large power of two for update, or has one set bit for query
per operation; to build
per operation
Recurrence Tree Topology
Senior SWE Deconstruction & Hardware Caveats
Triggers: "change one element", "sum of a contiguous stretch", "in any order". Point updates mixed with range sums: Fenwick Tree (two prefix sums per range, O(log N) each).
values in and at most calls. Sums stay within . Naive: up to steps; Fenwick: about .
In a concurrent service, an update is several writes (one per block), so readers must not see a half-applied update: guard the walk with a lock or apply updates from a single writer.
Range Sum Query - Mutable (LeetCode 307)
You get an integer array nums, followed by a mix of two kinds of requests in any order: change one element, or report the sum of a contiguous stretch of elements.
Build the NumArray class:
NumArray(nums)stores the array.update(index, val)setsnums[index]toval.sumRange(left, right)returnsnums[left] + nums[left + 1] + ... + nums[right], both ends included (left <= right).
Worked Examples
["NumArray", "sumRange", "update", "sumRange"]
[[[1, 3, 5]], [0, 2], [1, 2], [0, 2]][null, 9, null, 8][1, 3, 5], so sumRange(0, 2) is 1 + 3 + 5 = 9. update(1, 2) makes it [1, 2, 5], and the next sumRange(0, 2) is 1 + 2 + 5 = 8.⚖️Formal Constraints & Bounds
1 <= nums.length <= 3 * 104-100 <= nums[i] <= 1000 <= index < nums.length-100 <= val <= 1000 <= left <= right < nums.lengthAt most
3 * 104calls will be made toupdateandsumRange.
Core Insight
Store sums of blocks whose length is the index's lowest set bit: a prefix is the sum of O(log N) disjoint blocks, and a changed position belongs to O(log N) blocks, so both operations walk one block per bit.
Real-World Scenario & Production Applications
Live dashboards and leaderboards that keep running totals while individual values keep changing: bucketed counters (requests per second, sales per day) where a single bucket is corrected and a range total is read on every page load. A plain prefix array would need an O(N) rebuild after every change.
Step-by-Step Execution Trace Table
nums = [1, 3, 5] (n = 3). Each value is added at i = index + 1; i & (-i) decides the jump.
| Call | Walk of i | tree[1..3] after | Returns |
|---|---|---|---|
build: update(1, 1) | 1 → 2 → 4 (stop, > 3) | [1, 1, 0] | |
build: update(2, 3) | 2 → 4 (stop) | [1, 4, 0] | |
build: update(3, 5) | 3 → 4 (stop) | [1, 4, 5] | |
sumRange(0, 2) = query(3) - query(0) | query(3): tree[3] + tree[2] = 5 + 4 (3 → 2 → 0) | [1, 4, 5] | 9 |
update(1, 2): add 2 - 3 = -1 at i = 2 | 2 → 4 (stop) | [1, 3, 5] | null |
sumRange(0, 2) | query(3): 5 + 3 = 8 | [1, 3, 5] | 8 |
The Math, Step by Step
1. The range each tree entry covers
The Fenwick tree numbers the array positions from 1, so position index of nums is . Write for the value of the lowest 1 bit of . For example, is 110 in binary, so .
Entry tree[i] holds the sum of values: the positions from to . Written as a half-open range, it covers .
| in binary | = i & -i | tree[i] covers | |
|---|---|---|---|
| 1 | 0001 | 1 | |
| 2 | 0010 | 2 | |
| 3 | 0011 | 1 | |
| 4 | 0100 | 4 | |
| 5 | 0101 | 1 | |
| 6 | 0110 | 2 | |
| 7 | 0111 | 1 | |
| 8 | 1000 | 8 |
2. Why i & -i is the lowest 1 bit
In two's complement, is computed as "flip every bit of , then add 1". Say ends with a 1 followed by zeros. Flipping gives a 0 followed by ones. Adding 1 turns those ones back into zeros and the 0 into a 1. So has the same lowest 1 bit as , with the same zeros after it. Every bit to the left of it is flipped.
& keeps a bit only where both numbers have a 1. Only the lowest 1 bit is 1 in both. So i & -i equals . For : in 8 bits, is 00000110 and is 11111010, and their & is 00000010, which is 2.
3. Why a query and an update each touch about entries
A prefix sum walks down. query(i) adds tree[i], which covers . Then it sets and repeats. The next range ends exactly where the last one started. So the ranges fit together without gaps or overlaps, and they cover . Each step clears the lowest 1 bit of , so the loop runs once per 1 bit, at most times. For the ranges are , and .
An update walks up. A change at position must reach every entry whose range contains . update(i, delta) adds delta to tree[i], then sets and repeats. Adding the lowest 1 bit moves to the next larger entry whose range still reaches down to position . Each step makes the lowest 1 bit at least twice as large, so the loop runs at most times. With , a change at position 3 visits entries 3, 4 and 8. The table of part 1 shows that these are exactly the entries that cover position 3.
A range sum is the difference of two prefix sums, as for a fixed array:
with 1-based positions. sumRange adds 1 to both indices first.
4. The 1-based positions and the size of the change
Position 0 does not work in a Fenwick tree. , so update(0, ...) would add 0 to forever. That is why the code stores position index at , and why tree has entries with tree[0] unused. For query(0), the loop while i > 0 runs zero times and returns 0, which is the sum of no elements.
The tree stores sums, so it must receive the change of a value, not the new value. NumArray.update sends val - self.nums[index], and then stores val in self.nums for the next change. Sending val itself would add the new value on top of the old one.
5. Example 1 by hand
nums = [1, 3, 5], so . The constructor calls update once per value:
| Call | Entries visited | tree[1..3] after |
|---|---|---|
update(1, 1) | 1, then | |
update(2, 3) | 2 (next would be 4, past ) | |
update(3, 5) | 3 (next would be 4, past ) |
So tree[2] = 4 holds positions 1 and 2, and tree[3] = 5 holds position 3 alone.
sumRange(0, 2) computes query(3) - query(0). query(3) adds tree[3] = 5, moves to , adds tree[2] = 4, and moves to . It returns . query(0) returns 0. The answer is , the expected output.
update(1, 2) changes nums[1] from 3 to 2, so it sends the change to position 2. Entry 2 becomes . The next entry would be , past , so the update stops.
The second sumRange(0, 2) adds tree[3] = 5 and tree[2] = 3, and returns , the expected output.
6. From the math to the code
| Math | In the code |
|---|---|
positions , tree[0] unused | self.tree = [0] * (n + 1) and index + 1 |
i & (-i) | |
| walk up to every entry that covers | while i <= self.n: with i += i & (-i) |
| walk down over ranges that tile | while i > 0: with i -= i & (-i) |
return self.query(right) - self.query(left - 1) | |
| send the change, not the new value | self.fenwick.update(index + 1, val - self.nums[index]) |
Both walks take steps. The constructor calls update times, so it takes time. The tree and the copy of nums take space.
| 1 | `tree[i]` holds the sum of the `i & (-i)` positions that end at `i`, so any prefix is a few whole blocks and any position sits in a few blocks. |
| 2 | `query(i)` adds `tree[i]` and steps `i -= i & (-i)` down to 0; `update(i, delta)` adds `delta` to `tree[i]` and steps `i += i & (-i)` up past `n`. |
| 3 | `NumArray`: keep `self.nums`, build by calling `update(index + 1, val)` for each value, `update` adds `val - self.nums[index]`, `sumRange` returns `range_query(left + 1, right + 1)`. |
| 4 | The trap: the tree is 1-indexed. `i & (-i)` is 0 when `i` is 0, so `update(0, ...)` never moves and loops forever; shift every index up by one. |
Conceptual Narrative
🧭 Conceptual Foundation & Pattern Intuition
A prefix-sum array answers range sums in but needs an rebuild after every change; a plain array updates in but sums in . A Fenwick tree (Binary Indexed Tree) meets in the middle: both operations take . Slot tree[i] stores the sum of a block of positions that ends at i and is i & (-i) long (the value of i's lowest set bit).
🏟️ The Analogy: Receipts Filed by Month, Quarter and Year
An accountant keeps receipts in folders of different sizes: some cover one month, some two, some four, some eight, each ending at a certain month. To total January to November, you add a handful of folders that together cover exactly those months. When a March receipt is corrected, you fix every folder that contains March: a handful again.
🔑 Why It Works
Removing the lowest set bit lands just before the block tree[i] covers, so the blocks query adds never overlap and cover exactly 1..i. Adding it reaches the next bigger block that contains i. Both loops touch one block per bit: .
Using index 0 in the tree:
i & (-i)is0wheniis0, soupdate(0, delta)adds forever without moving. Shift every LeetCode index by one:update(index + 1, ...).Adding
valinstead of the change:update(index, val)sets a value, but the tree can only add. Addval - self.nums[index]and then storeval, or every repeated update double-counts.Off-by-one in
sumRange: with 1-indexed slots the sum ofnums[left..right]isquery(right + 1) - query(left), which isrange_query(left + 1, right + 1).Rebuilding a prefix array on every update: correct but
O(N)per update; with3 * 104calls on3 * 104elements that is about 10^9 steps.
4-Phase Thought Process Model
Pattern Recognition Signals
The 10-second spot
"Change one element" mixed with "sum of a contiguous stretch", many times and in any order: a prefix array alone rebuilds in O(N) per change, and a plain array sums in O(N). Point updates plus prefix-based range sums are the exact job of a Fenwick Tree.
Formulating the Predicate & Invariants
Turning intuition into a boolean rule
self.tree[i] always equals the sum of the values at positions i - (i & -i) + 1 through i; update keeps it true by walking i += i & (-i), and query(i) adds disjoint blocks walking i -= i & (-i).
Silent Failure Traps & Edge Cases
Where confident candidates still lose points
This problem's traps are in Common Pitfalls above.
The 60-Second Interview Pitch
Say this out loud before you type a single line
I'd use a Fenwick Tree, because I need point updates and range sums mixed together. Slot i of the tree stores the sum of a block ending at i whose length is i's lowest set bit. A prefix sum walks down by removing that bit each time, adding whole blocks that never overlap; an update walks up by adding the bit, fixing every block that contains the position. Each walk touches one block per bit, so both are
O(log N). In NumArray I keep the current values so an update can add val minus the old value, since the tree only adds, and a range sum is the prefix at right plus one minus the prefix at left. The trap is indexing: the tree is 1-indexed, because at zero the lowest set bit is zero and the loop would never move, so I shift every index up by one. Building costsO(N log N)and space isO(N).
Complexity & Mathematical Proof
O(log N) update & query
Look at the loops: update runs i += i & (-i) until i > n; each step moves the lowest set bit of i at least one place higher, so it runs at most about log2(N) + 1 times. query runs i -= i & (-i) until i == 0; each step removes one set bit, so it runs at most log2(N) + 1 times. sumRange is two queries and NumArray.update is one tree update: O(log N) each. Building NumArray makes N tree updates: O(N log N) once.
O(N)
self.tree has N + 1 slots and self.nums keeps N current values: O(N). The loops are iterative, so there is no recursion stack.
T(update) = T(query) = O(log N); T(sumRange) = 2 · O(log N); build = N · O(log N)
Derivation Progression
N × O(log N)
__init__ calls self.fenwick.update(index + 1, val) once per value.
O(log N)
i += i & (-i) climbs at most one step per bit of N.
O(log N)
i -= i & (-i) clears one set bit per step.
2 × O(log N)
range_query is query(right) - query(left - 1).
Variable Definitions
Length of nums
The lowest set bit of i: the length of the block tree[i] covers
Memory Architecture & Bounds
O(1): both walks are loops
O(N): tree (N + 1 slots) and nums
O(1) per call
Boundary Best / Worst Cases
steps when i is a large power of two for update, or has one set bit for query
per operation; to build
per operation
Recurrence Tree Topology
Senior SWE Deconstruction & Hardware Caveats
Triggers: "change one element", "sum of a contiguous stretch", "in any order". Point updates mixed with range sums: Fenwick Tree (two prefix sums per range, O(log N) each).
values in and at most calls. Sums stay within . Naive: up to steps; Fenwick: about .
In a concurrent service, an update is several writes (one per block), so readers must not see a half-applied update: guard the walk with a lock or apply updates from a single writer.