1. Introduction & Motivation
A hash table turns "search through everything" into "look it up directly," giving $O(1)$ expected-time lookups, inserts, and deletes. That single property is why hash-based structures show up everywhere: caches, symbol tables, deduplication, and a huge share of coding-interview questions. But knowing how a hash table is built is different from knowing when and how to reach for one as a problem-solving tool.
This lesson assumes you already know what a hash table is and how it resolves collisions. It does not re-explain that machinery. Instead it focuses on the techniques that sit on top: recognizing the frequency-counting pattern, combining a hashmap with a running prefix sum, designing a hash function for your own data types, and understanding why some hash functions resist adversarial inputs better than others.
2. Prerequisites
Read Part 11: Heaps, Sorting & Hashing first if you have not already. That lesson covers, in full, the material this one deliberately does not repeat:
- What a hash table / hash set is, including a complete
HashTableclass with separate chaining, load factor, and rehashing. - Collision handling — separate chaining versus open addressing, linear probing, and the tradeoffs between them.
- The three classic "Common Hash Table Problems": Two Sum, Group Anagrams, and Longest Consecutive Sequence, each implemented in Python, C++, and Java.
If any of those bullets are unfamiliar, go read that section now — everything below assumes you can already build and use a hash table, and instead teaches you new patterns for applying one.
3. A Concrete Example
Tracing the running-frequency-count technique
Consider nums = [1, 2, 3, -3, 4] and k = 3 — count subarrays whose elements sum to exactly 3. Maintain a running prefix sum curr_sum and a hashmap seen that counts how many times each prefix-sum value has occurred so far, seeded with {0: 1} for the empty prefix. At each step, before updating seen, check how many earlier prefix sums equal curr_sum - k — each one marks a subarray ending here that sums to k.
| Index | Value | curr_sum | curr_sum - k | count found in seen | running total | seen after update |
|---|---|---|---|---|---|---|
| — | — | 0 | — | — | 0 | {0: 1} |
| 0 | 1 | 1 | -2 | 0 | 0 | {0: 1, 1: 1} |
| 1 | 2 | 3 | 0 | 1 | 1 | {0: 1, 1: 1, 3: 1} |
| 2 | 3 | 6 | 3 | 1 | 2 | {0: 1, 1: 1, 3: 1, 6: 1} |
| 3 | -3 | 3 | 0 | 1 | 3 | {0: 1, 1: 1, 3: 2, 6: 1} |
| 4 | 4 | 7 | 4 | 0 | 3 | {0: 1, 1: 1, 3: 2, 6: 1, 7: 1} |
The final running total is 3, matching the three subarrays that actually sum to 3: [1, 2], [3] (at index 2), and [1, 2, 3, -3]. Each match was found the instant its closing element was processed, purely by looking up curr_sum - k in seen — without ever re-scanning the array.
4. Core Insight
- Identify the derived quantity that must repeat or match (a running sum, a running XOR, a character count, a normalized key).
- Decide what "seen before" means for that quantity — exact match, complement, or normalized form.
- Walk the input once, updating the derived quantity as you go.
- Before inserting the current derived value into the hashmap, query the hashmap for the value that would complete a match.
- Insert (or increment the count for) the current derived value so future elements can match against it.
5. Correctness
The pattern is correct because of a simple invariant: after processing index $i$, the hashmap contains every derived value produced by prefixes ending at or before $i$, each tagged with how many times it occurred. When index $i+1$ queries the hashmap for a target derived value, any hit corresponds to an earlier prefix that, combined with the elements between the two prefixes, produces exactly the property being counted. Because the hashmap is updated strictly after the query at each step, a prefix never matches against itself, and no valid pair is skipped because every earlier prefix remains in the map permanently (counts only grow, never shrink, during a single forward pass).
6. Complexity Analysis
7. Three-Language Implementation
The following implements the general running-derived-value-plus-hashmap technique, specialized here to counting subarrays with a target sum (the same shape used in the worked example above; the full Subarray Sum Equals K writeup with LeetCode framing follows in section 9).
8. Topic-Specific Deep Dives
The Frequency-Counting Pattern
The frequency-counting pattern is a general technique, not a single trick tied to one problem: use a hashmap to count how many times each value, character, or derived key appears, then answer the actual question from those counts instead of rescanning the input. Recognizing it means noticing any of these question shapes:
- "How many times does X occur?" — count directly.
- "What is the most/least frequent element?" — count, then take a max/min over the counts.
- "Are two collections permutations of each other?" — count both, compare the count maps.
- "How many pairs/subarrays/substrings satisfy a sum, difference, or parity condition?" — count a derived quantity (a running sum, a running XOR, a character-count signature) rather than the raw elements.
The pattern always has the same two-phase shape: a counting phase that builds a hashmap of key → frequency in $O(n)$, followed by a query phase that reads from that map in $O(1)$ per query. When the "key" being counted is the raw input value, this looks like plain frequency counting (contains-duplicate style problems). When the "key" is a running derived value computed incrementally, it looks like the running-sum technique traced above — same pattern, different key.
Subarray Sum Equals K
LeetCode 560, Subarray Sum Equals K: given an integer array nums and an integer k, return the number of contiguous subarrays whose sum equals k. This is the general technique from section 7, made concrete and complete:
It is tempting to file this under "just prefix sums," but it needs more than the plain prefix-sum technique from Prefix Sums, Difference Arrays & Coordinate Compression. That lesson's prefix-sum array answers range-sum queries: given fixed endpoints $i$ and $j$, what is the sum of nums[i..j]? It precomputes an array once and then answers each query in $O(1)$ by subtracting two precomputed values, because the endpoints are known in advance.
Subarray Sum Equals K does not have fixed endpoints — it asks "for how many pairs of endpoints does the sum equal $k$," across all possible pairs simultaneously. There is no query with known $i$ and $j$ to look up; instead, the problem needs to count how many times a particular running-sum value has occurred so far, so that every earlier occurrence can pair with the current position. That counting requirement — not just storing prefix sums, but tallying how often each one repeats — is exactly why a hashmap is layered on top of the running sum. Plain prefix sums answer range queries; this problem counts occurrences of a running value, which is a hashmap's job.
Designing a Custom Hash Function
Built-in types already have good hash functions, but real systems frequently need to hash custom objects — a 2D point, a composite key, a small record. Suppose you want to hash a point (x, y) with integer coordinates so it can be used as a dictionary key or set member.
A naive first attempt, hash = x + y, is a poor choice: points like (1, 5) and (3, 3) collide even though they are unrelated, and the hash barely uses the available output range — points along any diagonal x + y = c all land in the same bucket, causing exactly the clustering that good hash design tries to avoid. A better approach mixes the coordinates so that changing either one changes most of the output bits, and multiplies by odd, non-trivial constants (large primes work well) to spread values across the table:
Three design points generalize beyond points: (1) the equals/hash contract is non-negotiable — Python's __hash__/__eq__ and Java's hashCode()/equals() both require that objects considered equal produce the same hash, or lookups silently fail even when an "equal" entry is already stored. (2) combine fields with XOR and multiplication by distinct constants, never plain addition, so that no simple linear relationship between fields (like a shared diagonal) creates clustering. (3) test distribution by hashing a representative sample of your real keys and checking bucket occupancy is roughly uniform, not just testing a handful of arbitrary examples.
A Note on Universal Hashing
Every hash function has inputs that collide — for any fixed function $h$ mapping a large key space into $m$ buckets, an adversary who knows $h$ can always construct a set of keys that all hash to the same bucket, collapsing average-case $O(1)$ operations into worst-case $O(n)$. This is not a bug in a particular implementation; it is unavoidable for any single, fixed hash function.
Universal hashing sidesteps the problem by using a family of hash functions rather than one fixed function, and choosing a member of that family at random each time a hash table is created. A family is called universal if, for any two distinct fixed keys, the probability that a randomly chosen function from the family sends them to the same bucket is at most $1/m$ — the same bound you would expect from a genuinely random function. Because the choice of function is randomized and hidden from whoever supplies the keys, no adversary can pick inputs in advance that are guaranteed to collide against whichever function actually gets chosen.
This matters most for adversarial-input resistance: a public-facing service that hashes attacker-controlled strings (form fields, URL parameters, uploaded identifiers) into an internal hash table can be deliberately driven toward its worst case unless the hash function is unpredictable to the attacker. A full treatment of universal hashing families — their construction, the algebra behind proving the $1/m$ bound, and how languages like Python randomize string hashing per-process to get this property in practice — lives in a later, more advanced curriculum entry; this is just the concept, not an implementation.
9. Applications & Alternatives
Where these techniques show up
- Analytics pipelines: frequency counting powers top-K queries, histogram generation, and anomaly detection over event streams.
- Financial and inventory systems: running-sum-plus-hashmap counting detects balanced or target-matching transaction windows without an $O(n^2)$ scan.
- Spatial indexes and game engines: custom hash functions for coordinate tuples back spatial hash grids used for collision detection and neighbor queries.
- Public-facing web services: universal/randomized hashing defends request-parameter hash tables against algorithmic-complexity denial-of-service attacks.
Alternatives exist for each technique: a Fenwick tree or segment tree can answer sum-range queries with updates, trading simplicity for $O(\log n)$ update support the hashmap-based running count does not need. A sorted structure (balanced BST, sorted array with binary search) can replace a hash set when ordered traversal or range queries matter more than raw $O(1)$ lookup. For custom hash functions, language-provided combinators (Python's hash(tuple), C++'s boost::hash_combine, Java's Objects.hash) are usually good enough and safer than a hand-rolled mix unless you have measured a real distribution problem.
10. Exercises
Easy
- Given an array and an integer
k, determine whether the array contains two equal values whose indices differ by at mostk(a Two Sum variant using a hashmap of value → last-seen index). - Given two strings, use frequency counting to determine whether one is an anagram of the other without sorting either string.
Medium
- Given an array
numsand an integerk, count the number of contiguous subarrays whose sum is divisible byk(a Subarray Sum Equals K variant: hash the remainder of the running sum modulokinstead of the sum itself). - Design a hash function for a small fixed-length string (for example, a 3-character ticker symbol) and discuss how you would verify it distributes realistic inputs evenly across a table of 1024 buckets.
Hard
- Given a string and an integer
k, find the length of the longest substring containing at mostkdistinct characters. This combines the frequency-counting pattern with the sliding-window technique from Two Pointers & Sliding Window: maintain a hashmap of character counts inside a shrinking/growing window and shrink the window whenever the distinct-character count exceedsk.
Review rhythm: tomorrow, re-derive the running-sum-plus-hashmap trace for a new small array without looking at this page; next week, implement Subarray Sum Equals K from memory in all three languages and then adapt it to the divisible-by-k variant; several weeks later, design a hash function for a different custom type (a 3-tuple, a small immutable record) and check it against the same three design points — contract, mixing, distribution. Finally, explain to someone else why plain prefix sums and this lesson's technique solve different problems despite both starting from a running sum.
11. Summary Cheat Sheet
- The frequency-counting pattern: build a hashmap of key → count in one pass, then answer the real question from those counts.
- The running-derived-value technique: compute a quantity incrementally (running sum, running XOR, running remainder) and query a hashmap of past values before inserting the current one.
- Subarray Sum Equals K needs a hashmap on top of a running sum because it counts occurrences of a value across all pairs of endpoints, not a single range query with fixed endpoints — that is the job of plain prefix sums.
- Custom hash functions must respect the equals/hash contract, mix fields with XOR and distinct multipliers instead of addition, and be checked against representative real data for uniform distribution.
- Universal hashing uses a randomly chosen family member per hash table instance so no adversary can predict collisions in advance; it defends against algorithmic-complexity attacks on public-facing hash tables.
- These techniques compose: frequency counting plus sliding window solves substring-with-constraint problems; running-sum-plus-hashmap generalizes to any "count occurrences of a repeated derived value" problem.