Lesson 17 of 66 5 min

Problem: Range Sum Query (Easy)

Master the Prefix Sum pattern. Learn how to transform O(N) range queries into O(1) constant time by pre-calculating the cumulative totals of an array.

1. Problem Statement

Given an integer array nums, handle multiple queries of the following type:

  1. Calculate the sum of the elements of nums between indices left and right inclusive where left <= right.

Implement the NumArray class:

  • NumArray(int[] nums) Initializes the object with the integer array nums.
  • int sumRange(int left, int right) Returns the sum of the elements of nums between indices left and right inclusive.

Input: ["NumArray", "sumRange", "sumRange"] [[[[-2, 0, 3, -5, 2, -1]]], [0, 2], [2, 5]]
Output: [null, 1, -1]

2. The Mental Model: The "Subtraction" Intuition

Imagine you have a long tape measure. You want to find the length of the section from the 10-inch mark to the 25-inch mark. Instead of counting every inch between them, you simply take the total distance from the start (25 inches) and subtract the distance you don't need (the first 10 inches).

In arrays, we call this the Prefix Sum.

  • Prefix[i] stores the sum of all elements from nums[0] to nums[i-1].
  • Sum(left, right) = Prefix[right + 1] - Prefix[left].

This allows us to answer any query in $O(1)$ time after a single $O(N)$ preprocessing step.

3. Visual Execution (Cumulative Totals)

graph TD
    Nums[-2, 0, 3, -5, 2, -1]
    Prefix[0, -2, -2, 1, -4, -2, -3]
    
    subgraph "Query: Sum(2, 5)"
        Q[Prefix(6) - Prefix(2)]
        V[-3 - (-2)]
        R[Result: -1]
    end

4. Java Implementation (Optimal)

class NumArray {
    private int[] prefixSums;

    public NumArray(int[] nums) {
        // 1. Create a prefix array of size N + 1
        // The first element (index 0) is a sentinel 0.
        prefixSums = new int[nums.length + 1];
        
        // 2. Pre-calculate: O(N)
        for (int i = 0; i < nums.length; i++) {
            prefixSums[i + 1] = prefixSums[i] + nums[i];
        }
    }

    public int sumRange(int left, int right) {
        // 3. Query: O(1)
        // Note: prefixSums[right+1] is sum(0...right)
        // prefixSums[left] is sum(0...left-1)
        return prefixSums[right + 1] - prefixSums[left];
    }
}

5. Verbal Interview Script (Staff Tier)

Interviewer: "What is the complexity difference between a naive sum and prefix sums?"

You: "If we have $M$ queries on an array of size $N$, the naive approach takes $O(M \cdot N)$ because each query performs a full scan. By using a Prefix Sum array, I can reduce the total complexity to $O(N + M)$. The $O(N)$ is for the one-time preprocessing, and each subsequent query is a constant $O(1)$ subtraction. In a senior role, I always consider the Trade-off: Prefix Sums are perfect for static data where queries are frequent. However, if the array elements were being updated frequently, I would instead use a Segment Tree or a Fenwick Tree to maintain $O(\log N)$ updates and $O(\log N)$ queries."

6. Staff-Level Follow-Ups

Follow-up 1: "What about a 2D matrix?"

  • The Answer: "The logic extends to 2D using 2D Prefix Sums. Each cell (r, c) would store the sum of the rectangle from (0,0) to (r,c). To query a sub-rectangle, I would use the inclusion-exclusion principle, involving 4 lookups and 3 additions/subtractions. It remains $O(1)$ for queries."

Follow-up 2: "How do you handle integer overflow in the sum?"

  • The Answer: "If the array contains many large integers, int will overflow quickly. In a production Java app, I would use a long[] for the prefix sums to safely handle totals up to $2^{63}-1$. If even that isn't enough (e.g., big data analytics), I would use BigInteger."

7. Performance Nuances (The Java Perspective)

  1. Immutability: Since the prefix array is only created once, I mark it as private final. This is a clear signal to the JVM that the data won't change, allowing for potential JIT compiler optimizations.
  2. Indexing Trick: Using a size N+1 prefix array is a common senior pattern. It eliminates the need for an if (left == 0) check inside the sumRange method, making the hot path branch-free and faster.

6. Staff-Level Verbal Masterclass (Communication)

Interviewer: "How would you defend this specific implementation in a production review?"

You: "In a mission-critical environment, I prioritize the Big-O efficiency of the primary data path, but I also focus on the Predictability of the system. In this implementation, I chose a state-based dynamic programming approach. While a recursive solution is more readable, I would strictly monitor the stack depth. If this were to handle skewed inputs, I would immediately transition to an explicit stack on the heap to avoid a StackOverflowError. From a memory perspective, I leverage primitive arrays to ensure that we minimize the garbage collection pauses (Stop-the-world) that typically plague high-throughput Java applications."

7. Global Scale & Distributed Pivot

When a problem like this is moved from a single machine to a global distributed architecture, the constraints change fundamentally.

  1. Data Partitioning: We would shard the input space using Consistent Hashing. This ensures that even if our dataset grows to petabytes, any single query only hits a small subset of our cluster, maintaining logarithmic lookup times.
  2. State Consistency: For problems involving state updates (like DP or Caching), we would use a Distributed Consensus protocol like Raft or Paxos to ensure that all replicas agree on the final state, even in the event of a network partition (The P in CAP theorem).

8. Performance Nuances (The Staff Perspective)

  1. Cache Locality: Accessing a 2D matrix in row-major order (reading [i][j] then [i][j+1]) is significantly faster than column-major order in modern CPUs due to L1/L2 cache pre-fetching. I always structure my loops to align with how the memory is physically laid out.
  2. Autoboxing and Generics: In Java, using List<Integer> instead of int[] can be 3x slower due to the overhead of object headers and constant wrapping. For the most performance-sensitive sections of this algorithm, I advocate for primitive specialized structures.

Want to track your progress?

Sign in to save your progress, track completed lessons, and pick up where you left off.