Reader Solutions Reviewed: Two Sum
One-Pass Solutions, Off-by-One Traps, and Hash Collision Edge Cases
Two Sum
Two Sum is the problem interviewers use when they want to see how you think. It looks deceptively simple, but the details separating a clean solution from a buggy one are exactly what senior engineers notice. Today we go beyond the basic answer: one-pass hash map, off-by-one errors and hash collision edge cases.
The One-Pass Hash Map Solution
Most explanations teach a two-pass approach: first scan the array to populate a map, then scan again to find complements. You don’t need two passes. You can build the map and check for complements simultaneously in a single traversal.
The insight: when you reach index i, every element before it has already been added to the map. So if the complement of nums[i] is already in the map, you’ve found your pair. No second pass needed.
TypeScript
function twoSum(nums: number[], target: number): number[] {
const indexByValue = new Map<number, number>();
for (let i = 0; i < nums.length; i++) {
const complement = target - nums[i];
if (indexByValue.has(complement)) {
return [indexByValue.get(complement)!, i];
}
indexByValue.set(nums[i], i); // store AFTER checking — crucial
}
}C#
public class Solution {
public int[] TwoSum(int[] nums, int target) {
var indexByValue = new Dictionary<int, int>(); // value -> index
for (int i = 0; i < nums.length; i++) {
int complement = target - nums[i];
if (indexByValue.TryGetValue(complement, out int complementIndex)) {
return new int[] { complementIndex, i };
}
indexByValue[nums[i]] = i; // store AFTER the check
}
}
}Java
import java.util.HashMap;
import java.util.Map;
class Solution {
public int[] twoSum(int[] nums, int target) {
Map<Integer, Integer> indexByValue = new HashMap<>();
for (int i = 0; i < nums.length; i++) {
int complement = target - nums[i];
if (indexByValue.containsKey(complement)) {
return new int[]{ indexByValue.get(complement), i };
}
indexByValue.put(nums[i], i); // store AFTER checking
}
}
}Notice the comment on that last line in each snippet: "store AFTER checking." That single ordering decision is where most one-pass bugs are born, and it connects directly to our next section.
Common Off-by-One and Ordering Errors
“Off-by-one” usually makes people think of loop bounds. In Two Sum, the trickier errors are about when you write to the map relative to when you read from it. Here are the two that I saw most often.
Error 1: Storing before checking (same-element reuse)
// BUGGY — stores num[i] before looking for its complement
for (int i = 0; i < nums.length; i++) {
indexByValue.put(nums[i], i); // ← stored first
int complement = target - nums[i];
if (indexByValue.containsKey(complement)) {
return new int[]{ indexByValue.get(complement), i };
}
}If target = 6 and nums[i] = 3, you store 3 → i, then immediately ask "is 3 in the map?" And find the element you just stored. The problem says you cannot use the same element twice. Storing first breaks that constraint for self-complementary values.
Fix: always check before storing, as shown in the correct solutions above.
Error 2: Off-by-one in loop bounds
// BUGGY — misses the last element
for (let i = 0; i < nums.length - 1; i++) {This is the classic off-by-one. Subtracting 1 from the upper bound seems harmless — you might reason “I need at least two elements left.” But in a one-pass hash map approach, you only need the current element and whatever is already in the map. The final element could be the one whose complement is already stored.
Fix: iterate to nums.length (TypeScript/Java) or nums.Length (C#), inclusive of the last index.
Hash Collision Edge Cases
This is the section almost nobody covers in Two Sum posts, and it’s the kind of depth that impresses a senior interviewer.
What is a hash collision?
A hash collision happens when two different keys produce the same hash code, causing them to land in the same underlying bucket. All major hash map implementations handle this with either chaining (linked list of entries per bucket) or open addressing (probe for the next empty slot).
In average-case analysis, lookup is O(1). In the worst case, if every key collides into the same bucket, lookup degrades to O(n), turning your elegant O(n) Two Sum into an accidental O(n²).
Does this matter in practice for Two Sum?
For most real inputs: no. Java’s HashMap, C#’s Dictionary, and JavaScript’s Map all use robust hash functions for integers that distribute keys well. But here’s when it does matter:
1. Adversarial inputs in competitive programming
Some judges are designed to generate inputs that maximize collisions for HashMap in Java specifically. The fix is to use a randomized hash function or switch to a TreeMap (which uses a balanced BST and gives O(log n) guaranteed lookup, at the cost of O(n log n) total time).
// Java: TreeMap gives O(log n) guaranteed lookup, no collision risk
import java.util.TreeMap;
Map<Integer, Integer> indexByValue = new TreeMap<>();2. Integer overflow when computing the complement
Not a hash collision, but a closely related “edge case nobody mentions”:
// nums[i] = Integer.MAX_VALUE = 2,147,483,647
// target = 0
int complement = target - nums[i]; // = -2,147,483,647 ✓ fine here
// But: target = Integer.MIN_VALUE, nums[i] = 1
int complement = target - nums[i]; // overflows in Java/C# (int arithmetic)In Java and C#, int arithmetic wraps silently. The fix is to use long for the complement calculation when inputs can be large:
long complement = (long)target - nums[i];Mentioning this in an interview separates you from candidates who only think about the algorithm and not the data.
Putting It Together: What “Production-Ready” Looks Like
Here’s a C# version that integrates all the guards discussed above:
public class Solution {
public int[] TwoSum(int[] nums, int target) {
if (nums == null || nums.Length < 2)
throw new ArgumentException("Input must contain at least two elements.");
var indexByValue = new Dictionary<int, int>(nums.Length); // pre-size the dict
for (int i = 0; i < nums.Length; i++) {
long complement = (long)target - nums[i]; // guard against overflow
// complement must fit in int range to be a valid array value
if (complement >= int.MinValue && complement <= int.MaxValue
&& indexByValue.TryGetValue((int)complement, out int complementIndex)) {
return new int[] { complementIndex, i };
}
indexByValue[nums[i]] = i; // store AFTER checking
}
throw new InvalidOperationException("No solution found.");
}
}Notice three upgrades from the interview version:
nums.Lengthpassed to theDictionaryconstructor pre-allocates capacity and avoids resize operations.longarithmetic guards against integer overflow on the complement.Exceptions instead of empty returns give the caller meaningful error signals. (Yes, the challenge guaranteed a unique solution, but that’s not always the case.
Reviewing a Python Solution
A full reader submission:
class Found(Exception): pass
def makesum (target: int, nums: list[int]) :
try:
for i in range (len(nums)):
for j in range (i+1,len(nums)):
if nums[i]+nums[j] == target :
raise Found
except Found :
return i,j
return None,None #not reachable as there IS a solution
target = 27
nums = [2,7,11,15,16]
i,j = makesum(target,nums)
print(f"[{i},{j}]")It works for the given input, but it’s not a good style or interview-quality solution.
What’s good
Correct brute-force logic: you check all (i, j) with j > i and return the first pair whose sum hits the target, which is the standard O(n^2) approach.
Clear intent: once a pair is found, you stop searching, so you don’t do unnecessary extra work.
Main issues
Misuse of exceptions for control flow
Using
raise Found/except Foundjust to break out of nested loops is considered unpythonic and harder to read than a simple flag or an extracted helper function.It also makes
iandjlook “magic”: they are set in the loop, but returned from inside theexceptblock, which depends on subtle scoping rules rather than explicit data flow.
Incorrect comment about reachability
The comment
#not reachable as there IS a solutionencodes a guarantee into the function rather than its caller.In a real codebase, the function should either:
Explicitly document “assumes a solution exists”, or
Handle the no-solution case gracefully (e.g., return
Noneor raise a meaningful error).
Type hints and interface
Return type is effectively
tuple[int, int] | tuple[None, None]but isn’t annotated.For code clarity, you might either:
Annotate
def makesum(...) -> tuple[int, int] | None:and returnNone, orIf you truly assume a solution exists, document that in the docstring and not return the
(None, None)sentinel.
Naming and style
makesumis vague; something liketwo_sum_indicesorfind_two_sumis clearer.PEP 8: spaces after function name before parentheses and around arguments are unusual (
def makesum (target: int, nums: list[int]) :).A small thing, but interviewers often care about these details.
How to write a similar brute-force
Here’s a direct, readable O(n^2) brute-force version that keeps the nested-loop idea, without exceptions:
from typing import Optional, Tuple
def two_sum_bruteforce(target: int, nums: list[int]) -> Optional[Tuple[int, int]]:
for i in range(len(nums)):
for j in range(i + 1, len(nums)):
if nums[i] + nums[j] == target:
return i, j
return NoneUsage:
target = 27
nums = [2, 7, 11, 15, 16]
result = two_sum_bruteforce(target, nums)
if result is not None:
i, j = result
print(f"[{i},{j}]")
else:
print("No solution found")For interviews: show the hash map version
Most interviewers expect you to move from this brute-force to an O(n) hash-map solution.
from typing import Optional, Tuple
def two_sum(target: int, nums: list[int]) -> Optional[Tuple[int, int]]:
index_by_value: dict[int, int] = {}
for i, num in enumerate(nums):
complement = target - num
if complement in index_by_value:
return index_by_value[complement], i
index_by_value[num] = i
return NoneWhat to Say in an Interview
Here’s the arc that lands well:
Restate the constraints — can we use the same element twice? Exactly one solution?
Offer the brute force — O(n²) time, O(1) space, nested loops.
Identify the bottleneck — “We recompute the complement search from scratch every iteration.”
Propose the hash map — “We can cache what we’ve seen for O(1) lookup.”
Do it in one pass — explain the store-after-check ordering.
Name the tradeoffs — O(n) space, amortized O(1) per operation, worst-case collision behavior.
Handle edge cases unprompted — duplicate values, integer overflow, self-complementary elements.
That arc shows algorithmic thinking, communication skills, and engineering maturity, all in one problem.
Up Next: Sliding Window
Two Sum teaches you to remember what you’ve seen using a hash map. The next pattern takes that idea further: what if instead of a single pair, you need to track a whole subarray as it moves through your data?
That’s Sliding Window, and it shows up everywhere: longest substring without repeating characters, maximum sum subarray of size k, minimum window substring, and more. It’s one of the highest-leverage patterns for medium and hard interview problems, and it’s more nuanced than most posts let on.
Next week’s deep-dive (Wednesday for paid subscribers) will cover fixed-window vs. variable-window variants, when to shrink from the left vs. expand from the right, and the exact questions that trip up candidates who think they understand it.
Subscribe so you don’t miss it.
Found this useful? Share it with someone grinding LeetCode right now — they’ll thank you later.
If you’re a paid subscriber, you also get a weekly deep‑dive post and the option to request a private resume review.
Not paid yet? You can upgrade here:
© The Coding Interview Gym | paulepps.substack.com


