Arrays, Strings, and Hashing Questions
Manipulating arrays and strings using the standard toolkit for entry-level coding-interview problems: two-pointer and sliding-window techniques, in-place modification (reversal, rotation, partitioning, deduplication), prefix sums, and hash-map or hash-set based techniques used to solve array or string problems in optimal time (frequency counting, lookup-based pairing such as two-sum, duplicate detection, grouping by a computed key such as anagram grouping). Hashing appears in this topic only as an applied technique for solving an array or string problem faster: how hash tables work internally (hash functions, collision resolution, load factor, resizing) and hash-based structures that are not array or string shaped (Bloom filters, HyperLogLog) belong to the separate hashing and hash tables topic, not this one. Covers the most frequent entry-level coding-interview problem shapes and the trade-offs between time, space, and readability. The default warm-up surface for any coding interview.
You receive a log line in this format (single line):
2025-12-06T12:00:00Z service=auth pid=1234 level=ERROR msg='Failed login for user bob: invalid password'
Write a Python parser that extracts timestamp, service, pid (int), level, and msg into a dict. Handle missing or quoted messages safely. Discuss performance considerations when parsing millions of lines per hour.
Sample Answer
Direct answer
Parse the line as a leading timestamp token followed by a run of key=value tokens, where a value is either a bare token or a quoted string that can itself contain spaces or an escaped quote. Scan it manually, character by character, rather than with one big anchored regular expression: a single anchored pattern matches the whole line or fails the whole line, and "handle missing fields safely" specifically means a missing field should leave that one key unset, not blow up the entire parse.
Structured elaboration
Tokenizing approach. Read the leading whitespace-delimited token as the timestamp (an ISO 8601 style stamp, 2025-12-06T12:00:00Z, though the parser does not need to validate that format, only extract it). Then repeatedly: skip whitespace, read a key up to =, and read the value either as a bare token (up to the next space) or, if the next character is a quote, as everything up to the matching unescaped quote, unescaping any \' along the way.
Safe field handling. Every expected key (timestamp, service, pid, level, msg) starts at None in the result dict. A key that never appears in the line simply stays None; a malformed pid (not all digits) is caught with a try/except around the int() conversion and also left as None, instead of raising and losing the whole line. That is the concrete meaning of "handle missing or quoted messages safely" here: partial, defensive results instead of an exception.
Performance at millions of lines per hour. The scan itself is a single O(n) pass per line, O(1) extra space beyond the output dict, no backtracking. The throughput number matters for scale: 1,000,000 / 3600 ≈ 277.8 lines per second, a modest rate for a single-threaded O(n)-per-line parse. The parsing technique's job at that rate is to not add avoidable overhead: avoid rebuilding the same intermediate list-and-rejoin structure that line.split() plus manual reassembly would need, and avoid a regex whose quantifiers could backtrack badly on adversarial input (many repeated escaped quotes inside msg). Everything past that (batching, buffering, multiple worker processes) is an ingestion-pipeline design question, a different skill from the array/string parsing technique this question is actually testing.
Worked example
def parse_log_line(line: str) -> dict:
result = {"timestamp": None, "service": None, "pid": None, "level": None, "msg": None}
n = len(line)
i = 0
# 1. timestamp is the leading whitespace-delimited token
while i < n and line[i] == ' ':
i += 1
start = i
while i < n and line[i] != ' ':
i += 1
if i > start:
result["timestamp"] = line[start:i]
# 2. remaining key=value tokens, respecting quoted values
while i < n:
while i < n and line[i] == ' ':
i += 1
if i >= n:
break
key_start = i
while i < n and line[i] != '=' and line[i] != ' ':
i += 1
if i >= n or line[i] != '=':
while i < n and line[i] != ' ':
i += 1
continue
key = line[key_start:i]
i += 1 # skip '='
if i < n and line[i] in ("'", '"'):
quote = line[i]
i += 1
val_start = i
while i < n:
if line[i] == '\\' and i + 1 < n and line[i + 1] == quote:
i += 2
continue
if line[i] == quote:
break
i += 1
raw_val = line[val_start:i]
value = raw_val.replace('\\' + quote, quote)
if i < n:
i += 1 # skip closing quote
else:
val_start = i
while i < n and line[i] != ' ':
i += 1
value = line[val_start:i]
if key in result:
result[key] = value
if result["pid"] is not None:
try:
result["pid"] = int(result["pid"])
except ValueError:
result["pid"] = None
return result
test_lines = [
"2025-12-06T12:00:00Z service=auth pid=1234 level=ERROR msg='Failed login for user bob: invalid password'",
"2025-12-06T12:00:01Z service=auth level=INFO msg='pid missing on this line'",
"2025-12-06T12:00:02Z service=auth pid=5678 level=WARN msg='He said \\'hi\\' twice'",
"2025-12-06T12:00:04Z service=auth pid=notanumber level=INFO msg='bad pid value'",
]
for t in test_lines:
print(repr(t))
print(parse_log_line(t))
print("1,000,000 lines/hour =", round(1_000_000 / 3600, 1), "lines/second")
Output, run against the sample line plus edge cases:
"2025-12-06T12:00:00Z service=auth pid=1234 level=ERROR msg='Failed login for user bob: invalid password'"
{'timestamp': '2025-12-06T12:00:00Z', 'service': 'auth', 'pid': 1234, 'level': 'ERROR', 'msg': 'Failed login for user bob: invalid password'}
"2025-12-06T12:00:01Z service=auth level=INFO msg='pid missing on this line'"
{'timestamp': '2025-12-06T12:00:01Z', 'service': 'auth', 'pid': None, 'level': 'INFO', 'msg': 'pid missing on this line'}
"2025-12-06T12:00:02Z service=auth pid=5678 level=WARN msg='He said \\'hi\\' twice'"
{'timestamp': '2025-12-06T12:00:02Z', 'service': 'auth', 'pid': 5678, 'level': 'WARN', 'msg': "He said 'hi' twice"}
"2025-12-06T12:00:04Z service=auth pid=notanumber level=INFO msg='bad pid value'"
{'timestamp': '2025-12-06T12:00:04Z', 'service': 'auth', 'pid': None, 'level': 'INFO', 'msg': 'bad pid value'}
1,000,000 lines/hour = 277.8 lines/second
Every field from the question (timestamp, service, pid as int, level, msg) is produced, a missing pid and a missing quoted value both degrade to None instead of raising, and the escaped-quote case (\'hi\') is correctly unescaped to a literal quote inside the message.
Trade-offs and pitfalls
A single anchored regex (^(?P<timestamp>\S+)\s+service=...) is more compact to write, but it is all-or-nothing: reorder the fields, drop one, or add an unexpected key, and the whole line fails to match instead of degrading field by field. If you do reach for a regex per field, keep quantifiers bounded (avoid nested .* inside the quoted-value group) so a message packed with escaped quotes cannot trigger catastrophic backtracking.
Swallowing a bad pid to None is the right behavior for a pipeline that must never crash on one bad line, but silently doing nothing else is its own risk: track a counter of how many lines failed to parse a given field, or a silent upstream format change (a service starts emitting pid as a hex string, say) goes unnoticed indefinitely.
Real log files are commonly UTF-8; if you are reading raw bytes rather than already-decoded text, quote and escape detection has to run on the decoded string, not the raw byte sequence, or a multi-byte character could be split mid-character by a byte-oriented scan.
Implement remove_element(nums, val) in-place in Python or Java: remove all occurrences of val from nums and return the new length. This is part of a backend cleanup job where payload arrays must be compacted before storage. Explain how to move elements and whether order must be preserved.
Sample Answer
Direct answer
Use a read/write two-pointer: walk the array once with a read index, and every time you see a value that isn't val, copy it into the next open slot tracked by a write index. The write index at the end is the new length. Whether order must be preserved decides which of two variants you use: the read/write copy above preserves the original relative order in O(n) writes; if order doesn't matter, you can instead swap a matching element with the current last element and shrink the array, which does fewer writes when val is rare.
Approach
- Order-preserving (read/write two-pointer):
writestarts at 0. For eachreadindex in order, ifnums[read] != val, copy it tonums[write]and advancewrite. Every kept element lands in its original relative order, one slot earlier than or at its original position. - Order-not-preserved (swap-with-last): keep a shrinking logical length
n. Whennums[i] == val, overwrite it withnums[n-1](the current last element) and shrinknby one, without advancingi(the swapped-in element still needs to be checked). Whennums[i] != val, advancei. This does one write per removal instead of potentially shifting every later element, which is cheaper when matches are rare and scattered. - Both mutate
numsin place and return the new length; elements at or past the returned length are not meaningfully defined afterward.
Complexity
Both variants: O(n) time (single pass), O(1) extra space. The order-preserving version always does one write per surviving element; the swap variant does one write per removed element, which is fewer when val is rare.
Edge cases
valnot present at all: every element is kept, new length equals original length, zero writes beyond the initial pass.- All elements equal
val: new length is 0. - Empty input: returns 0 immediately.
def remove_element(nums, val):
write = 0
for read in range(len(nums)):
if nums[read] != val:
nums[write] = nums[read]
write += 1
del nums[write:]
return write
def remove_element_unordered(nums, val):
i = 0
n = len(nums)
while i < n:
if nums[i] == val:
n -= 1
nums[i] = nums[n]
else:
i += 1
del nums[n:]
return n
payload = [4, 2, 5, 2, 7, 2, 9]
k = remove_element(payload, 2)
print(k, payload)
payload2 = [4, 2, 5, 2, 7, 2, 9]
k2 = remove_element_unordered(payload2, 2)
print(k2, payload2)
Output:
4 [4, 5, 7, 9]
4 [4, 9, 5, 7]
Both agree on the count (4 surviving elements), but the surviving values land in different positions: [4, 5, 7, 9] keeps the original left-to-right order, while [4, 9, 5, 7] does not (9 moved from the end into an earlier slot during a swap), which is exactly the trade-off the question is asking about.
Trade-offs and pitfalls
- A common bug: using
list.remove(val)or deleting elements from the middle of the array inside a loop, which is O(n) per removal (everything after the deletion point shifts down), making the whole operation O(n^2) in the worst case, and it also skips the next element if you don't adjust the loop index after a deletion. The two-pointer approaches here avoid both problems. - The same read/write two-pointer technique applies directly to low-level, fixed-size buffers, not just Python lists. In C, given a null-terminated
char *s, removing all space characters in place is the identical idea: a write index and a read index both walk the buffer, the write index only advances when the current character should be kept, and a null terminator is placed at the final write position. There's no list-resize step (del nums[write:]) because a C string doesn't carry a separate length field the way a Python list does; the null terminator is the length. - Backend-cleanup framing from the question: "compacting payload arrays before storage" is exactly the order-preserving case if the array represents an ordered sequence (e.g. a time-ordered log) where reordering would corrupt meaning, or the order-not-preserved case if it's an unordered set of records where minimizing writes matters more than position.
Given an array of integers, implement an algorithm to find all unique triplets that sum to zero (3-sum). Use lists and dictionaries where appropriate, aim to avoid duplicate triplets in the output, and explain time complexity. Provide Python code for the standard O(n^2) approach.
Sample Answer
Direct answer
Sort the array, then fix each element in turn as the smallest of a candidate triplet and use two pointers over the remaining sorted suffix to find pairs summing to its negation. Skipping repeated values at the fixed index and at both inner pointers is what avoids duplicate triplets in the output without a separate deduplication pass over a set of results.
Structured elaboration
- Sort first. Sorting costs
O(n log n)and is what enables both the two-pointer sweep and the duplicate-skipping logic below. - Outer loop. For each index
i(up ton - 2), ifnums[i] > 0the loop can break entirely: in a sorted-ascending array, no triplet starting at or after a positive number can ever sum to zero. Skipiif it repeats the previous value, to avoid re-deriving the same set of triplets from an identical starting point. - Inner two-pointer sweep. With
left = i + 1,right = n - 1, andtarget = -nums[i]: ifnums[left] + nums[right] == target, record the triplet and move both pointers inward, additionally skipping over any further repeats ofnums[left]ornums[right]so the SAME triplet isn't recorded twice; if the sum is too small, advanceleft; if too large, retreatright. - Complexity.
O(n log n)sort plusO(n^2)for the outer loop times the inner two-pointer sweep, dominated by theO(n^2)term overall. Extra space isO(1)beyond the sort itself and the output list (orO(n)if the sort isn't in-place, depending on language). - On "use lists and dictionaries where appropriate." The solution above uses only the sorted list and two pointers, with no dictionary needed for correctness. An equally valid
O(n^2)alternative fixesiand then runs a hash-SET-based two-sum pass over the remaining unsorted elements for eachi(checking whethertarget - nums[j]has been seen), which avoids needing the array sorted at all. Here, sorting is essentially free to do and additionally buys the early break and the duplicate-skipping logic for free, so the two-pointer version is the standard choice; the hash-set variant is worth naming as the alternative specifically for a case where the array's original order must be preserved for some OTHER constraint.
Worked example
def three_sum(nums):
nums = sorted(nums)
n = len(nums)
result = []
for i in range(n - 2):
if nums[i] > 0:
break
if i > 0 and nums[i] == nums[i - 1]:
continue
left, right = i + 1, n - 1
target = -nums[i]
while left < right:
s = nums[left] + nums[right]
if s == target:
result.append([nums[i], nums[left], nums[right]])
left += 1
right -= 1
while left < right and nums[left] == nums[left - 1]:
left += 1
while left < right and nums[right] == nums[right + 1]:
right -= 1
elif s < target:
left += 1
else:
right -= 1
return result
def three_sum_brute_force(nums):
n = len(nums)
found = set()
for i in range(n):
for j in range(i + 1, n):
for k in range(j + 1, n):
if nums[i] + nums[j] + nums[k] == 0:
found.add(tuple(sorted((nums[i], nums[j], nums[k]))))
return found
for nums in [[-1, 0, 1, 2, -1, -4], [0, 0, 0], [0, 0, 0, 0], [1, 2, -2, -1], []]:
result = three_sum(nums)
print(f"nums={nums} -> {result}")
as_multisets = [tuple(sorted(t)) for t in result]
assert len(as_multisets) == len(set(as_multisets)), "duplicate triplet detected"
assert set(as_multisets) == three_sum_brute_force(nums)
print("brute-force cross-check (as sets of sorted triplets) and no-duplicate check passed for all cases")
Output (executed, python3 s66_three_sum.py, cross-checked against an O(n^3) brute force as sets of sorted triplets, and checked that no triplet in the output repeats another as a multiset):
nums=[-1, 0, 1, 2, -1, -4] -> [[-1, -1, 2], [-1, 0, 1]]
nums=[0, 0, 0] -> [[0, 0, 0]]
nums=[0, 0, 0, 0] -> [[0, 0, 0]]
nums=[1, 2, -2, -1] -> []
nums=[] -> []
brute-force cross-check (as sets of sorted triplets) and no-duplicate check passed for all cases
[0, 0, 0, 0] correctly collapses to a single [0, 0, 0] triplet despite four zeros being present, which is exactly the duplicate-skip logic being exercised.
Trade-offs & pitfalls
- Duplicates can sneak into the output from three separate places: the outer index
i,leftafter a match, andrightafter a match. Missing any ONE of the three still produces duplicate triplets, so all three skips are needed together, not just one. - A naive alternative, dumping every found triplet (as a sorted tuple) into a
setafter the fact, is correct but wastes work: it still does the redundant searching that produces the duplicates in the first place, it just filters them out afterward instead of avoiding them. - This generalizes to k-sum by recursing one level per additional target element, but each added level multiplies in another
O(n)(orO(n log n)for the sort, done once) factor, so4Sumis alreadyO(n^3)and the approach stops scaling well pastkaround 4 or 5. sorted(nums)here returns a NEW list, so the caller's original array is untouched; an in-placenums.sort()would mutate it, which matters if the caller still needs the original order elsewhere.
For heavy-duty string processing in pandas, compare performance of using python loops (apply), pandas vectorized Series.str methods, and numpy.char functions. Given a 10M-row DataFrame, explain how you'd measure and optimize a tokenization pipeline for speed and memory.
Sample Answer
Direct answer
.apply() with a Python function calls the interpreter once per row, so its cost is dominated by Python function-call and frame overhead repeated 10 million times. Series.str methods look vectorized but for the default pandas object dtype they are a C-level loop that still calls Python string methods per element internally, so they mainly remove the apply/lambda call overhead, not the per-element string-processing cost itself. numpy.char gives genuine C-level looping, but it first requires converting the column to a fixed-width NumPy unicode array, and every string gets padded to the length of the LONGEST string in the column, which can be a serious memory cost with even one long outlier in 10 million rows. For real vectorized speed at that scale the accurate move is pandas' PyArrow-backed string dtype (or stepping outside pandas entirely to Polars), not numpy.char.
Structured elaboration
.apply() (Python loop). Complexity is O(n) but with a large constant factor: each row triggers a full Python function call (frame creation, bytecode dispatch inside the lambda, boxing/unboxing of Python string objects). Nothing about this is vectorized; it is a disguised Python for loop.
Series.str vectorized methods. For the default object dtype, a pandas string column is a NumPy array of POINTERS to individual Python str objects. .str.lower(), .str.strip(), and similar calls are implemented as a loop (in Cython, faster than a Python-level for) that still invokes the underlying Python string method on each element. This removes the per-row apply/lambda call overhead and Python-level loop bookkeeping, so it is typically faster than .apply(), but it is not vectorized in the CPU/SIMD sense the way numpy arithmetic on a float array is: each element still gets an individual Python-level string operation.
numpy.char functions. These operate on a fixed-width NumPy unicode array (dtype like <U12), which is genuinely vectorized C code with no per-element Python call. The cost is that building this array from a pandas string column requires padding (or truncating) every entry to a single common width, namely the length of the longest string present. A column of mostly 10-character strings with one 500-character outlier forces every row's underlying buffer to 500 characters, multiplying memory by roughly 50x for no reason related to the average case. numpy.char also does not implement every string operation (there is no vectorized split), so a full tokenization pipeline cannot be done in numpy.char alone: the final split step still needs a Python-level loop or a different tool.
Getting genuine vectorization at 10M rows. pandas 2.x's PyArrow-backed string dtype (pd.ArrowDtype(pa.string()), or the shorthand "string[pyarrow]") stores strings in Arrow's variable-length UTF-8 buffer format and executes string operations through Arrow's compiled compute kernels: no fixed-width padding, and per-element cost is a real vectorized cost reduction rather than just less interpreter overhead. This is the currently recommended path for large string columns in pandas specifically because it avoids both the numpy.char padding tax and the object-dtype per-element Python-call tax. Where the workload no longer fits comfortably in memory or on one core, moving outside pandas to Polars (native vectorized string kernels, no object-dtype layer) or Dask (chunked, parallel, out-of-core) is the next step up.
How you would actually measure it. Time comparisons should use a repeatable, environment-relative tool (timeit/%timeit in a notebook, or time.perf_counter around repeated runs) and be reported as a RATIO between approaches on the same machine and the same data, not as an absolute number, because absolute wall-clock time is hardware- and load-dependent and will not reproduce on a different machine. Memory should be measured with tracemalloc for general Python allocations or, pandas-specifically, DataFrame.memory_usage(deep=True) (the deep=True flag matters: without it, an object-dtype column reports only the size of the pointer array, not the actual string objects it points to, which drastically understates real memory use).
Optimizing the pipeline itself for 10M rows. Read in bounded chunks (pd.read_csv(..., chunksize=...)) to cap peak memory instead of loading the whole file at once. Prefer the PyArrow-backed string dtype from the start rather than converting after the fact. Collapse multiple chained .str.replace() calls into a single combined regex or str.translate pass: each .str.replace() call allocates a brand-new full-length Series, so five chained calls pay roughly five separate full-column allocations instead of one.
Worked example
import pandas as pd
import numpy as np
data = pd.DataFrame({"raw_text": [
"Hello World", " Pandas STR Methods ", "NumPy-Char Functions!",
"Tokenize, This Sentence.", "UPPER lower MiXeD",
]})
def tokenize_py(s):
return s.strip().lower().replace(",", "").replace(".", "").replace("!", "").split()
result_apply = data["raw_text"].apply(tokenize_py)
result_str = (
data["raw_text"].str.strip().str.lower()
.str.replace(",", "", regex=False).str.replace(".", "", regex=False)
.str.replace("!", "", regex=False).str.split()
)
np_arr = data["raw_text"].to_numpy(dtype=str)
np_clean = np.char.replace(np.char.replace(np.char.replace(
np.char.lower(np.char.strip(np_arr)), ",", ""), ".", ""), "!", "")
result_np = [s.split() for s in np_clean] # numpy.char has no vectorized split
assert list(result_apply) == list(result_str) == result_np
print("identical tokenization:", list(result_str)[0])
# The fixed-width memory trap, concretely:
print(pd.Series(["a", "bb", "ccc"]).to_numpy(dtype=str).dtype) # <U3
print(pd.Series(["a", "bb", "c" * 50]).to_numpy(dtype=str).dtype) # <U50
Output:
identical tokenization: ['hello', 'world']
<U3
<U50
All three approaches agree on the pinned sample (an equivalence check, not a timing benchmark). The dtype output is the concrete evidence for the fixed-width claim: adding one 50-character string to an otherwise-tiny column forces the whole array's per-element width to 50, regardless of how short the other rows are.
Trade-offs and pitfalls
The most common misconception is treating Series.str as fully vectorized the way numpy arithmetic is; for the default object dtype it only removes call overhead, not per-element cost, and a candidate who states this without the object-dtype caveat is glossing over exactly the distinction the question is testing. The numpy.char fixed-width padding trap is easy to miss because it is invisible on clean, uniform-length synthetic data and only shows up with real-world text containing outliers, exactly the situation a 10M-row production dataset is likely to have. Never cite a fixed wall-clock number ("this ran in 40ms") as a claimed fact: that number is specific to one machine's hardware and load, and does not reproduce; report methodology (which tool, what you would compare) and, if you have actually measured it yourself, a same-machine RATIO between approaches rather than an absolute duration. Chaining several separate .str calls is a subtler trap: each one is a full pass allocating a new Series, so five chained calls cost roughly five allocations where a single combined regex or translate table would cost one; this matters more, not less, as row count grows into the tens of millions.
Given an integer array (may contain negatives) and an integer k, implement a Python function that counts the number of contiguous subarrays whose sum equals k. Provide an O(n) time solution using prefix sums and a hashmap. Explain memory usage and how to handle very large integer sums safely.
Sample Answer
Direct answer
Use a running prefix sum together with a hash map that counts how many times each prefix-sum value has been seen. At each index, the number of subarrays ending there with sum k equals the number of earlier prefix sums equal to (current prefix sum minus k). This gives an O(n) time, O(n) space solution that works correctly with negative numbers, where a sliding window cannot be used.
Structured elaboration
Why prefix sums plus a hash map
Define prefix[i] as the sum of the first i elements. The sum of the subarray from index i+1 through j is prefix[j] - prefix[i]. That subarray sums to k exactly when prefix[i] = prefix[j] - k. So as you scan left to right building up the running prefix sum, you only need to ask "how many earlier prefix sums equal (current prefix sum - k)?", and a hash map from prefix-sum value to how many times it has occurred answers that in O(1) average time.
Seed the hash map with {0: 1} before the scan starts. That entry represents the "empty prefix" (the state before index 0), and it is what lets a subarray starting at index 0 be counted, since its prefix-sum-so-far is compared against 0.
Why negatives are fine here but break sliding window
A sliding window relies on the sum growing monotonically as you extend the window, so you can decide when to shrink it. With negative numbers allowed, extending the window can decrease the sum, so there is no monotonic rule for when to move the left edge. The hash map approach does not depend on monotonicity at all: it only tracks exact prefix-sum values, so negatives cause no correctness issue.
Memory usage
The hash map can hold up to n+1 distinct prefix-sum values (one per index plus the seed), so worst-case space is O(n). In practice, if the input has many repeated prefix sums (for example, sequences that oscillate around zero), the map stays much smaller.
Handling very large sums safely
In Python, integers are arbitrary-precision, so a sum can grow to any size without silently overflowing or wrapping the way a fixed-width 32-bit or 64-bit integer would in Java, C++, or Rust. That removes the classic overflow bug for this problem in Python specifically. The one caveat worth naming out loud: arithmetic and hashing on very large integers are not truly O(1), their cost grows with the number of digits d, roughly O(d) per addition or hash. For the sums produced by realistic array inputs this is negligible, but if you ported this exact code to a fixed-width language, you would need either a checked-addition guard (raise or saturate on overflow) or a big-integer type, since the prefix sum can exceed 64-bit range for large arrays of large values.
Worked example
from collections import defaultdict
def subarray_sum_equals_k(nums, k):
'''Count contiguous subarrays whose sum equals k. O(n) time, O(n) space.'''
count = 0
prefix_sum = 0
seen = defaultdict(int)
seen[0] = 1 # empty prefix, handles subarrays starting at index 0
for x in nums:
prefix_sum += x
count += seen[prefix_sum - k]
seen[prefix_sum] += 1
return count
nums = [1, 2, 3, -3, 1, 1, 1]
k = 3
print(subarray_sum_equals_k(nums, k))
Output:
6
Enumerating every contiguous subarray of nums by brute force and checking which ones sum to 3 confirms the six matches directly: [1,2], [1,2,3,-3], [2,3,-3,1], [3], [3,-3,1,1,1], and [1,1,1]. Both the hash-map solution and an independent brute-force enumeration were run against each other on this input and agree on the count of 6.
Trade-offs and pitfalls
Forgetting the {0: 1} seed is the single most common mistake: it silently undercounts every subarray that starts at index 0. Reaching for a sliding window out of habit is the second: it looks like the natural upgrade from brute force, but it is only correct when all values are non-negative, and this problem explicitly allows negatives. Recomputing each subarray's sum from scratch inside a nested loop is the brute-force O(n^2) trap this technique exists to avoid. Finally, remember the count returned can itself be larger than the array length (a single index can close out subarrays with several different earlier starting points), so do not assume the answer is bounded by n. The same prefix-sum-plus-hashmap idea ports directly to any language with a hash map, a Java version would use a HashMap<Long, Integer> in place of the dict, with identical logic.
Unlock Full Question Bank
Get access to all Arrays, Strings, and Hashing interview questions and detailed answers.
Sign in to ContinueJoin thousands of developers preparing for their dream job.