307. Range Sum Query - Mutable
Problem Description
This problem asks you to implement a data structure that supports two operations on an integer array:
- Update Operation: Change the value of an element at a specific index in the array
- Range Sum Query: Calculate the sum of all elements between two given indices (inclusive)
You need to implement a NumArray
class with three methods:
NumArray(int[] nums)
: Constructor that initializes the data structure with the given integer arraynums
void update(int index, int val)
: Updates the element at positionindex
to have the new valueval
int sumRange(int left, int right)
: Returns the sum of all elements from indexleft
to indexright
(both inclusive)
For example, if you have an array [1, 3, 5]
:
sumRange(0, 2)
would return1 + 3 + 5 = 9
- After
update(1, 2)
, the array becomes[1, 2, 5]
- Now
sumRange(0, 2)
would return1 + 2 + 5 = 8
The challenge is to implement these operations efficiently, especially when there are many update and query operations. A naive approach would be to simply update the array directly and sum elements for each query, but this becomes inefficient with frequent operations.
The solution uses a Binary Indexed Tree (also known as Fenwick Tree) data structure, which allows both update and range sum query operations to be performed in O(log n)
time complexity. This is much more efficient than the naive O(n)
approach for range sum queries when dealing with multiple operations.
Intuition
When we need to frequently update array elements and calculate range sums, the naive approach has a trade-off problem:
- If we store the original array, updates are
O(1)
but range sum queries areO(n)
- If we store a prefix sum array, range sum queries are
O(1)
but updates becomeO(n)
since we need to update all affected prefix sums
We need a data structure that balances both operations efficiently. This leads us to think about tree-based structures that can decompose the problem hierarchically.
The key insight is that we can represent partial sums in a clever way where:
- Each position in our auxiliary array stores the sum of a specific range of elements
- These ranges are determined by the binary representation of the index
- Any prefix sum can be computed by combining a logarithmic number of these partial sums
The Binary Indexed Tree (Fenwick Tree) achieves this by using the least significant bit (LSB) of an index to determine the range of elements it's responsible for. The operation x & -x
isolates the LSB, which tells us:
- How many elements this position covers
- Which position to jump to next when traversing the tree
For updates, we only need to update the nodes that include the changed element in their range. We find these nodes by repeatedly adding the LSB to the current position: x += x & -x
.
For queries, we can compute a prefix sum by accumulating values while moving down the tree. We find the relevant nodes by repeatedly removing the LSB: x -= x & -x
.
The beauty of this approach is that both operations traverse at most O(log n)
nodes because we're essentially moving through the binary representation of the index, which has at most log n
bits. This gives us the perfect balance: both updates and range queries in O(log n)
time.
Solution Approach
The solution implements a Binary Indexed Tree (Fenwick Tree) to efficiently handle both update and range sum operations.
Binary Indexed Tree Implementation
The BinaryIndexedTree
class maintains:
n
: the size of the arrayc
: an auxiliary array of sizen + 1
(1-indexed) that stores partial sums
Key Operations:
-
update(x, delta)
: Addsdelta
to the element at positionx
while x <= self.n: self.c[x] += delta x += x & -x
- Starting from position
x
, we update all nodes that include this position in their range x & -x
gives us the least significant bit, which determines the jump distance- We keep jumping to parent nodes until we exceed the array size
- Starting from position
-
query(x)
: Returns the prefix sum from index 1 tox
s = 0 while x > 0: s += self.c[x] x -= x & -x return s
- We accumulate partial sums by traversing down the tree
x & -x
tells us which position to jump to next- We continue until we reach 0
NumArray Implementation
The NumArray
class wraps the Binary Indexed Tree to provide the required interface:
-
Constructor: Initializes the tree with the given array
self.tree = BinaryIndexedTree(len(nums)) for i, v in enumerate(nums, 1): self.tree.update(i, v)
- Creates an empty tree
- Inserts each element using 1-based indexing (Fenwick Trees are traditionally 1-indexed)
-
update(index, val)
: Updates the value at the given indexprev = self.sumRange(index, index) self.tree.update(index + 1, val - prev)
- First finds the current value at the index using
sumRange
- Updates the tree with the difference (
val - prev
) rather than the absolute value - Converts to 1-based indexing by adding 1 to the index
- First finds the current value at the index using
-
sumRange(left, right)
: Calculates the sum between indicesreturn self.tree.query(right + 1) - self.tree.query(left)
- Uses the prefix sum property:
sum[left...right] = prefixSum[right] - prefixSum[left-1]
- Adjusts for 1-based indexing by using
right + 1
andleft
- Uses the prefix sum property:
Time Complexity
- Update:
O(log n)
- traverses at mostlog n
nodes - Range Sum Query:
O(log n)
- performs two prefix sum queries, each takingO(log n)
- Initialization:
O(n log n)
- performsn
updates, each takingO(log n)
Space Complexity
O(n)
for storing the auxiliary array in the Binary Indexed Tree
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a small example with the array [1, 3, 5]
to understand how the Binary Indexed Tree works.
Initial Setup:
- Original array:
[1, 3, 5]
(0-indexed) - BIT array
c
:[0, 0, 0, 0]
(1-indexed, size n+1)
Building the tree: We insert each element one by one.
-
Insert 1 at index 1:
update(1, 1)
: Start at position 1c[1] += 1
โc = [0, 1, 0, 0]
- Next position:
1 + (1 & -1) = 1 + 1 = 2
c[2] += 1
โc = [0, 1, 1, 0]
- Next position:
2 + (2 & -2) = 2 + 2 = 4
(exceeds n=3, stop)
-
Insert 3 at index 2:
update(2, 3)
: Start at position 2c[2] += 3
โc = [0, 1, 4, 0]
- Next position:
2 + (2 & -2) = 2 + 2 = 4
(exceeds n=3, stop)
-
Insert 5 at index 3:
update(3, 5)
: Start at position 3c[3] += 5
โc = [0, 1, 4, 5]
- Next position:
3 + (3 & -3) = 3 + 1 = 4
(exceeds n=3, stop)
Final BIT array: c = [0, 1, 4, 5]
c[1]
stores sum of element at index 1:1
c[2]
stores sum of elements at indices 1-2:1 + 3 = 4
c[3]
stores sum of element at index 3:5
Query Example: sumRange(0, 2) (sum all elements)
- Convert to 1-indexed:
query(3) - query(0)
query(3)
:- Start at position 3:
s = 0 + c[3] = 0 + 5 = 5
- Next position:
3 - (3 & -3) = 3 - 1 = 2
- Add
c[2]
:s = 5 + 4 = 9
- Next position:
2 - (2 & -2) = 2 - 2 = 0
(stop) - Result:
9
- Start at position 3:
query(0)
= 0- Final result:
9 - 0 = 9
Update Example: update(1, 2) (change index 1 from 3 to 2)
- Current value at index 1:
sumRange(1, 1) = 3
- Delta:
2 - 3 = -1
update(2, -1)
in BIT (using 1-indexed position 2):c[2] += -1
โc[2] = 4 - 1 = 3
- Next position:
2 + 2 = 4
(exceeds n, stop)
- Updated BIT:
c = [0, 1, 3, 5]
Query After Update: sumRange(0, 2)
query(3) - query(0)
query(3)
:s = c[3] = 5
- Next:
s = 5 + c[2] = 5 + 3 = 8
- Result:
8
- Final result:
8 - 0 = 8
The key insight is how the BIT uses binary representation to determine which nodes to update and query. Position 2 (binary: 10) is responsible for 2 elements because its LSB represents 2. Position 3 (binary: 11) is responsible for 1 element because its LSB represents 1. This clever indexing scheme ensures we only touch O(log n) nodes for any operation.
Solution Implementation
1class BinaryIndexedTree:
2 """
3 Binary Indexed Tree (Fenwick Tree) for efficient prefix sum queries and updates.
4 Uses 1-based indexing internally.
5 """
6 __slots__ = ["n", "c"]
7
8 def __init__(self, n: int) -> None:
9 """
10 Initialize a Binary Indexed Tree with size n.
11
12 Args:
13 n: The size of the array
14 """
15 self.n = n
16 self.c = [0] * (n + 1) # 1-indexed array for the tree
17
18 def update(self, x: int, delta: int) -> None:
19 """
20 Add delta to the value at position x.
21
22 Args:
23 x: The 1-based index to update
24 delta: The value to add to position x
25 """
26 while x <= self.n:
27 self.c[x] += delta
28 # Move to the next node that this position affects
29 # x & -x gives the lowest set bit
30 x += x & -x
31
32 def query(self, x: int) -> int:
33 """
34 Get the prefix sum from index 1 to x (inclusive).
35
36 Args:
37 x: The 1-based index up to which to calculate the sum
38
39 Returns:
40 The prefix sum from 1 to x
41 """
42 total_sum = 0
43 while x > 0:
44 total_sum += self.c[x]
45 # Move to the parent node by removing the lowest set bit
46 x -= x & -x
47 return total_sum
48
49
50class NumArray:
51 """
52 Data structure that supports updating elements and calculating range sums efficiently.
53 Uses a Binary Indexed Tree internally for O(log n) updates and queries.
54 """
55 __slots__ = ["tree"]
56
57 def __init__(self, nums: list[int]) -> None:
58 """
59 Initialize the NumArray with the given array.
60
61 Args:
62 nums: The initial array of numbers
63 """
64 self.tree = BinaryIndexedTree(len(nums))
65 # Build the tree by updating each position with its initial value
66 # enumerate with start=1 for 1-based indexing
67 for i, value in enumerate(nums, 1):
68 self.tree.update(i, value)
69
70 def update(self, index: int, val: int) -> None:
71 """
72 Update the value at the given index to val.
73
74 Args:
75 index: The 0-based index to update
76 val: The new value to set at the index
77 """
78 # Get the current value at the index
79 current_value = self.sumRange(index, index)
80 # Update the tree with the difference (delta)
81 # Add 1 to index for 1-based indexing in the tree
82 self.tree.update(index + 1, val - current_value)
83
84 def sumRange(self, left: int, right: int) -> int:
85 """
86 Calculate the sum of elements from left to right (inclusive).
87
88 Args:
89 left: The 0-based left boundary of the range
90 right: The 0-based right boundary of the range
91
92 Returns:
93 The sum of elements in the range [left, right]
94 """
95 # Range sum = prefix_sum(right) - prefix_sum(left - 1)
96 # Add 1 to convert from 0-based to 1-based indexing
97 return self.tree.query(right + 1) - self.tree.query(left)
98
99
100# Your NumArray object will be instantiated and called as such:
101# obj = NumArray(nums)
102# obj.update(index, val)
103# param_2 = obj.sumRange(left, right)
104
1/**
2 * Binary Indexed Tree (Fenwick Tree) implementation for efficient range sum queries
3 * and point updates. Uses 1-based indexing internally.
4 */
5class BinaryIndexedTree {
6 private int size;
7 private int[] tree;
8
9 /**
10 * Initializes the Binary Indexed Tree with given size
11 * @param size the size of the array to be represented
12 */
13 public BinaryIndexedTree(int size) {
14 this.size = size;
15 // Use 1-based indexing, so array size is size + 1
16 this.tree = new int[size + 1];
17 }
18
19 /**
20 * Updates the value at position index by adding delta
21 * @param index the position to update (1-based)
22 * @param delta the value to add
23 */
24 public void update(int index, int delta) {
25 // Propagate the update up the tree
26 while (index <= size) {
27 tree[index] += delta;
28 // Move to next node that this index affects
29 // index & -index gives the lowest set bit
30 index += index & -index;
31 }
32 }
33
34 /**
35 * Queries the prefix sum from index 1 to index (inclusive)
36 * @param index the end position of the range (1-based)
37 * @return the sum of elements from 1 to index
38 */
39 public int query(int index) {
40 int sum = 0;
41 // Traverse ancestors to calculate prefix sum
42 while (index > 0) {
43 sum += tree[index];
44 // Move to parent node by removing the lowest set bit
45 index -= index & -index;
46 }
47 return sum;
48 }
49}
50
51/**
52 * NumArray class that supports range sum queries and point updates
53 * using a Binary Indexed Tree for O(log n) operations
54 */
55class NumArray {
56 private BinaryIndexedTree binaryIndexedTree;
57
58 /**
59 * Initializes the NumArray with the given array
60 * @param nums the initial array of numbers
61 */
62 public NumArray(int[] nums) {
63 int length = nums.length;
64 binaryIndexedTree = new BinaryIndexedTree(length);
65
66 // Build the Binary Indexed Tree with initial values
67 for (int i = 0; i < length; i++) {
68 // Convert to 1-based indexing for the tree
69 binaryIndexedTree.update(i + 1, nums[i]);
70 }
71 }
72
73 /**
74 * Updates the value at the given index to a new value
75 * @param index the position to update (0-based)
76 * @param val the new value to set
77 */
78 public void update(int index, int val) {
79 // Get the current value at this index
80 int currentValue = sumRange(index, index);
81 // Update by the difference between new and old value
82 // Convert to 1-based indexing for the tree
83 binaryIndexedTree.update(index + 1, val - currentValue);
84 }
85
86 /**
87 * Returns the sum of elements from left to right (inclusive)
88 * @param left the start index of the range (0-based)
89 * @param right the end index of the range (0-based)
90 * @return the sum of elements in the range [left, right]
91 */
92 public int sumRange(int left, int right) {
93 // Sum[left, right] = Sum[0, right] - Sum[0, left-1]
94 // Convert to 1-based indexing for the tree
95 return binaryIndexedTree.query(right + 1) - binaryIndexedTree.query(left);
96 }
97}
98
99/**
100 * Your NumArray object will be instantiated and called as such:
101 * NumArray obj = new NumArray(nums);
102 * obj.update(index, val);
103 * int param_2 = obj.sumRange(left, right);
104 */
105
1/**
2 * Binary Indexed Tree (Fenwick Tree) for efficient range sum queries and point updates
3 * Time Complexity: O(log n) for both update and query operations
4 * Space Complexity: O(n)
5 */
6class BinaryIndexedTree {
7public:
8 int size; // Size of the array
9 vector<int> tree; // BIT array (1-indexed)
10
11 /**
12 * Constructor: Initialize BIT with given size
13 * @param n: Size of the array to be represented
14 */
15 BinaryIndexedTree(int n) : size(n), tree(n + 1, 0) {}
16
17 /**
18 * Update the value at index by adding delta
19 * @param index: 1-based index to update
20 * @param delta: Value to add at the index
21 */
22 void update(int index, int delta) {
23 // Traverse all ancestors of index in the BIT
24 while (index <= size) {
25 tree[index] += delta;
26 // Get next index by adding the last set bit
27 index += index & (-index);
28 }
29 }
30
31 /**
32 * Get prefix sum from index 1 to index (inclusive)
33 * @param index: 1-based index up to which sum is calculated
34 * @return: Sum of elements from 1 to index
35 */
36 int query(int index) {
37 int sum = 0;
38 // Traverse all parents of index in the BIT
39 while (index > 0) {
40 sum += tree[index];
41 // Get parent index by removing the last set bit
42 index -= index & (-index);
43 }
44 return sum;
45 }
46};
47
48/**
49 * NumArray class supporting range sum queries with point updates
50 * Uses Binary Indexed Tree for efficient operations
51 */
52class NumArray {
53public:
54 BinaryIndexedTree* fenwickTree; // Pointer to the BIT instance
55
56 /**
57 * Constructor: Build the BIT from initial array
58 * @param nums: Initial array of integers
59 */
60 NumArray(vector<int>& nums) {
61 int n = nums.size();
62 fenwickTree = new BinaryIndexedTree(n);
63
64 // Initialize BIT with all values from nums
65 // Note: BIT uses 1-based indexing, so we add 1 to indices
66 for (int i = 0; i < n; ++i) {
67 fenwickTree->update(i + 1, nums[i]);
68 }
69 }
70
71 /**
72 * Update the value at given index to val
73 * @param index: 0-based index to update
74 * @param val: New value to set at index
75 */
76 void update(int index, int val) {
77 // Get current value at index
78 int currentValue = sumRange(index, index);
79 // Update with the difference (new value - current value)
80 fenwickTree->update(index + 1, val - currentValue);
81 }
82
83 /**
84 * Calculate sum of elements from left to right (inclusive)
85 * @param left: 0-based left boundary of range
86 * @param right: 0-based right boundary of range
87 * @return: Sum of elements in range [left, right]
88 */
89 int sumRange(int left, int right) {
90 // Range sum = prefixSum(right) - prefixSum(left - 1)
91 // Convert to 1-based indexing for BIT
92 return fenwickTree->query(right + 1) - fenwickTree->query(left);
93 }
94};
95
96/**
97 * Your NumArray object will be instantiated and called as such:
98 * NumArray* obj = new NumArray(nums);
99 * obj->update(index, val);
100 * int param_2 = obj->sumRange(left, right);
101 */
102
1// Binary Indexed Tree (Fenwick Tree) for efficient range sum queries
2// Array size for the tree
3let treeSize: number;
4// Tree array storing cumulative values (1-indexed)
5let treeArray: number[];
6
7// Original array reference for NumArray operations
8let originalNums: number[];
9
10/**
11 * Initializes the Binary Indexed Tree with given size
12 * @param size - The size of the array to support
13 */
14function initializeBIT(size: number): void {
15 treeSize = size;
16 treeArray = new Array(size + 1).fill(0);
17}
18
19/**
20 * Updates the Binary Indexed Tree by adding delta to position x
21 * Uses the property: x & -x gives the rightmost set bit
22 * @param position - The position to update (1-indexed)
23 * @param delta - The value to add at the position
24 */
25function updateBIT(position: number, delta: number): void {
26 while (position <= treeSize) {
27 treeArray[position] += delta;
28 // Move to next position affected by this update
29 position += position & -position;
30 }
31}
32
33/**
34 * Queries the prefix sum from index 1 to x (inclusive)
35 * @param position - The end position for prefix sum (1-indexed)
36 * @returns The sum of elements from index 1 to position
37 */
38function queryBIT(position: number): number {
39 let sum = 0;
40 while (position > 0) {
41 sum += treeArray[position];
42 // Move to parent node in the tree
43 position -= position & -position;
44 }
45 return sum;
46}
47
48/**
49 * Constructor function for NumArray
50 * Initializes the data structure with the given integer array nums
51 * @param nums - Initial array of numbers
52 */
53function NumArray(nums: number[]): void {
54 const n = nums.length;
55 originalNums = [...nums];
56
57 // Initialize Binary Indexed Tree
58 initializeBIT(n);
59
60 // Build the tree by updating each position with initial values
61 for (let i = 0; i < n; i++) {
62 updateBIT(i + 1, nums[i]);
63 }
64}
65
66/**
67 * Updates the value of nums at index to val
68 * @param index - The index to update (0-indexed)
69 * @param val - The new value to set
70 */
71function update(index: number, val: number): void {
72 // Calculate the difference between new and old value
73 const previousValue = sumRange(index, index);
74 // Update the tree with the difference
75 updateBIT(index + 1, val - previousValue);
76}
77
78/**
79 * Returns the sum of elements between indices left and right (inclusive)
80 * @param left - Starting index (0-indexed)
81 * @param right - Ending index (0-indexed)
82 * @returns The sum of elements from left to right
83 */
84function sumRange(left: number, right: number): number {
85 // Use prefix sum property: sum(left, right) = sum(0, right) - sum(0, left-1)
86 return queryBIT(right + 1) - queryBIT(left);
87}
88
Time and Space Complexity
Time Complexity:
-
Initialization (
__init__
):O(n log n)
wheren
is the length of the input array. The initialization creates a Binary Indexed Tree and performsn
update operations. Each update operation takesO(log n)
time as it traverses up the tree by adding the least significant bit (x += x & -x
), which visits at mostlog n
nodes. -
Update operation (
update
):O(log n)
. The update method first callssumRange
to get the previous value at the index, which takesO(log n)
time. Then it calls the tree's update method with the difference, which also takesO(log n)
time. Total:O(log n) + O(log n) = O(log n)
. -
Range sum query (
sumRange
):O(log n)
. The method performs two prefix sum queries using the Binary Indexed Tree's query method. Each query operation takesO(log n)
time as it traverses down the tree by removing the least significant bit (x -= x & -x
), visiting at mostlog n
nodes. Total:O(log n) + O(log n) = O(log n)
.
Space Complexity:
O(n)
where n
is the length of the input array. The Binary Indexed Tree maintains an array c
of size n + 1
to store the tree nodes. No additional significant space is used beyond this array. The __slots__
declaration helps optimize memory usage by preventing the creation of __dict__
for instances, but doesn't change the asymptotic space complexity.
Common Pitfalls
1. Index Confusion Between 0-based and 1-based Systems
The Pitfall: One of the most common mistakes when implementing a Binary Indexed Tree is mixing up 0-based indexing (used by the NumArray interface) and 1-based indexing (used internally by the Fenwick Tree). This can lead to:
- Off-by-one errors
- Incorrect range sum calculations
- Array index out of bounds exceptions
- Infinite loops in the update/query operations
Example of the mistake:
# WRONG: Forgetting to convert indices
def sumRange(self, left: int, right: int) -> int:
return self.tree.query(right) - self.tree.query(left) # Missing +1 adjustment
The Solution: Always be explicit about index conversions at the boundary between the public interface and the internal tree implementation:
def update(self, index: int, val: int) -> None:
# Explicitly convert 0-based to 1-based
tree_index = index + 1
current_value = self.sumRange(index, index)
self.tree.update(tree_index, val - current_value)
def sumRange(self, left: int, right: int) -> int:
# Convert to 1-based: right+1 for inclusive, left stays as is for exclusive prefix
return self.tree.query(right + 1) - self.tree.query(left)
2. Incorrect Delta Calculation in Update
The Pitfall: When updating a value, directly passing the new value instead of the difference (delta) to the tree's update method. The Binary Indexed Tree stores partial sums, so it needs the change amount, not the absolute value.
Example of the mistake:
# WRONG: Passing absolute value instead of delta
def update(self, index: int, val: int) -> None:
self.tree.update(index + 1, val) # This adds val to existing value!
The Solution: Calculate the difference between the new and old values:
def update(self, index: int, val: int) -> None:
current_value = self.sumRange(index, index)
delta = val - current_value
self.tree.update(index + 1, delta)
3. Bit Manipulation Error in Tree Navigation
The Pitfall:
Using incorrect bit manipulation for finding the next node to update or query. The expression x & -x
isolates the least significant bit, which is crucial for tree traversal.
Example of the mistake:
# WRONG: Various incorrect bit manipulations x += x & x # Doesn't isolate LSB x += x & (x-1) # Removes LSB instead of isolating it x += 1 # Simple increment doesn't follow tree structure
The Solution: Use the correct bit manipulation formulas:
def update(self, x: int, delta: int) -> None:
while x <= self.n:
self.c[x] += delta
x += x & -x # Correct: adds the least significant bit
def query(self, x: int) -> int:
s = 0
while x > 0:
s += self.c[x]
x -= x & -x # Correct: removes the least significant bit
return s
4. Initialization with Wrong Tree Size
The Pitfall:
Creating the Binary Indexed Tree with size n
instead of n + 1
for the internal array, or forgetting that the tree uses 1-based indexing.
Example of the mistake:
# WRONG: Array too small
def __init__(self, n: int) -> None:
self.n = n
self.c = [0] * n # Should be n + 1
The Solution:
Always allocate n + 1
elements for 1-based indexing:
def __init__(self, n: int) -> None:
self.n = n
self.c = [0] * (n + 1) # Correct size for 1-based indexing
Which algorithm should you use to find a node that is close to the root of the tree?
Recommended Readings
Segment Tree Faster Range Queries For this article we want to introduce the idea of a Segment Tree Segment Trees allow us to quickly perform range queries as well as range updates Suppose we have an array and we want to know the sum of a particular range of numbers as well
https assets algo monster cover_photos dnd svg Divide and Conquer The main idea of divide and conquer is to split the main problem into two smaller components assuming that each one of the components had already been solved recursively and then try to solve the bigger problem using the solutions
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Want a Structured Path to Master System Design Too? Donโt Miss This!