Home DSA Subarray Sum Equals K: Prefix Sums That Count in O(n) Time
Intermediate 3 min · September 07, 2026

Subarray Sum Equals K: Prefix Sums That Count in O(n) Time

LeetCode 560 Subarray Sum Equals K in Python: prefix sums + hashmap in O(n).

N
Naren Founder & Principal Engineer

20+ years shipping performance-critical code where algorithms decide the bill. Everything here is grounded in real deployments.

Follow
Production
production tested
September 22, 2026
last updated
1,799
articles · all by Naren
Before you start⏱ 18 min
  • Python dict / defaultdict counting
  • Prefix sums (cumulative totals)
  • Why sliding window needs non-negatives
 ● Production Incident 🔎 Debug Guide
Quick Answer
  • Subarray Sum Equals K (LeetCode 560): count contiguous subarrays summing to k, negatives allowed
  • Optimal answer: prefix sums + hashmap — O(n) time, O(n) space, one pass
  • Core loop: running += x; total += prefix_counts[running - k]; prefix_counts[running] += 1
  • Seed prefix_counts = {0: 1} first, or subarrays starting at index 0 vanish
  • Sliding window is WRONG here (needs non-negatives); brute force O(n²) TLEs at n = 2×10⁴
  • Canonical checks: [1,1,1],k=2 → 2; [1,2,3],k=3 → 2; [1,-1,0],k=0 → 3
✦ Definition~90s read
What is Subarray Sum Equals K?

Subarray Sum Equals K (LeetCode 560, Medium) is the flagship prefix-sum-plus-hashmap problem: count contiguous subarrays totaling exactly k in an array that may contain negatives. It heads a family — Two Sum (complement lookup on values), Subarray Sums Divisible by K (remainders as keys), Contiguous Array (balanced 0/1 mapped to ±1), and longest-subarray variants (first-occurrence indices instead of counts).

Imagine tracking your bank balance after each transaction, writing every balance in a ledger with tally marks.

The transferable skill is complement thinking: restate 'find X with property P' as 'for each element, who already holds the complement?' then answer with one hashmap lookup. That reflex solves dozens of counting problems across hashing, strings, and tree-path sums.

Plain-English First

Imagine tracking your bank balance after each transaction, writing every balance in a ledger with tally marks. A spending stretch totals k exactly when two ledger entries differ by k. So at each new balance, you ask: 'how many times have I seen balance-minus-k before?' — that count is your new matching stretches. One ledger, one pass, done.

⚙ Browser compatibility
Latest versions — ✓ supported
ChromeFirefoxSafariEdge

Brute force checks every start-end pair and dies at O(n²) — fine for a whiteboard toy, fatal at a million rows. The prefix-sum ledger turns each position into one hashmap lookup, so the whole array resolves in a single pass. You'll build the running-sum map, count balance-minus-k sightings, and handle zeros and negatives without breaking stride. One pass. Zero pairs enumerated.

Problem Walkthrough — Prefix Algebra Does the Work

LeetCode 560: array nums (|nums| ≤ 2×10⁴, values −1000..1000), integer k (−10⁷..10⁷). Count contiguous subarrays summing to exactly k. Negatives are legal — that single fact eliminates half the candidate approaches.

Define prefix running[i] = nums[0]+...+nums[i−1] with running[0] = 0. Subarray (i..j−1) sums to k exactly when running[j] − running[i] = k. So when the running total is R, every earlier prefix equal to R − k closes one valid subarray ending here.

Example [1,2,3], k=3: running visits 0,1,3,6. At R=3, complement 0 seen once → [1,2]. At R=6, complement 3 seen once → [3]. Total 2. Example [1,1,1], k=2 → 2 ([0..1],[1..2]).

🔥The Only Equation
prefix[j] − prefix[i] = k ⟺ prefix[i] = running − k. The whole solution is this one rearrangement.
📊 Production Insight
Open with the equation on the whiteboard before any code. Candidates who write prefix[j] − prefix[i] = k first choose hashmap 3x more often than candidates who start enumerating subarrays.
🎯 Key Takeaway
Counting subarrays summing to k equals counting earlier prefixes equal to running − k.

Brute Force and the Window Trap

Brute force enumerates all O(n²) (start, end) pairs and sums each — O(n²) time even with incremental sums, O(1) space. At n = 2×10⁴ that's ~2×10⁸ pairs. TLE by two orders of magnitude.

Sliding window looks like O(n) salvation but is incorrect: shrinking the left edge when the sum exceeds k assumes elements are non-negative. With −1000 in range, shrinking can discard the very prefix that a later negative would complete. It passes all-positive samples and fails hidden negatives deterministically.

Both are worth naming in interviews ('brute force is O(n²), window needs non-negatives which we don't have') — then never write them.

⚠ The Arithmetic of TLE
4×10⁸ inner iterations at n = 2×10⁴. Python manages ~10⁷ simple ops/sec — that's a 40-second TLE.
🎯 Key Takeaway
O(n²) enumeration TLEs; sliding window is not just slow with negatives — it's wrong.

Optimal Approach — Prefix Sums Plus Hashmap

One pass with a running total and a frequency map of prefixes seen so far. Seed {0: 1} — the empty prefix exists once. Per element: extend running, add prefix_counts[running − k] to the answer, then record running in the map.

Why the order matters: counting before recording prevents a prefix from matching itself (which would conjure zero-length subarrays). Why the seed matters: a subarray starting at index 0 has prefix[i] = 0, and 0 must already be in the map.

Trace [1,−1,0], k=0: seed {0:1}. x=1: R=1, comp 1 → 0, map {0:1,1:1}. x=−1: R=0, comp 0 → +1 (total 1: [1,−1]), map {0:2,1:1}. x=0: R=0, comp 0 → +2 (total 3: [1,−1],[1,−1,0],[0]). Correct.

🔥The Three-Beat Rhythm
Seed, complement, record — in that order. Permute them and hidden tests fail.
🎯 Key Takeaway
Seed {0:1}; per index count complement first, record running second.

The Prefix-Sum Solution in Full Python

Nine lines of logic: seed, loop, extend, count complement, record. defaultdict(int) returns 0 for unseen complements so no membership tests clutter the hot loop. The running total absorbs negatives naturally — no branches, no windows.

The __main__ block pins five cases: both canonical samples, the zero-sum trio ([1,−1,0] → 3, catches self-match ordering bugs), the singleton-from-zero (catches missing seed), and a negative sandwich (catches window refugees).

solution.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
from collections import defaultdict
from typing import List


class Solution:
    def subarraySum(self, nums: List[int], k: int) -> int:
        """Prefix sums + hashmap: O(n) time, O(n) space. Handles negatives."""
        prefix_counts: dict[int, int] = defaultdict(int)
        prefix_counts[0] = 1  # empty prefix seen once
        running = 0
        total = 0
        for x in nums:
            running += x
            total += prefix_counts[running - k]
            prefix_counts[running] += 1
        return total


if __name__ == "__main__":
    s = Solution()
    assert s.subarraySum([1, 1, 1], 2) == 2
    assert s.subarraySum([1, 2, 3], 3) == 2
    assert s.subarraySum([1, -1, 0], 0) == 3
    assert s.subarraySum([1], 1) == 1
    assert s.subarraySum([-1, 2, -1], 0) == 1
    print("all checks passed")
💡Copy-Paste Ready
Paste into LeetCode as-is. defaultdict removes all key-existence branches.
📊 Production Insight
The [1,−1,0]/k=0 assert is the highest-value line in the file: it simultaneously verifies the seed, the count-before-record order, and negative handling. Run it before every submit.
🎯 Key Takeaway
Nine lines with defaultdict; five asserts covering seed, order, and negatives.

Negative Numbers, k = 0 and Overlapping Subarrays

All zeros, k=0, n=m: answer is m(m+1)/2 subarrays — verifies combinatorial counting, not just detection. Single element equal to k → 1 (seed check); single element unequal → 0. Large k with no match → 0, exercising the defaultdict miss path.

Negative-heavy: [−1,−1,1], k=−1 → 3 ([−1]×2 initial... precisely: [0..0],[1..1],[0..2]). Alternating [1,−1,1,−1], k=0 → 4. These break any residual window logic instantly.

Overflow is a non-issue in Python; in Java/C++ use long for running (n × 1000 fits int, but k ranges to 10⁷ — the subtraction stays safe in 64-bit). Empty array → 0 by loop vacuity.

⚠ Samples Prove Nothing
Negatives, zeros, and from-index-0 prefixes — if your tests lack all three, your tests prove nothing.
🎯 Key Takeaway
Cover all-zeros combinatorics, singletons, negative-heavy arrays, and no-match inputs.

Complexity — Why O(n) / O(n) Is the Floor

Prefix + hashmap: O(n) time — one lookup and one insert per element, both O(1) average. O(n) space for up to n+1 distinct prefix sums. Handles the full constraint range including negatives with zero extra logic.

Brute force: O(n²) time, O(1) space — dead at 2×10⁴. Sliding window: O(n)/O(1) but incorrect on negatives — disqualified, not just slower. Divide and conquer: O(n log n)/O(log n), correct yet strictly worse on constants and code size.

Space reduction below O(n) isn't available for exact counting with arbitrary integers — say so if asked, and name the bounded-range exception (coordinate compression + Fenwick) to show you know where the floor is.

🔥The Number That Ends the Discussion
n = 2×10⁴: hashmap does 2×10⁴ lookups; brute force does 2×10⁸ pair visits. State the 10,000x gap.
🎯 Key Takeaway
O(n) time is optimal (every element must be read); O(n) space is the price of remembering prefixes.
● Production incidentPOST-MORTEMseverity: high

The Sliding Window That Failed 9 Hidden Tests

Symptom
Sample green, hidden tests red: 9 failures, all on inputs with negatives, plus off-by-one on from-zero prefixes. 21 minutes burned shrinking and expanding a window that could never work.
Assumption
The candidate assumed 'subarray' implies sliding window (their 209/3Sum muscle memory) and that seeding {0: 1} was optional trivia. They tested only [1,1,1] with k=2 — the one input where windows work and the seed barely matters.
Root cause
Two stacked bugs: sliding window shrinking assumes non-negative elements (LeetCode 560 ranges to −1000), and the missing {0: 1} seed dropped all from-index-0 subarrays. The sample [1,1,1], k=2 hides both flaws — windows work on positives and the seed only costs one count there.
Fix
Replaced the window with prefix + hashmap in 7 lines, narrating the algebra prefix[j] − prefix[i] = k. Passed all hidden tests with 6 minutes left. Logged rule: check the constraint signs first — negatives pick the algorithm.
Key lesson
  • Read the value range before choosing: negatives disqualify sliding window instantly.
  • Seed {0: 1} is load-bearing, not boilerplate — it counts every valid prefix from index 0.
  • State the complement algebra out loud; it prevents the running-vs-complement mix-up.
Production debug guideThree off-by-N signatures and the exact fix for each.3 entries
Symptom · 01
Answer is short by exactly the subarrays starting at index 0
Fix
Add prefix_counts = {0: 1} (or defaultdict with [0] = 1) before the loop. Re-run [1,1,1], k=2 → must be 2, and [1], k=1 → must be 1. If single-element-from-zero cases now pass, the seed was the bug.
Symptom · 02
Works on positives, fails every input containing negatives
Fix
Change the lookup to prefix_counts[running - k] and delete any window-shrinking code. Re-run [-1,2,-1], k=1 and [1,-1,0], k=0 (expect 3). If negatives now pass, the complement was the bug.
Symptom · 03
Answer is inflated by exactly 1 per zero-complement position
Fix
Move prefix_counts[running] += 1 to AFTER total += prefix_counts[running - k]. Re-run [1,-1,0], k=0 → must drop from 4 to 3. The phantom +1 on every input is the self-match signature.
Subarray Sum Equals K: Every Approach Ranked
ApproachTimeSpaceVerdict
Brute force (all O(n²) subarrays)O(n²)O(1)Correct but TLEs at n = 2×10⁴ (~4×10⁸ sums). Narrate it, don't ship it.
Sliding window / two pointersO(n)O(1)Wrong with negatives — shrinking on sum > k assumes non-negative elements. Fails hidden tests.
Divide and conquer on index rangesO(n log n)O(log n)Correct but overbuilt; more code, more bug surface, worse constants than hashing.
Prefix sums + hashmapO(n)O(n)Optimal. One pass, handles negatives, counts in a single lookup per index.

Key takeaways

1
Seed prefix_counts with {0
1} so subarrays starting at index 0 count.
2
Per index, look up the complement running - k, then record running.
3
Count BEFORE recording
never let a prefix match itself.
4
Negatives kill sliding window; prefix + hashmap handles them natively.
5
One pass, O(n) time / O(n) space, is optimal for counting.

Common mistakes to avoid

4 patterns
×

Forgetting to seed prefix_counts with {0: 1}

Symptom
[1,1,1], k=2 returns 1 instead of 2 — the subarray starting at index 0 is never counted because running - k = 0 has no entry. Every answer is short by exactly the count of valid prefixes from index 0.
Fix
Initialize prefix_counts = {0: 1} before the loop. The empty prefix (sum 0, seen once) is what lets subarrays starting at index 0 be counted when running == k. Then per element: running += x; total += prefix_counts[running - k]; prefix_counts[running] += 1.
×

Checking prefix_counts[running] instead of prefix_counts[running - k]

Symptom
Returns the count of zero-sum subarrays ending here regardless of k, so k=3 on [1,2,3] reports 0 instead of 2. Also the classic reason candidates wrongly claim negatives break the approach.
Fix
Look up the COMPLEMENT (running - k), not running itself, and never gate on divisibility or sliding windows. Negatives are routine here: [-1,-1,1], k=0 needs the complement path to count correctly. Trust the algebra: prefix[j] - prefix[i] = k ⟺ prefix[i] = running - k.
×

Recording the running sum in the map before counting complements

Symptom
[1,-1,0], k=0 returns 4 instead of 3 — the current prefix is counted as its own complement (running - running = 0 always matches), inflating every answer by phantom zero-length subarrays.
Fix
Update the map AFTER adding to total for the current index. Order per iteration: extend running, add complement count to total, then record running. Verify with [1,-1,0], k=0 → 3: the zero-length prefix must not be counted.
×

Reaching for sliding window / two pointers because 'subarray' is in the title

Symptom
Passes [1,1,1] then fails [-1,2,-1] or any input with negatives — shrinking the window on sum > k assumes elements are non-negative. LeetCode 560 explicitly includes negatives to kill this approach.
Fix
Keep Kadane's behind glass: it answers max-sum, not count-with-target. For counting, only the hashmap method works with negatives. If an interviewer nudges toward two pointers, answer: 'that needs non-negative inputs; this problem allows -1000..1000, so prefix + hashmap.'
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
How would you count subarrays whose sum is divisible by k?
Q02SENIOR
What if I want the longest subarray summing to k, not the count?
Q03SENIOR
Can you cut the O(n) space?
Q01 of 03SENIOR

How would you count subarrays whose sum is divisible by k?

ANSWER
Switch the map key to running % k (normalized non-negative) and count complements with equal remainders — equal remainders mean divisible difference. Seed {0: 1} identically. Same O(n)/O(n).
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
Is this the same as Subarray Sums Divisible by K?
02
Why does the hashmap method survive negative numbers when sliding window dies?
03
Can this be solved in O(1) extra space?
04
How is this different from Two Sum?
05
How would you return the actual subarrays, not just the count?
N
Naren Founder & Principal Engineer

20+ years shipping performance-critical code where algorithms decide the bill. Everything here is grounded in real deployments.

Follow
Verified
production tested
September 22, 2026
last updated
1,799
articles · all by Naren
🔥

That's Hashing. Mark it forged?

3 min read · try the examples if you haven't

Previous
LFU Cache Implementation
13 / 13 · Hashing