Hi👋SpeedAlgo • Deliberate Practice & Cognitive Ergonomics for Software Engineers

An interactive algorithm mastery and technical interview preparation platform published by Hi👋WebEnterprise. Built for senior and staff software engineers preparing for rigorous coding screens at top tech companies (FAANG/MAMAA).

13 Core Algorithmic Patterns & 225 Practice Problems

  • 1. Two Pointers (16 Paradigms, 43 Problems): Opposite ends, sorted pair sums, palindrome verification, trapping rain water, 3Sum, container with most water — includes the Sliding Window and Fast & Slow Pointers paradigms (contiguous subarray invariants, longest substrings, Floyd's cycle detection, linked list middle).
  • 2. Binary Search (9 Paradigms, 14 Problems): Monotonic predicate partitioning (feasible(x)), boundary search, rotated sorted arrays, square roots, minimum ship capacity, median of two sorted arrays, matrix median on value range.
  • 3. Bit Manipulation (5 Paradigms, 11 Problems): Counting set bits, XOR cancellation, bit reversal, bitmask pairing, maximum XOR with a bitwise trie.
  • 4. Math & Geometry (8 Paradigms, 14 Problems): Sieve of Eratosthenes, integer/roman conversions, modular arithmetic, geometric simulation.
  • 5. Tree/Graph Depth-First Search (15 Paradigms, 24 Problems): Path sums, lowest common ancestor, tree diameter, validating BSTs, tree DP (House Robber III, Binary Tree Cameras).
  • 6. Tree/Graph Breadth-First Search (5 Paradigms, 12 Problems): Level-order traversals, shortest path in unweighted graphs, zig-zag traversals, rotting oranges, word ladders.
  • 7. Graphs (12 Paradigms, 22 Problems): Topological sort, cycle detection, Dijkstra's algorithm, bipartite graph validation, network delay.
  • 8. Backtracking (6 Paradigms, 14 Problems): Subsets, permutations, combinations, constraint satisfaction, pruning, N-Queens, Sudoku solver.
  • 9. Dynamic Programming (13 Paradigms, 28 Problems): Memoization vs tabulation, 0/1 knapsack, unbounded knapsack, coin change, edit distance, longest common subsequence.
  • 10. Heap / Priority Queue (9 Paradigms, 11 Problems): Running medians, top-k elements, interval scheduling, IPO, k-way merges.
  • 11. Advanced Data Structures (7 Paradigms, 17 Problems): Trie (prefix tree), Union-Find (Disjoint Set Union), LRU Cache, LFU Cache, range-sum queries with updates.
  • 12. Stack (5 Paradigms, 9 Problems): Next greater element, largest rectangle in histogram, remove k digits, decode string, min stack.
  • 13. Intervals & Greedy (2 Paradigms, 6 Problems): Merge intervals, interval scheduling, meeting rooms by sweep line, jump game.

4-Stage Deliberate Practice Framework

  1. Stage 1 (Compare & Learn): Read the pattern's mental model, loop invariant and Big-O proof, compare the pattern's template with the problem's solution side by side, and use the step-by-step debugger. The solution can also be shown in C#, Java, TypeScript, C++, Go or Rust for almost every problem.
  2. Stage 2 (Active Recall): Rebuild the solution from memory in a code editor, with an invariant checklist and optional progressive hints, and check it against a judged test suite.
  3. Stage 3 (AI Interview Coach, coming soon): A mock-interview stage still in development; for now, students go straight from Stage 2 to Stage 4.
  4. Stage 4 (Solve on Your Own): Solve a new problem on your own against a 15-minute timer; your code is graded against a test suite. Python and TypeScript run in your browser and C# runs on our server (design problems grade in Python and TypeScript).

Equipped with a spaced-repetition review hub on a fixed 7-step schedule (1 day up to 90 days), a two-pane Studio workspace, and study notes.

Pricing, Access & Commercial Terms

  • Core Curriculum: 100% Free. No credit card required.
  • Coins: New accounts get 40 free coins once, the daily claim adds +20 coins once a day, and inviting a friend earns you +25 coins when they complete their first practice step. These are the only ways to earn coins.
  • AI Coach: Each AI coach question costs 1-5 coins, priced by the server according to its size.
  • BYOK (Bring Your Own Key): AI coaching is free with your own Google Gemini, OpenAI or OpenRouter API key (BYOK).
  • Extras: Coins also unlock extras for 24 hours: a Recall hint level (1 coin), a Solve solution reveal (2), a LeetCode problem card's solutions (1) and a pattern's extra canonical problems (1).
  • Payments & Refunds: Nothing on the site is sold for money: coins can only be earned, never bought. There are no subscriptions or charges, so there is nothing to refund.
  • Platform Operator: Hi👋WebEnterprise. Support & policies at hispeedalgo.com.
Skip to main content
Hi👋SpeedAlgo

Invariant-First Algorithmic Mastery

Arena
238Items
Theory Context•Two Pointers & Sliding Window
EasyLC 303

Range Sum Query - Immutable (LeetCode 303)

An integer array nums is given once, and it never changes. After that, many questions arrive. Each question names two indices, left and right, with left <= right, and asks for the total of the elements from index left to index right, both ends included. That total is the sum below.

nums[left] + nums[left + 1] + ... + nums[right]

Your class is NumArray. Its constructor, NumArray(nums), receives the array. Its method sumRange(left, right) answers one question and returns that total as an integer.

Worked Examples

Example 1
Input:["NumArray", "sumRange", "sumRange", "sumRange"] [[[-2, 0, 3, -5, 2, -1]], [0, 2], [2, 5], [0, 5]]
Output:[null, 1, -1, -3]
-200132-5324-15leftright
Explanation: NumArray([-2, 0, 3, -5, 2, -1]) receives the array and returns null. sumRange(0, 2) is -2 + 0 + 3 = 1. sumRange(2, 5) is 3 + (-5) + 2 + (-1) = -1. sumRange(0, 5) is the whole array: -2 + 0 + 3 + (-5) + 2 + (-1) = -3.

⚖️Formal Constraints & Bounds

  • 1 <= nums.length <= 104

  • -105 <= nums[i] <= 105

  • 0 <= left <= right < nums.length

  • sumRange is called at most 104 times.

Deep-Dive & Conceptual Insights

Core Insight

self.prefix[i] holds the sum of the first i elements, with self.prefix[0] = 0. The first right + 1 elements are the first left elements followed by nums[left..right]. So every range sum is one subtraction of two stored sums.

Real-World Scenario & Production Applications

A dashboard shows totals over many date ranges of the same daily figures, such as sales per day or requests per hour. Past days do not change. So the running totals from the first day can be computed once, and the total of any date range is then one subtraction.

Subproblems & Recurrence Decomposition2 Phases

🧩Subproblem 1: Build the Prefix Array Once

self.prefix gets one slot more than nums. Slot 0 is the sum of no elements, 0, and each next slot adds one element, so self.prefix[i] is the sum of the first i elements. This is one pass, O(N).

Mathematical Recurrence / Code Invariant
self.prefix = [0] * (len(nums) + 1)
for i, num in enumerate(nums):
    self.prefix[i + 1] = self.prefix[i] + num

Step-by-Step Execution Trace Table

The debugger's first preset is Example 1, nums = [-2, 0, 3, -5, 2, -1]. __init__ fills self.prefix one slot at a time:

inumself.prefix[i + 1] = self.prefix[i] + numself.prefix after the step
(start)——[0, 0, 0, 0, 0, 0, 0]
0-20 + (-2) = -2[0, -2, 0, 0, 0, 0, 0]
10-2 + 0 = -2[0, -2, -2, 0, 0, 0, 0]
23-2 + 3 = 1[0, -2, -2, 1, 0, 0, 0]
3-51 + (-5) = -4[0, -2, -2, 1, -4, 0, 0]
42-4 + 2 = -2[0, -2, -2, 1, -4, -2, 0]
5-1-2 + (-1) = -3[0, -2, -2, 1, -4, -2, -3]

Then each call reads two slots: sumRange(0, 2) = self.prefix[3] - self.prefix[0] = 1 - 0 = 1, sumRange(2, 5) = self.prefix[6] - self.prefix[2] = -3 - (-2) = -1, and sumRange(0, 5) = self.prefix[6] - self.prefix[0] = -3 - 0 = -3.

Worked on Paper

The Math, Step by Step

1. The number the code stores

Call P[i]P[i]P[i] the prefix sum at iii: the total of the first iii elements of nums. So P[0]=0P[0] = 0P[0]=0, because it adds no elements. P[1]P[1]P[1] is nums[0], and P[2]P[2]P[2] is nums[0] + nums[1]. An array of nnn elements has n+1n + 1n+1 prefix sums, from P[0]P[0]P[0] to P[n]P[n]P[n].

The code stores P[i]P[i]P[i] in self.prefix[i]. Every question is then answered with one formula:

sum(left…right)=P[right+1]−P[left]\text{sum}(\text{left} \ldots \text{right}) = P[\text{right} + 1] - P[\text{left}]sum(left…right)=P[right+1]−P[left]

2. Computing every prefix sum in one pass

Each prefix sum is the one before it plus one more element:

P[i+1]=P[i]+nums[i]P[i + 1] = P[i] + \text{nums}[i]P[i+1]=P[i]+nums[i]

So the code needs one pass from left to right. It starts with P[0]=0P[0] = 0P[0]=0 and adds one element per step. There are no cases to tell apart. The same line runs for every index.

3. Why one subtraction gives the range total

Split the first right+1\text{right} + 1right+1 elements into two parts. The first part is the first left\text{left}left elements. The second part is nums[left] up to nums[right]. Written as totals:

P[right+1]=P[left]+sum(left…right)P[\text{right} + 1] = P[\text{left}] + \text{sum}(\text{left} \ldots \text{right})P[right+1]=P[left]+sum(left…right)

Move P[left]P[\text{left}]P[left] to the other side, and you get the formula from part 1. The proof only splits one sum into two parts. It never needs the values to be positive, and it never needs PPP to grow. So the formula also holds when nums has negative values, as in the example below.

4. The + 1 and the zero at the start

The two indices of the formula are not symmetric. P[right+1]P[\text{right} + 1]P[right+1] includes nums[right], because it counts right+1\text{right} + 1right+1 elements. P[left]P[\text{left}]P[left] excludes nums[left], because it counts only the left\text{left}left elements before it. If you write self.prefix[right] - self.prefix[left], the result leaves out nums[right].

The extra entry P[0]=0P[0] = 0P[0]=0 handles ranges that start at index 0. For left=0\text{left} = 0left=0 the formula reads P[right+1]−P[0]P[\text{right} + 1] - P[0]P[right+1]−P[0]. That is the total of the first right+1\text{right} + 1right+1 elements minus nothing. Without P[0]P[0]P[0], a range starting at index 0 would need its own if.

5. Example 1 by hand

nums = [-2, 0, 3, -5, 2, -1]. The constructor builds the prefix sums one step at a time:

iiinums[i]P[i+1]=P[i]+nums[i]P[i + 1] = P[i] + \text{nums}[i]P[i+1]=P[i]+nums[i]
startP[0]=0P[0] = 0P[0]=0
0−2-2−2P[1]=0+(−2)=−2P[1] = 0 + (-2) = -2P[1]=0+(−2)=−2
1000P[2]=−2+0=−2P[2] = -2 + 0 = -2P[2]=−2+0=−2
2333P[3]=−2+3=1P[3] = -2 + 3 = 1P[3]=−2+3=1
3−5-5−5P[4]=1+(−5)=−4P[4] = 1 + (-5) = -4P[4]=1+(−5)=−4
4222P[5]=−4+2=−2P[5] = -4 + 2 = -2P[5]=−4+2=−2
5−1-1−1P[6]=−2+(−1)=−3P[6] = -2 + (-1) = -3P[6]=−2+(−1)=−3
00-21-2213-44-25-36P[2]P[6]

The picture shows the seven prefix sums P[0]P[0]P[0] to P[6]P[6]P[6] for Example 1, with the index of each one under its cell. The two marked entries answer the question sumRange(2, 5), where left=2\text{left} = 2left=2 and right+1=6\text{right} + 1 = 6right+1=6. P[6]=−3P[6] = -3P[6]=−3 is the total of all six elements. P[2]=−2P[2] = -2P[2]=−2 is the total of the first two elements, nums[0] and nums[1]. Their difference is the total of nums[2] to nums[5].

The three questions of Example 1 each take one subtraction:

QuestionP[right+1]P[\text{right} + 1]P[right+1]P[left]P[\text{left}]P[left]Answer
sumRange(0, 2)P[3]=1P[3] = 1P[3]=1P[0]=0P[0] = 0P[0]=01−0=11 - 0 = 11−0=1
sumRange(2, 5)P[6]=−3P[6] = -3P[6]=−3P[2]=−2P[2] = -2P[2]=−2−3−(−2)=−1-3 - (-2) = -1−3−(−2)=−1
sumRange(0, 5)P[6]=−3P[6] = -3P[6]=−3P[0]=0P[0] = 0P[0]=0−3−0=−3-3 - 0 = -3−3−0=−3

The answers 111, −1-1−1 and −3-3−3 are the expected output. A direct check of the second one: 3+(−5)+2+(−1)=−13 + (-5) + 2 + (-1) = -13+(−5)+2+(−1)=−1.

6. From the formulas to the code
FormulaIn NumArray
P[0]=0P[0] = 0P[0]=0, and n+1n + 1n+1 entriesself.prefix = [0] * (len(nums) + 1)
P[i+1]=P[i]+nums[i]P[i + 1] = P[i] + \text{nums}[i]P[i+1]=P[i]+nums[i]self.prefix[i + 1] = self.prefix[i] + num
P[right+1]−P[left]P[\text{right} + 1] - P[\text{left}]P[right+1]−P[left]return self.prefix[right + 1] - self.prefix[left]

__init__ runs the pass of part 2 once, in O(n)O(n)O(n) time. After that, sumRange reads two stored values and subtracts them, in O(1)O(1)O(1) time per question.

Code Shape
Archetype Code Shape
Python
1self.prefix = [0] * (len(nums) + 1)
2for i, num in enumerate(nums):
3 self.prefix[i + 1] = <the slot before it, plus num>
4return self.prefix[right + 1] - self.prefix[left]

Conceptual Narrative

🧭 Conceptual Foundation & Pattern Intuition

Adding up nums[left..right] on every call reads up to n elements each time. With 10^4 elements and 10^4 calls, that is up to 10^8 additions. But the array never changes, so the calls repeat the same additions. Prefix Sums does the adding once. self.prefix[i] stores the sum of the first i elements. Any range is then the difference of two stored sums, self.prefix[right + 1] - self.prefix[left].

🛣️ The Analogy: Distance Markers on a Road

A road has a marker every kilometer, and each marker shows the distance from the start of the road. To find the distance from marker 42 to marker 78, you do not measure the road again. You subtract: 78 - 42 = 36 kilometers. self.prefix is the list of markers, and self.prefix[0] = 0 is the start of the road.

🔑 Why It Works

After __init__, self.prefix[i] is the sum of nums[0..i-1] for every i from 0 to n. The first right + 1 elements are the first left elements followed by nums[left..right]. So the two sums differ by exactly the sum of the range. The values may be negative. The formula still holds, because it never needs the totals to increase.

  • Writing self.prefix[right] instead of self.prefix[right + 1]

  • Dropping the leading 0 and reading self.prefix[left - 1]

  • Adding up the range inside sumRange

Senior SWE Reasoning Architecture

4-Phase Thought Process Model

Pattern Recognition Signals

The 10-second spot

The statement says that the array is given once and never changes. Then sumRange is called up to 10^4 times, on up to 10^4 elements. A fixed array plus many questions about the sum of a given range is the signal for Prefix Sums. You pay once to build the sums, and each question becomes one subtraction.

Formulating the Predicate & Invariants

Turning intuition into a boolean rule

Invariant: after __init__, self.prefix[i] is the sum of the first i elements, nums[0..i-1], for every i from 0 to n, and self.prefix[0] = 0. The first right + 1 elements are the first left elements followed by the range, so sumRange returns self.prefix[right + 1] - self.prefix[left].

Silent Failure Traps & Edge Cases

Where confident candidates still lose points

  • Read self.prefix[right + 1], not self.prefix[right]. self.prefix[i] stops just before nums[i], so self.prefix[right] leaves nums[right] out. On [-2, 0, 3, -5, 2, -1], sumRange(0, 2) would return -2 instead of 1.

  • Fill self.prefix[i + 1] from self.prefix[i], the slot just before it, not from self.prefix[i - 1]. At i = 0, self.prefix[i - 1] is self.prefix[-1], the last slot, which is still 0. From i = 1 on, each slot adds num to the slot two places back, so it leaves one element out. On [-2, 0, 3, -5, 2, -1], sumRange(2, 5) then returns -6 instead of -1.

The 60-Second Interview Pitch

Say this out loud before you type a single line

I'd use Prefix Sums. The array never changes and there are many range questions, so I do the adding once. In the constructor I build prefix with n plus 1 slots. prefix at 0 is 0, and each next slot is the one before it plus one element. So prefix at i is the sum of the first i elements. For sumRange of left and right, the first right plus 1 elements are the first left elements followed by my range. So I return prefix at right plus 1, minus prefix at left. The leading 0 lets a range that starts at index 0 use the same formula. The trap is the index shift: prefix at right stops just before nums at right, so I must read prefix at right plus 1. Building takes O(N) time and O(N) space, and each query is one subtraction, O(1).

Big-O Invariant Derivation

Complexity & Mathematical Proof

TIME COMPLEXITY

O(N) to build, O(1) per sumRange

Look at the code. self.prefix = [0] * (len(nums) + 1) writes N + 1 zeros. The for loop runs N times and does one addition each time. So __init__ costs O(N). sumRange reads two slots of self.prefix and subtracts once, whatever the length of the range. So each call costs O(1), and Q calls cost O(Q).

SPACE COMPLEXITY

O(N)

self.prefix holds N + 1 integers, so O(N). sumRange keeps no extra memory. There is no recursion, so the call stack stays the same size.

Formal Recurrence Relation

T(N, Q) = O(N) to build + Q · O(1) for the calls = O(N + Q)

Derivation Progression

Slots

O(N + 1)

[0] * (len(nums) + 1) writes one zero per slot.

Build pass

N · O(1)

self.prefix[i + 1] = self.prefix[i] + num: one addition per element.

One call

O(1)

self.prefix[right + 1] - self.prefix[left]: two reads and one subtraction, for any range length.

Total

O(N + Q)

One pass over the array, then constant work per call.

Variable Definitions

NNN

The number of elements, len(nums)

QQQ

The number of sumRange calls

Memory Architecture & Bounds

🟣 Call Stack

O(1): no recursion

🔵 Auxiliary Heap

O(N): self.prefix, N + 1 integers

🟢 Output Space

O(1): one integer per call

Boundary Best / Worst Cases

Best Case

O(1)O(1)O(1) per call: a one-element range such as sumRange(3, 3) reads two slots

Average Case

O(1)O(1)O(1) per call

Worst Case

O(1)O(1)O(1) per call: the whole array, sumRange(0, N - 1), also reads only two slots

Staff+ Engineering Perspective

Senior SWE Deconstruction & Hardware Caveats

ARCHITECTURAL SIGNALS & INTERVIEW TRIGGERS

Triggers: "many questions arrive" and "sumRange is called at most 104 times", on an array that is given once and never changes. At scale the same shape is read-heavy data: written once, then queried by range many times, such as daily totals in a report.

CONSTRAINTS & BOUNDS

N≤104N \le 10^4N≤104 values of absolute size up to 10510^5105, so every prefix total lies between −109-10^9−109 and 10910^9109 (104×10510^4 \times 10^5104×105). That fits in a 32-bit signed integer, whose largest value is 231−1≈2.1×1092^{31} - 1 \approx 2.1 \times 10^9231−1≈2.1×109. On larger inputs the totals grow with NNN, so production code stores them as 64-bit integers.

FAANG PRODUCTION TRAPS & EDGE CASES

The table is correct only while the data does not change. One changed element changes every prefix total after it, so an update costs O(N) to rebuild. When updates and range questions alternate, a Fenwick tree answers both in O(log N). With floating-point data, each prefix total is rounded, and subtracting two large rounded totals can lose the digits of a small range sum.

Core Algorithmic State Invariants

  1. Negative Values Need No Special Case

The totals in self.prefix can go up and down. For [-2, 0, 3, -5, 2, -1] they are [0, -2, -2, 1, -4, -2, -3]. The formula only uses the fact that the first right + 1 elements are the first left elements plus the range. It never needs the totals to increase, so negative values need no special case.

  1. One Table Answers Every Range

An array of n elements has n * (n + 1) / 2 ranges, and self.prefix answers each one from two of its n + 1 slots. For the 6 elements of Example 1, that is 21 ranges from 7 slots.

Theory Context•Two Pointers & Sliding Window
EasyLC 303

Range Sum Query - Immutable (LeetCode 303)

An integer array nums is given once, and it never changes. After that, many questions arrive. Each question names two indices, left and right, with left <= right, and asks for the total of the elements from index left to index right, both ends included. That total is the sum below.

nums[left] + nums[left + 1] + ... + nums[right]

Your class is NumArray. Its constructor, NumArray(nums), receives the array. Its method sumRange(left, right) answers one question and returns that total as an integer.

Worked Examples

Example 1
Input:["NumArray", "sumRange", "sumRange", "sumRange"] [[[-2, 0, 3, -5, 2, -1]], [0, 2], [2, 5], [0, 5]]
Output:[null, 1, -1, -3]
-200132-5324-15leftright
Explanation: NumArray([-2, 0, 3, -5, 2, -1]) receives the array and returns null. sumRange(0, 2) is -2 + 0 + 3 = 1. sumRange(2, 5) is 3 + (-5) + 2 + (-1) = -1. sumRange(0, 5) is the whole array: -2 + 0 + 3 + (-5) + 2 + (-1) = -3.

⚖️Formal Constraints & Bounds

  • 1 <= nums.length <= 104

  • -105 <= nums[i] <= 105

  • 0 <= left <= right < nums.length

  • sumRange is called at most 104 times.

Deep-Dive & Conceptual Insights

Core Insight

self.prefix[i] holds the sum of the first i elements, with self.prefix[0] = 0. The first right + 1 elements are the first left elements followed by nums[left..right]. So every range sum is one subtraction of two stored sums.

Real-World Scenario & Production Applications

A dashboard shows totals over many date ranges of the same daily figures, such as sales per day or requests per hour. Past days do not change. So the running totals from the first day can be computed once, and the total of any date range is then one subtraction.

Subproblems & Recurrence Decomposition2 Phases

🧩Subproblem 1: Build the Prefix Array Once

self.prefix gets one slot more than nums. Slot 0 is the sum of no elements, 0, and each next slot adds one element, so self.prefix[i] is the sum of the first i elements. This is one pass, O(N).

Mathematical Recurrence / Code Invariant
self.prefix = [0] * (len(nums) + 1)
for i, num in enumerate(nums):
    self.prefix[i + 1] = self.prefix[i] + num

Step-by-Step Execution Trace Table

The debugger's first preset is Example 1, nums = [-2, 0, 3, -5, 2, -1]. __init__ fills self.prefix one slot at a time:

inumself.prefix[i + 1] = self.prefix[i] + numself.prefix after the step
(start)——[0, 0, 0, 0, 0, 0, 0]
0-20 + (-2) = -2[0, -2, 0, 0, 0, 0, 0]
10-2 + 0 = -2[0, -2, -2, 0, 0, 0, 0]
23-2 + 3 = 1[0, -2, -2, 1, 0, 0, 0]
3-51 + (-5) = -4[0, -2, -2, 1, -4, 0, 0]
42-4 + 2 = -2[0, -2, -2, 1, -4, -2, 0]
5-1-2 + (-1) = -3[0, -2, -2, 1, -4, -2, -3]

Then each call reads two slots: sumRange(0, 2) = self.prefix[3] - self.prefix[0] = 1 - 0 = 1, sumRange(2, 5) = self.prefix[6] - self.prefix[2] = -3 - (-2) = -1, and sumRange(0, 5) = self.prefix[6] - self.prefix[0] = -3 - 0 = -3.

Worked on Paper

The Math, Step by Step

1. The number the code stores

Call P[i]P[i]P[i] the prefix sum at iii: the total of the first iii elements of nums. So P[0]=0P[0] = 0P[0]=0, because it adds no elements. P[1]P[1]P[1] is nums[0], and P[2]P[2]P[2] is nums[0] + nums[1]. An array of nnn elements has n+1n + 1n+1 prefix sums, from P[0]P[0]P[0] to P[n]P[n]P[n].

The code stores P[i]P[i]P[i] in self.prefix[i]. Every question is then answered with one formula:

sum(left…right)=P[right+1]−P[left]\text{sum}(\text{left} \ldots \text{right}) = P[\text{right} + 1] - P[\text{left}]sum(left…right)=P[right+1]−P[left]

2. Computing every prefix sum in one pass

Each prefix sum is the one before it plus one more element:

P[i+1]=P[i]+nums[i]P[i + 1] = P[i] + \text{nums}[i]P[i+1]=P[i]+nums[i]

So the code needs one pass from left to right. It starts with P[0]=0P[0] = 0P[0]=0 and adds one element per step. There are no cases to tell apart. The same line runs for every index.

3. Why one subtraction gives the range total

Split the first right+1\text{right} + 1right+1 elements into two parts. The first part is the first left\text{left}left elements. The second part is nums[left] up to nums[right]. Written as totals:

P[right+1]=P[left]+sum(left…right)P[\text{right} + 1] = P[\text{left}] + \text{sum}(\text{left} \ldots \text{right})P[right+1]=P[left]+sum(left…right)

Move P[left]P[\text{left}]P[left] to the other side, and you get the formula from part 1. The proof only splits one sum into two parts. It never needs the values to be positive, and it never needs PPP to grow. So the formula also holds when nums has negative values, as in the example below.

4. The + 1 and the zero at the start

The two indices of the formula are not symmetric. P[right+1]P[\text{right} + 1]P[right+1] includes nums[right], because it counts right+1\text{right} + 1right+1 elements. P[left]P[\text{left}]P[left] excludes nums[left], because it counts only the left\text{left}left elements before it. If you write self.prefix[right] - self.prefix[left], the result leaves out nums[right].

The extra entry P[0]=0P[0] = 0P[0]=0 handles ranges that start at index 0. For left=0\text{left} = 0left=0 the formula reads P[right+1]−P[0]P[\text{right} + 1] - P[0]P[right+1]−P[0]. That is the total of the first right+1\text{right} + 1right+1 elements minus nothing. Without P[0]P[0]P[0], a range starting at index 0 would need its own if.

5. Example 1 by hand

nums = [-2, 0, 3, -5, 2, -1]. The constructor builds the prefix sums one step at a time:

iiinums[i]P[i+1]=P[i]+nums[i]P[i + 1] = P[i] + \text{nums}[i]P[i+1]=P[i]+nums[i]
startP[0]=0P[0] = 0P[0]=0
0−2-2−2P[1]=0+(−2)=−2P[1] = 0 + (-2) = -2P[1]=0+(−2)=−2
1000P[2]=−2+0=−2P[2] = -2 + 0 = -2P[2]=−2+0=−2
2333P[3]=−2+3=1P[3] = -2 + 3 = 1P[3]=−2+3=1
3−5-5−5P[4]=1+(−5)=−4P[4] = 1 + (-5) = -4P[4]=1+(−5)=−4
4222P[5]=−4+2=−2P[5] = -4 + 2 = -2P[5]=−4+2=−2
5−1-1−1P[6]=−2+(−1)=−3P[6] = -2 + (-1) = -3P[6]=−2+(−1)=−3
00-21-2213-44-25-36P[2]P[6]

The picture shows the seven prefix sums P[0]P[0]P[0] to P[6]P[6]P[6] for Example 1, with the index of each one under its cell. The two marked entries answer the question sumRange(2, 5), where left=2\text{left} = 2left=2 and right+1=6\text{right} + 1 = 6right+1=6. P[6]=−3P[6] = -3P[6]=−3 is the total of all six elements. P[2]=−2P[2] = -2P[2]=−2 is the total of the first two elements, nums[0] and nums[1]. Their difference is the total of nums[2] to nums[5].

The three questions of Example 1 each take one subtraction:

QuestionP[right+1]P[\text{right} + 1]P[right+1]P[left]P[\text{left}]P[left]Answer
sumRange(0, 2)P[3]=1P[3] = 1P[3]=1P[0]=0P[0] = 0P[0]=01−0=11 - 0 = 11−0=1
sumRange(2, 5)P[6]=−3P[6] = -3P[6]=−3P[2]=−2P[2] = -2P[2]=−2−3−(−2)=−1-3 - (-2) = -1−3−(−2)=−1
sumRange(0, 5)P[6]=−3P[6] = -3P[6]=−3P[0]=0P[0] = 0P[0]=0−3−0=−3-3 - 0 = -3−3−0=−3

The answers 111, −1-1−1 and −3-3−3 are the expected output. A direct check of the second one: 3+(−5)+2+(−1)=−13 + (-5) + 2 + (-1) = -13+(−5)+2+(−1)=−1.

6. From the formulas to the code
FormulaIn NumArray
P[0]=0P[0] = 0P[0]=0, and n+1n + 1n+1 entriesself.prefix = [0] * (len(nums) + 1)
P[i+1]=P[i]+nums[i]P[i + 1] = P[i] + \text{nums}[i]P[i+1]=P[i]+nums[i]self.prefix[i + 1] = self.prefix[i] + num
P[right+1]−P[left]P[\text{right} + 1] - P[\text{left}]P[right+1]−P[left]return self.prefix[right + 1] - self.prefix[left]

__init__ runs the pass of part 2 once, in O(n)O(n)O(n) time. After that, sumRange reads two stored values and subtracts them, in O(1)O(1)O(1) time per question.

Code Shape
Archetype Code Shape
Python
1self.prefix = [0] * (len(nums) + 1)
2for i, num in enumerate(nums):
3 self.prefix[i + 1] = <the slot before it, plus num>
4return self.prefix[right + 1] - self.prefix[left]

Conceptual Narrative

🧭 Conceptual Foundation & Pattern Intuition

Adding up nums[left..right] on every call reads up to n elements each time. With 10^4 elements and 10^4 calls, that is up to 10^8 additions. But the array never changes, so the calls repeat the same additions. Prefix Sums does the adding once. self.prefix[i] stores the sum of the first i elements. Any range is then the difference of two stored sums, self.prefix[right + 1] - self.prefix[left].

🛣️ The Analogy: Distance Markers on a Road

A road has a marker every kilometer, and each marker shows the distance from the start of the road. To find the distance from marker 42 to marker 78, you do not measure the road again. You subtract: 78 - 42 = 36 kilometers. self.prefix is the list of markers, and self.prefix[0] = 0 is the start of the road.

🔑 Why It Works

After __init__, self.prefix[i] is the sum of nums[0..i-1] for every i from 0 to n. The first right + 1 elements are the first left elements followed by nums[left..right]. So the two sums differ by exactly the sum of the range. The values may be negative. The formula still holds, because it never needs the totals to increase.

  • Writing self.prefix[right] instead of self.prefix[right + 1]

  • Dropping the leading 0 and reading self.prefix[left - 1]

  • Adding up the range inside sumRange

Senior SWE Reasoning Architecture

4-Phase Thought Process Model

Pattern Recognition Signals

The 10-second spot

The statement says that the array is given once and never changes. Then sumRange is called up to 10^4 times, on up to 10^4 elements. A fixed array plus many questions about the sum of a given range is the signal for Prefix Sums. You pay once to build the sums, and each question becomes one subtraction.

Formulating the Predicate & Invariants

Turning intuition into a boolean rule

Invariant: after __init__, self.prefix[i] is the sum of the first i elements, nums[0..i-1], for every i from 0 to n, and self.prefix[0] = 0. The first right + 1 elements are the first left elements followed by the range, so sumRange returns self.prefix[right + 1] - self.prefix[left].

Silent Failure Traps & Edge Cases

Where confident candidates still lose points

  • Read self.prefix[right + 1], not self.prefix[right]. self.prefix[i] stops just before nums[i], so self.prefix[right] leaves nums[right] out. On [-2, 0, 3, -5, 2, -1], sumRange(0, 2) would return -2 instead of 1.

  • Fill self.prefix[i + 1] from self.prefix[i], the slot just before it, not from self.prefix[i - 1]. At i = 0, self.prefix[i - 1] is self.prefix[-1], the last slot, which is still 0. From i = 1 on, each slot adds num to the slot two places back, so it leaves one element out. On [-2, 0, 3, -5, 2, -1], sumRange(2, 5) then returns -6 instead of -1.

The 60-Second Interview Pitch

Say this out loud before you type a single line

I'd use Prefix Sums. The array never changes and there are many range questions, so I do the adding once. In the constructor I build prefix with n plus 1 slots. prefix at 0 is 0, and each next slot is the one before it plus one element. So prefix at i is the sum of the first i elements. For sumRange of left and right, the first right plus 1 elements are the first left elements followed by my range. So I return prefix at right plus 1, minus prefix at left. The leading 0 lets a range that starts at index 0 use the same formula. The trap is the index shift: prefix at right stops just before nums at right, so I must read prefix at right plus 1. Building takes O(N) time and O(N) space, and each query is one subtraction, O(1).

Big-O Invariant Derivation

Complexity & Mathematical Proof

TIME COMPLEXITY

O(N) to build, O(1) per sumRange

Look at the code. self.prefix = [0] * (len(nums) + 1) writes N + 1 zeros. The for loop runs N times and does one addition each time. So __init__ costs O(N). sumRange reads two slots of self.prefix and subtracts once, whatever the length of the range. So each call costs O(1), and Q calls cost O(Q).

SPACE COMPLEXITY

O(N)

self.prefix holds N + 1 integers, so O(N). sumRange keeps no extra memory. There is no recursion, so the call stack stays the same size.

Formal Recurrence Relation

T(N, Q) = O(N) to build + Q · O(1) for the calls = O(N + Q)

Derivation Progression

Slots

O(N + 1)

[0] * (len(nums) + 1) writes one zero per slot.

Build pass

N · O(1)

self.prefix[i + 1] = self.prefix[i] + num: one addition per element.

One call

O(1)

self.prefix[right + 1] - self.prefix[left]: two reads and one subtraction, for any range length.

Total

O(N + Q)

One pass over the array, then constant work per call.

Variable Definitions

NNN

The number of elements, len(nums)

QQQ

The number of sumRange calls

Memory Architecture & Bounds

🟣 Call Stack

O(1): no recursion

🔵 Auxiliary Heap

O(N): self.prefix, N + 1 integers

🟢 Output Space

O(1): one integer per call

Boundary Best / Worst Cases

Best Case

O(1)O(1)O(1) per call: a one-element range such as sumRange(3, 3) reads two slots

Average Case

O(1)O(1)O(1) per call

Worst Case

O(1)O(1)O(1) per call: the whole array, sumRange(0, N - 1), also reads only two slots

Staff+ Engineering Perspective

Senior SWE Deconstruction & Hardware Caveats

ARCHITECTURAL SIGNALS & INTERVIEW TRIGGERS

Triggers: "many questions arrive" and "sumRange is called at most 104 times", on an array that is given once and never changes. At scale the same shape is read-heavy data: written once, then queried by range many times, such as daily totals in a report.

CONSTRAINTS & BOUNDS

N≤104N \le 10^4N≤104 values of absolute size up to 10510^5105, so every prefix total lies between −109-10^9−109 and 10910^9109 (104×10510^4 \times 10^5104×105). That fits in a 32-bit signed integer, whose largest value is 231−1≈2.1×1092^{31} - 1 \approx 2.1 \times 10^9231−1≈2.1×109. On larger inputs the totals grow with NNN, so production code stores them as 64-bit integers.

FAANG PRODUCTION TRAPS & EDGE CASES

The table is correct only while the data does not change. One changed element changes every prefix total after it, so an update costs O(N) to rebuild. When updates and range questions alternate, a Fenwick tree answers both in O(log N). With floating-point data, each prefix total is rounded, and subtracting two large rounded totals can lose the digits of a small range sum.

Core Algorithmic State Invariants

  1. Negative Values Need No Special Case

The totals in self.prefix can go up and down. For [-2, 0, 3, -5, 2, -1] they are [0, -2, -2, 1, -4, -2, -3]. The formula only uses the fact that the first right + 1 elements are the first left elements plus the range. It never needs the totals to increase, so negative values need no special case.

  1. One Table Answers Every Range

An array of n elements has n * (n + 1) / 2 ranges, and self.prefix answers each one from two of its n + 1 slots. For the 6 elements of Example 1, that is 21 ranges from 7 slots.

© 2026 Hi👋WebEnterprise. All rights reserved.
Sitemap•llms.txt•