Count the subarrays whose elements sum to k, where the values may be negative.
Keep a running prefix sum and a map from prefix-sum value to how many times it has occurred; at each index the number of subarrays ending there is the count already recorded for sum minus k. That is O(n) time for O(n) memory, and the map is what lets it tolerate negative numbers where a sliding window cannot.
What the interviewer is scoring
- Does the candidate notice that negative values rule out the sliding window before writing any code
- Whether the map is seeded with the empty prefix, since without it every subarray starting at index 0 is missed
- That the lookup happens before the current prefix sum is inserted, which matters when k is zero
- Whether the trade being made is named as memory for time rather than presented as the only possible approach
- Does the candidate know what the key type has to guarantee for the map to behave
Answer
Reading the constraint first
The presence of negative values is not colour in the problem statement, it is the whole reason a hash map is required. A sliding window works when growing the window monotonically increases the sum and shrinking it decreases it, because that monotonicity is what justifies moving the left boundary forward and never back. Negatives destroy it: extending the window can lower the sum, so a window you rejected might have become valid two elements later, and the window's central assumption is gone.
What survives is the prefix sum. If P(i) is the sum of the first i elements, the sum of the subarray (j, i] is P(i) - P(j). You want that to equal k, so for each i you want the number of earlier j with P(j) = P(i) - k. Counting those in constant time is exactly what a hash map buys you, and it is the whole algorithm.
The solution, and the two lines that decide it
int subarraySum(int[] nums, int k) {
Map<Integer, Integer> counts = new HashMap<>();
counts.put(0, 1); // the empty prefix, before any element
int sum = 0, total = 0;
for (int x : nums) {
sum += x;
total += counts.getOrDefault(sum - k, 0); // look up BEFORE recording this prefix
counts.merge(sum, 1, Integer::sum);
}
return total;
}
Seeding the map with {0: 1} is not a defensive nicety. Without it, a subarray that starts at index 0 has no earlier prefix to subtract from, so every answer anchored at the beginning of the array is silently lost, and the function returns a plausible-looking wrong number that passes small hand-written tests.
The ordering of the two statements inside the loop matters for k = 0. If you record sum first and then look up sum - k, you find the entry you just inserted and count the empty subarray ending at the current index, inflating the total by n. Doing the lookup first means the map contains only strictly earlier prefixes, which is what the derivation says it should contain.
Time is O(n) with one hash operation per element. Space is O(n) in the worst case, when every prefix sum is distinct. That worst case is the trade: you are paying a linear amount of memory to avoid the quadratic scan, and the memory is proportional to input size rather than to the answer.
What the map demands of its keys
Here the keys are boxed Integer values, whose hashCode and equals are already correct, so the caveats stay invisible. They stop being invisible the moment a key is a domain object - a coordinate pair, a normalised string, a composite id.
Three things have to hold. Equal objects must return equal hash codes, or two keys that are equals land in different buckets and the map appears to contain a duplicate. Unequal objects may share a hash code, which is legal and merely costs a chain walk, so equals still has to be exact. And the hash code must not change while the object is a key: mutating a field that participates in hashCode after insertion leaves the entry sitting in a bucket that lookups will never search again, so the value is unreachable but still retained. In Java the cheap correct route is a record, which derives both methods from the components; if you write them by hand, both methods must be derived from the same fields.
When a hash map is the wrong instrument
The default assumption that a hash map is free is worth resisting, and an interviewer who asks the fourth follow-up above is checking whether you can.
- The keys are dense small integers. An
int[]indexed directly beats aHashMap<Integer, Integer>on both memory and cache behaviour, with no boxing and no hashing. - You need order. A hash map has none, and reaching for it and then sorting the entries afterwards often means a sorted structure or a counting sort was the right answer from the start.
- Tail latency matters more than mean. Individual operations are O(1) expected, not worst case, and a resize touches every entry. In a request path with a p99 budget, an amortised bound quoted as if it were a guarantee is a real mistake.
- The keys are attacker-controlled. Colliding keys can be manufactured. Java's
HashMapconverts a long chain to a red-black tree, capping a degenerate bucket at O(log n) rather than O(n), but the mitigation is worth knowing rather than assuming. - n is tiny. For a handful of elements a linear scan over an array wins outright, because hashing and allocation dominate.
The strong version of this answer states the trade in one sentence - linear extra memory buys the removal of a nested loop - and then says where that trade stops being worth making. Candidates who can only argue the first half sound like they learned the technique from an editorial rather than from using it.
Likely follow-ups
- Why does the two-pointer window work for this problem when all values are positive, and where exactly does it break otherwise?
- Return the longest such subarray instead of the count. What changes in the map?
- The keys here are boxed integers. What would you need from a custom key class?
- When would you reject a hash map here and prefer sorting or an array?
Related questions
- How does HashMap work internally, and what happens when it resizes?mediumAlso on hashing3 min
- Find the length of the longest substring without repeating characters.mediumAlso on hashing2 min
- Design the data structure behind a search box's autocomplete. Why a trie, and when would you not use one?mediumAlso on space-time-tradeoff6 min
- For every index, give me the product of all the other elements - and you cannot use division.mediumAlso on prefix-sum3 min
- How would you design a thread-safe component, and why is adding synchronized to every method not a design?hardSame kind of round: concept7 min
- How do you tell whether a value escapes to the heap, and how would you find the allocations that are costing you?hardSame kind of round: concept6 min
- Adding a second LEFT JOIN doubled the revenue figure on a report. Explain what happened and write the correct query.mediumSame kind of round: coding4 min
- How do you decide between BFS and DFS - and how do you recognise a graph problem that nobody described as a graph?mediumSame kind of round: coding4 min