Problem
Given an integer array nums
, find the sum of the elements between indices i and j (i ≤ j), inclusive. The update(i, val)
function modifies nums
by updating the element at index i
to val
.
OR
Given an integer array nums
, handle multiple queries of the following types:
- Update the value of an element in
nums
. - Calculate the sum of the elements of
nums
between indicesleft
andright
inclusive whereleft <= right
.
Implement the NumArray
class:
NumArray(int[] nums)
Initializes the object with the integer arraynums
.void update(int index, int val)
Updates the value ofnums[index]
to beval
.int sumRange(int left, int right)
Returns the sum of the elements ofnums
between indicesleft
andright
inclusive (i.e.nums[left] + nums[left + 1] + ... + nums[right]
).
Solution
Method 1 - Naive
A basic approach involves iterating through the range from l
to r
to compute the sum of elements. Updating a value can be done directly with nums[i] = x
. This results in the sum operation taking O(n)
time and the update operation taking O(1)
time.
Alternatively, another array can be constructed to store the cumulative sum up to the ith index. This allows range sums to be calculated in O(1)
time but makes update operations take O(n)
time. This approach is suitable when query operations are frequent but updates are rare.
Method 2 - Segment Tree
To solve the problem efficiently, we can use a Segment Tree data structure to handle two types of operations:
- Update queries to change values at specific indices in the array.
- Range sum queries to compute the sum of elements within a given range.
The Segment Tree allows us to perform both update and range sum operations efficiently in O(log n)
time.
Representation of Segment Tree
- The leaf nodes in a segment tree correspond to the elements of the input array.
- Each internal node represents the result of merging its child nodes, with the type of merging depending on the problem. For this problem, the merging is the sum of the elements under a node. Segment trees are often represented using arrays: the left child of a node at index
i
is at2*i+1
, the right child at2*i+2
, and the parent node at(i-1)/2
.
Construction of Segment Tree
To construct a segment tree, start with the segment arr[0...n-1]
. Recursively divide the segment into two halves (unless it is already of length 1) and repeat the process for each half. At every segment, store the sum in the corresponding tree node.
The segment tree is a Full Binary Tree because segments are always split into two halves. All tree levels, except possibly the last one, are completely filled. With n
leaves, there are n-1
internal nodes, making the total number of nodes 2*n - 1
. The height of the tree is log2(n)
. Representing the tree as an array requires memory proportional to 2 * 2^log2(n) - 1
to maintain parent-child relationships.
Querying for sum of given range
After constructing the segment tree, we can retrieve the sum of elements using it. Below is the algorithm to compute the sum in the segment tree.
Updation of value
Similar to tree construction and query operations, updates can be performed recursively. For a given index to update, let diff
represent the value to be added. Starting from the root of the segment tree, diff
is added to all nodes whose range includes the specified index. Nodes that do not include the index in their range remain unchanged.
Code
|
|
|
|
Complexity
- ⏰ Time complexity
- Building the segment tree:
O(n)
(because we initialise all nodes in the tree). - Range sum query:
O(log n)
(since we traverse the tree height for queries). - Update operation:
O(log n)
(similar reasoning as querying).
- Building the segment tree:
- 🧺 Space complexity:
O(4*n)
space as the array is stored in a binary tree structure with some extra storage for intermediate nodes.
Method 3 - Binary Index Tree / Fenwick Tree
We can use a Fenwick Tree (or Binary Indexed Tree) to handle the same operations efficiently. Fenwick Tree allows:
- Range sum queries and
- Update operations in
O(log n)
time.
Fenwick Tree is an efficient data structure based on prefix sums. Each node (or index) keeps track of the cumulative sum of a certain range, allowing us to calculate range sums and update indices efficiently by adjusting sums in the tree.
Operations
- Update:
- When a value at index
index
is updated, we propagate the difference (delta = newValue - oldValue
) to update the cumulative sums stored in the tree. - For each update, the relevant indices are updated incrementally based on powers of two in the binary representation.
- When a value at index
- Query:
- To compute the sum of elements between indices
left
andright
, we use prefix sums:sumRange(left, right) = prefixSum(right) - prefixSum(left - 1)
- The prefix sum is easily calculated using the Fenwick Tree.
- To compute the sum of elements between indices
Code
|
|
|
|
Complexity
- ⏰ Time complexity
- Update operation:
O(log n)
(due to traversal of the tree to update cumulative sums at relevant indices). - Range sum query:
O(log n)
(prefix sums are computed by traversing the tree).
- Update operation:
- 🧺 Space complexity:
O(n)
(tree size proportional to the array size).