Facebook Pixel

303. Range Sum Query - Immutable

Problem Description

You need to design a data structure that can efficiently calculate the sum of elements in a given range of an integer array.

The problem asks you to implement a class called NumArray with the following functionality:

  1. Constructor NumArray(int[] nums): Takes an integer array nums as input and initializes the data structure with this array.

  2. Method sumRange(int left, int right): Returns the sum of all elements from index left to index right (inclusive). In other words, it calculates nums[left] + nums[left + 1] + ... + nums[right].

The key challenge is that multiple queries will be made using the sumRange method, so you need an efficient way to handle these repeated range sum queries rather than recalculating the sum from scratch each time.

Example usage:

  • First, create a NumArray object with an array like [1, 3, 5, 7, 9]
  • Then call sumRange(1, 3) which should return 3 + 5 + 7 = 15
  • Call sumRange(0, 2) which should return 1 + 3 + 5 = 9

The solution uses a prefix sum technique where we precompute cumulative sums during initialization. This allows each sumRange query to be answered in constant time O(1) by using the formula: prefix_sum[right + 1] - prefix_sum[left].

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

When we need to calculate range sums multiple times, the naive approach would be to iterate through the array from index left to right for each query, which takes O(n) time per query. If we have many queries, this becomes inefficient.

The key insight is that we can preprocess the array once to make all future queries fast. Think about how cumulative sums work: if we know the sum of elements from the start of the array up to any position, we can use this information to quickly find the sum of any subrange.

Consider an array [2, 4, 6, 8]. If we create a prefix sum array that stores cumulative sums from the beginning:

  • s[0] = 0 (sum of no elements)
  • s[1] = 2 (sum up to index 0)
  • s[2] = 6 (sum up to index 1)
  • s[3] = 12 (sum up to index 2)
  • s[4] = 20 (sum up to index 3)

Now, to find the sum between indices 1 and 2 (which is 4 + 6 = 10), we can observe that:

  • The sum up to index 2 is s[3] = 12
  • The sum up to index 0 is s[1] = 2
  • The difference 12 - 2 = 10 gives us exactly the sum we want!

This works because s[right + 1] contains the sum of all elements from index 0 to right, and s[left] contains the sum of all elements from index 0 to left - 1. When we subtract them, the elements before index left cancel out, leaving us with just the sum from left to right.

By spending O(n) time upfront to build the prefix sum array, we can answer each subsequent range query in just O(1) time using simple subtraction.

Solution Approach

The solution implements the prefix sum technique to efficiently handle multiple range sum queries.

Data Structure: We use a prefix sum array s where s[i] stores the sum of the first i elements from the original array. The array has length n + 1 where n is the length of the input array.

Implementation Details:

  1. Initialization (__init__ method):

    • We use Python's accumulate function from the itertools module to build the prefix sum array
    • The initial=0 parameter ensures our prefix sum array starts with 0, representing the sum of zero elements
    • For an input array nums = [a, b, c, d], the prefix sum array becomes s = [0, a, a+b, a+b+c, a+b+c+d]
    • This preprocessing takes O(n) time and O(n) space
  2. Range Sum Query (sumRange method):

    • To find the sum between indices left and right (inclusive), we use the formula: s[right + 1] - s[left]
    • Why this works:
      • s[right + 1] = sum of elements from index 0 to right
      • s[left] = sum of elements from index 0 to left - 1
      • Subtracting these gives us the sum from index left to right
    • Each query is answered in O(1) time

Example Walkthrough: For nums = [1, 3, 5, 7, 9]:

  • Prefix sum array: s = [0, 1, 4, 9, 16, 25]
  • Query sumRange(1, 3): returns s[4] - s[1] = 16 - 1 = 15
  • Query sumRange(0, 2): returns s[3] - s[0] = 9 - 0 = 9

This approach trades additional space for query speed, making it ideal when we need to perform many range sum queries on the same array.

Ready to land your dream job?

Unlock your dream job with a 3-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through the solution with a concrete example to see how the prefix sum technique works.

Given array: nums = [3, 5, 2, 8, 4]

Step 1: Build the prefix sum array during initialization

We create a prefix sum array s with length 6 (one more than the original array):

  • s[0] = 0 (sum of no elements)
  • s[1] = 0 + 3 = 3 (sum up to index 0)
  • s[2] = 3 + 5 = 8 (sum up to index 1)
  • s[3] = 8 + 2 = 10 (sum up to index 2)
  • s[4] = 10 + 8 = 18 (sum up to index 3)
  • s[5] = 18 + 4 = 22 (sum up to index 4)

So our prefix sum array is: s = [0, 3, 8, 10, 18, 22]

Step 2: Process range sum queries

Query 1: sumRange(1, 3) - Find sum of elements from index 1 to 3

  • We need: nums[1] + nums[2] + nums[3] = 5 + 2 + 8 = 15
  • Using our formula: s[right + 1] - s[left] = s[4] - s[1] = 18 - 3 = 15

Query 2: sumRange(0, 4) - Find sum of all elements

  • We need: nums[0] + nums[1] + nums[2] + nums[3] + nums[4] = 3 + 5 + 2 + 8 + 4 = 22
  • Using our formula: s[5] - s[0] = 22 - 0 = 22

Query 3: sumRange(2, 2) - Find sum of single element at index 2

  • We need: nums[2] = 2
  • Using our formula: s[3] - s[2] = 10 - 8 = 2

Why the formula works: When we calculate s[right + 1] - s[left]:

  • s[right + 1] contains the sum from index 0 to right
  • s[left] contains the sum from index 0 to left - 1
  • Subtracting removes all elements before index left, leaving exactly the range we want

This preprocessing allows us to answer any range sum query in constant O(1) time, regardless of the range size!

Solution Implementation

1from typing import List
2from itertools import accumulate
3
4
5class NumArray:
6    def __init__(self, nums: List[int]):
7        # Create a prefix sum array with an initial 0 at the beginning
8        # prefix_sums[i] represents the sum of elements from index 0 to i-1
9        # This allows us to calculate range sums efficiently
10        self.prefix_sums = list(accumulate(nums, initial=0))
11
12    def sumRange(self, left: int, right: int) -> int:
13        # Calculate the sum of elements from index left to right (inclusive)
14        # Sum[left, right] = prefix_sums[right+1] - prefix_sums[left]
15        # This works because:
16        # - prefix_sums[right+1] contains sum of elements from 0 to right
17        # - prefix_sums[left] contains sum of elements from 0 to left-1
18        # - Subtracting gives us the sum from left to right
19        return self.prefix_sums[right + 1] - self.prefix_sums[left]
20
21
22# Your NumArray object will be instantiated and called as such:
23# obj = NumArray(nums)
24# param_1 = obj.sumRange(left,right)
25
1class NumArray {
2    // Prefix sum array where prefixSum[i] stores sum of elements from index 0 to i-1
3    private int[] prefixSum;
4
5    /**
6     * Constructor initializes the prefix sum array
7     * @param nums Input array of integers
8     */
9    public NumArray(int[] nums) {
10        int length = nums.length;
11      
12        // Create prefix sum array with size n+1 for easier calculation
13        // prefixSum[0] = 0, prefixSum[i] = sum of nums[0] to nums[i-1]
14        prefixSum = new int[length + 1];
15      
16        // Build prefix sum array
17        for (int i = 0; i < length; i++) {
18            prefixSum[i + 1] = prefixSum[i] + nums[i];
19        }
20    }
21
22    /**
23     * Returns the sum of elements between indices left and right (inclusive)
24     * @param left Starting index (inclusive)
25     * @param right Ending index (inclusive)
26     * @return Sum of elements from nums[left] to nums[right]
27     */
28    public int sumRange(int left, int right) {
29        // Sum from left to right = prefixSum[right+1] - prefixSum[left]
30        // This works because prefixSum[right+1] contains sum up to index right
31        // and prefixSum[left] contains sum up to index left-1
32        return prefixSum[right + 1] - prefixSum[left];
33    }
34}
35
36/**
37 * Your NumArray object will be instantiated and called as such:
38 * NumArray obj = new NumArray(nums);
39 * int param_1 = obj.sumRange(left,right);
40 */
41
1class NumArray {
2public:
3    /**
4     * Constructor: Preprocesses the input array to build a prefix sum array
5     * @param nums: The input array of integers
6     * Time Complexity: O(n), where n is the size of nums
7     * Space Complexity: O(n) for storing the prefix sum array
8     */
9    NumArray(vector<int>& nums) {
10        int n = nums.size();
11        // Create prefix sum array with size n+1
12        // prefixSum[i] stores the sum of elements from nums[0] to nums[i-1]
13        prefixSum.resize(n + 1);
14      
15        // Build the prefix sum array
16        // prefixSum[0] = 0 (sum of zero elements)
17        for (int i = 0; i < n; ++i) {
18            prefixSum[i + 1] = prefixSum[i] + nums[i];
19        }
20    }
21  
22    /**
23     * Calculates the sum of elements between indices left and right (inclusive)
24     * @param left: The starting index (inclusive)
25     * @param right: The ending index (inclusive)
26     * @return: The sum of elements from nums[left] to nums[right]
27     * Time Complexity: O(1)
28     */
29    int sumRange(int left, int right) {
30        // Sum from left to right = prefixSum[right+1] - prefixSum[left]
31        // This gives us the sum of elements from index left to right (inclusive)
32        return prefixSum[right + 1] - prefixSum[left];
33    }
34  
35private:
36    vector<int> prefixSum;  // Stores cumulative sum where prefixSum[i] = sum of nums[0...i-1]
37};
38
39/**
40 * Your NumArray object will be instantiated and called as such:
41 * NumArray* obj = new NumArray(nums);
42 * int param_1 = obj->sumRange(left,right);
43 */
44
1// Global array to store prefix sums
2let prefixSums: number[];
3
4/**
5 * Initializes the data structure with the given array of numbers.
6 * Builds a prefix sum array where prefixSums[i] represents the sum of elements from index 0 to i-1.
7 * @param nums - The input array of numbers
8 */
9function NumArray(nums: number[]): void {
10    const length = nums.length;
11  
12    // Initialize prefix sum array with size n+1, all elements set to 0
13    // prefixSums[0] = 0 (sum of no elements)
14    // prefixSums[i] = sum of nums[0] to nums[i-1]
15    prefixSums = Array(length + 1).fill(0);
16  
17    // Build the prefix sum array
18    // Each position stores the cumulative sum up to that point
19    for (let i = 0; i < length; i++) {
20        prefixSums[i + 1] = prefixSums[i] + nums[i];
21    }
22}
23
24/**
25 * Calculates the sum of elements between indices left and right (inclusive).
26 * Uses the prefix sum array to compute the range sum in O(1) time.
27 * @param left - The starting index of the range (inclusive)
28 * @param right - The ending index of the range (inclusive)
29 * @returns The sum of elements from nums[left] to nums[right]
30 */
31function sumRange(left: number, right: number): number {
32    // Range sum = (sum from 0 to right) - (sum from 0 to left-1)
33    // This gives us the sum from left to right inclusive
34    return prefixSums[right + 1] - prefixSums[left];
35}
36

Time and Space Complexity

Time Complexity:

  • Initialization (__init__): O(n), where n is the length of the input array nums. The accumulate function iterates through all elements once to build the prefix sum array.
  • Query (sumRange): O(1). Each range sum query only requires two array lookups and one subtraction operation, all of which are constant time operations.

Space Complexity:

  • O(n), where n is the length of the input array. The prefix sum array self.s stores n + 1 elements (including the initial 0), which requires linear extra space.

The algorithm uses the prefix sum technique where self.s[i] stores the sum of all elements from index 0 to index i-1 in the original array. This allows for efficient range sum queries using the formula: sum(nums[left:right+1]) = self.s[right + 1] - self.s[left].

Common Pitfalls

1. Off-by-One Errors with Index Mapping

The most common mistake is incorrectly mapping between the original array indices and the prefix sum array indices. Since the prefix sum array has an extra element at the beginning (starting with 0), developers often confuse the index relationships.

Incorrect Implementation:

# Wrong: Using wrong indices
def sumRange(self, left: int, right: int) -> int:
    return self.prefix_sums[right] - self.prefix_sums[left - 1]  # Fails when left = 0

Why it fails: When left = 0, this would try to access prefix_sums[-1], which either gives an incorrect value (last element in Python) or throws an index error in other languages.

Correct Solution:

def sumRange(self, left: int, right: int) -> int:
    return self.prefix_sums[right + 1] - self.prefix_sums[left]

2. Building Prefix Sum Array Without Initial Zero

Another pitfall is constructing the prefix sum array without the initial zero element, making the range sum calculation more complex and error-prone.

Incorrect Implementation:

def __init__(self, nums: List[int]):
    # Wrong: No initial 0
    self.prefix_sums = list(accumulate(nums))  # [1, 4, 9, 16, 25] instead of [0, 1, 4, 9, 16, 25]

Why it fails: Without the initial zero, calculating sums that start from index 0 becomes complicated, requiring special case handling.

Correct Solution:

def __init__(self, nums: List[int]):
    self.prefix_sums = list(accumulate(nums, initial=0))  # Includes initial 0

3. Manual Prefix Sum Construction Errors

When implementing prefix sums manually without using accumulate, developers sometimes make mistakes in the loop logic.

Incorrect Implementation:

def __init__(self, nums: List[int]):
    self.prefix_sums = [0] * len(nums)
    for i in range(len(nums)):
        self.prefix_sums[i] = self.prefix_sums[i-1] + nums[i]  # Wrong size and logic

Correct Manual Implementation:

def __init__(self, nums: List[int]):
    self.prefix_sums = [0] * (len(nums) + 1)  # Note: size is n+1
    for i in range(len(nums)):
        self.prefix_sums[i + 1] = self.prefix_sums[i] + nums[i]

4. Not Handling Edge Cases

While the provided solution handles edge cases well, developers might forget to consider:

  • Empty arrays (though usually not tested in this problem)
  • Single element arrays
  • Queries where left == right

The beauty of the prefix sum approach with initial zero is that it naturally handles all these cases without special logic.

Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

Which of the following problems can be solved with backtracking (select multiple)


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More