Reading
Frequency Counting and Complement Lookup
Turn repeated comparisons into one-pass counting and partner searches.
Learning objectives
- Build and consume a frequency map correctly
- Derive a complement lookup from a target equation
- Trace duplicate and ordering edge cases
- Compare one-pass and two-pass approaches
Beginner: frequency maps are labeled counters
Consider the word banana. You could keep six unrelated marks on paper, but a frequency
map gives every distinct character a labeled counter:
b → 1
a → 3
n → 2 The key identifies the thing being counted; the value is its count. Build the map by reading one item at a time:
function countCharacters(text: string): Map<string, number> {
const counts = new Map<string, number>()
for (const character of text) {
counts.set(character, (counts.get(character) ?? 0) + 1)
}
return counts
} The invariant is: after processing the first i characters, each stored value equals
that character’s frequency in the processed prefix.
Worked example: consumable inventory
Suppose a magazine provides aabcc, and a note needs cab. First count the available
letters, then consume one count for every requested letter:
available starts as {a:2, b:1, c:2}
need c → 2 available → write c:1
need a → 2 available → write a:1
need b → 1 available → write b:0
all needs satisfied → true If a requested count is missing or zero, return false. The meaning of the map is now “inventory remaining,” not merely “total frequency.”
function canConstruct(note: string, magazine: string): boolean {
const available = countCharacters(magazine)
for (const character of note) {
const remaining = available.get(character) ?? 0
if (remaining === 0) return false
available.set(character, remaining - 1)
}
return true
} Could you count the note first instead?
Yes. You can store outstanding requirements, scan the magazine, and decrement needed letters. Both directions work. Choose one meaning for the map and keep it consistent.
Intermediate: complement lookup
When a pair must satisfy a target, ask what partner the current value needs. Look for that complement among previously processed values, then store the current value. Derive the lookup from the equation rather than memorizing a template:
current + partner = target
partner = target - current for (let index = 0; index < nums.length; index += 1) {
const needed = target - nums[index]
if (seenAt.has(needed)) return [seenAt.get(needed)!, index]
seenAt.set(nums[index], index)
} Check before inserting when the same array element may not be used twice.
Worked trace: [3, 2, 4], target 6
i current needed map before check action
0 3 3 {} store 3 → 0
1 2 4 {3 → 0} store 2 → 1
2 4 2 {3 → 0, 2 → 1} return [1, 2] For [3, 3] and target 6, the first 3 is stored and the second 3 finds it. This
uses two different indices while still supporting equal values.
Why does ordering matter?
If you insert first, the current value may satisfy its own complement lookup. Trace a single value equal to half the target.
One pass versus two passes
A two-pass solution can store every value first and look up complements second. It is still expected O(n), but it must explicitly reject a lookup that returns the current index. The one-pass version encodes “only earlier positions are candidates,” supports early return, and usually has the cleaner correctness argument.
Advanced: generalize the algebra
Complements appear whenever a required relationship can be rearranged around the current value:
a - b = ksuggests looking fora - korb + k.- A product target suggests
target / current, after handling zero and divisibility. a XOR b = targetsuggeststarget XOR currentbecause XOR reverses itself.
The syntax changes less than the reasoning. Ask which earlier state could pair with the current state, what key represents it, and whether you need existence, a count, or an index.
Common failure modes
- Forgetting to decrement a frequency used as consumable inventory.
- Returning values when the problem requests indices.
- Using truthiness to test an index that may be zero.
- Inserting before checking and allowing one element to match itself.
- Overwriting the earliest position when a later problem needs it preserved.
Recognition checklist
- What repeated question would a brute-force solution ask?
- Can its answer be stored under a stable key?
- Do I need existence, a count, or an index?
- What exactly does the structure contain after iteration
i? - Does check-before-insert or insert-before-check preserve the rule?
Derive before coding
For a target-sum problem, say the equation, isolate the needed earlier value, and state
what your Map stores. Then trace [3, 3] with target 6.