DSA DAY ONE
DSA DAY ONE
PART I — FOUNDATIONS
Chapter 1: C++ Essentials for DSA
Chapter 2: C++ STL Deep Dive
Chapter 3: Complexity Analysis
PART II — LINEAR DATA STRUCTURES
Chapter 4: Arrays & Strings
Chapter 5: Two Pointers & Sliding Window
Chapter 6: Hashing
Hash Functions — What Makes a Good HashCollision Handling — Chaining vs. Open Addressing`unordered_map` vs. `map` — When to Use WhichFrequency Counting — The Master PatternTwo Sum & The Complement Lookup PatternAnagram Detection & GroupingCustom Hashing for Pairs & StructsRolling Hash — Rabin-Karp IntroductionSample ProblemsLeetCode Problem Set
Chapter 7: Linked Lists
Chapter 8: Stacks
Chapter 9: Queues & Deques
PART III — TREES
Chapter 10: Binary Trees
Chapter 11: Binary Search Trees (BST)
Chapter 12: Heaps & Priority Queues
Chapter 13: Tries (Prefix Trees)
Chapter 14: Segment Tree & Fenwick Tree
PART IV — ALGORITHMS
Chapter 15: Sorting Algorithms
Chapter 16: Binary Search
Chapter 17: Recursion & Backtracking
Chapter 18: Graphs — Fundamentals & Traversals
Chapter 19: Graph Algorithms — Shortest Paths, MST & Union-Find
Chapter 20: Dynamic Programming
Chapter 21: Greedy Algorithms
Chapter 22: Divide & Conquer
PART V — COMPETITIVE & MATH
Chapter 23: Bit Manipulation
Chapter 24: Math & Number Theory
PART VI — PATTERNS
Chapter 25: Pattern Recognition Mastery
PART VII — SYSTEM DESIGN
Chapter 26: System Design for FAANG Interviews
PART VIII — INTERVIEW STRATEGY
Chapter 27: Cracking FAANG — Strategy & Communication
06

Hashing


Chapter 6 ·  Part 2: LINEAR DATA STRUCTURES ·  Est. 29 min read

Chapter Goal: Achieve complete mastery of hashing — from the internal mechanics of hash tables to every pattern that uses them in interviews. Hashing converts O(n) or O(n log n) brute-force solutions into O(n) elegant ones. It is the single most frequently used technique after arrays in FAANG interviews.


6.1 Hash Functions — What Makes a Good Hash

What Is a Hash Function?

A hash function maps an arbitrary input (integer, string, object) to a fixed-size integer index — the hash code. This index determines which bucket in the hash table stores the value.

Input:  "interview"   →   hash("interview") = 7,423,891   →   bucket 91  (mod 100)
Input:  "apple"       →   hash("apple")     = 2,300,412   →   bucket 12
Input:  42            →   hash(42)          = 42           →   bucket 42  (mod 100)

Properties of a Good Hash Function

Property Meaning Why It Matters
Deterministic Same input → same output always Correctness
Uniform Outputs spread evenly across buckets Minimizes collisions
Fast O(1) to compute Performance
Avalanche effect Small input change → large hash change Avoids clustering

Integer Hashing

The simplest hash: h(k) = k % m where m is the number of buckets.

// C++ std::hash for built-in types
#include <functional>
 
hash<int>    intHash;
hash<string> strHash;
 
cout << intHash(42)          << "\n";  // Some large number
cout << strHash("interview") << "\n";  // Some large number
 
// Modulo to get bucket index
int buckets = 1000;
int idx = intHash(42) % buckets;

Choosing m: A prime number reduces clustering. m = 1e9 + 7 and m = 998244353 are the two most common primes in competitive programming.


Polynomial String Hashing

For strings, hash each character using a polynomial:

hash("abc") = 'a'·B² + 'b'·B¹ + 'c'·B⁰  (mod M)

where B = base (e.g., 31 for lowercase, 131 for all ASCII)
      M = large prime (e.g., 1e9+7)
// Compute polynomial hash of a string
long long polyHash(const string& s, long long base = 31, long long mod = 1e9 + 7) {
    long long hash = 0, power = 1;
    for (char c : s) {
        hash  = (hash + (c - 'a' + 1) * power) % mod;
        power = (power * base) % mod;
    }
    return hash;
}
 
// Why 'a'+1 (1-indexed)? So that "a" and "aa" don't both start the same way.
// "a"  = 1·1         = 1
// "aa" = 1·1 + 1·31  = 32   (different from "a")

The Pigeonhole Principle — Collisions Are Unavoidable

If you hash n+1 items into n buckets, at least one bucket must hold ≥2 items. No matter how good your hash function, collisions are mathematically unavoidable when the input domain exceeds the number of buckets. This is why collision handling is part of every hash table.


6.2 Collision Handling — Chaining vs. Open Addressing

Separate Chaining (Used by unordered_map)

Each bucket holds a linked list (or similar structure) of all key-value pairs that hash to that bucket. On collision, just append to the list.

Buckets:    0 → NULL
            1 → [("one", 1)] → NULL
            2 → [("two", 2)] → [("twelve", 12)] → NULL    ← COLLISION
            3 → [("three", 3)] → NULL
            ...
  • Lookup: hash key → go to bucket → scan the list: O(1) avg, O(n) worst
  • Insert: hash key → go to bucket → prepend: O(1) avg
  • Space: O(n + m) where m = number of buckets
// What bad luck looks like (all keys map to same bucket):
unordered_map<int,int> mp;
// Adversarial input: all keys ≡ 0 (mod bucket_count)
// Every lookup degrades to O(n) list scan
// → Use map<int,int> instead when adversarial input is possible

Open Addressing (Not Used by STL, but Conceptually Important)

On collision, probe the table for the next empty slot.

  • Linear probing: try bucket+1, bucket+2, bucket+3, ...
  • Quadratic probing: try bucket+1², bucket+2², bucket+3², ...
  • Double hashing: try bucket + i * h2(key) for a second hash h2
Insert "xyz" → hashes to bucket 5, but bucket 5 is full
Linear probing: try 6, 7, 8... until empty slot found

Clustering problem: consecutive full buckets form "clusters" that grow
→ Quadratic/double hashing distributes probes better

Load Factor & Rehashing

Load factor = (number of elements) / (number of buckets)

A high load factor means more collisions. unordered_map automatically rehashes (doubles the bucket count and re-inserts everything) when load factor exceeds a threshold (~1.0 by default).

unordered_map<int,int> mp;
mp.reserve(100000);      // Pre-allocate buckets — avoids repeated rehashing
mp.max_load_factor(0.25);  // Trigger rehash earlier → fewer collisions, more memory
 
cout << mp.load_factor();   // Current load factor
cout << mp.bucket_count();  // Current number of buckets

Interview insight: reserve() on an unordered_map before bulk-inserting n elements reduces the expected number of rehashes to zero — a measurable speed-up for n ≥ 10⁴.


6.3 unordered_map vs. map — When to Use Which

We touched on this in Chapter 2. Here is the complete, decision-ready comparison.

Performance Comparison

Operation map (Red-Black Tree) unordered_map (Hash Table)
find, count O(log n) O(1) avg, O(n) worst
insert, erase O(log n) O(1) avg, O(n) worst
lower_bound, upper_bound O(log n) Not available
Iteration order Sorted by key Arbitrary order
Memory usage Lower Higher (hash table overhead)
Worst case safety Always O(log n) Warning: O(n) with collisions

Decision Rule

Need sorted order, predecessor/successor, or range queries?
    → map<K, V>

Need pure O(1) lookup, frequency count, memoization, or grouping?
    → unordered_map<K, V>

Dealing with adversarial input (competitive programming)?
    → map<K, V>  (safe O(log n) guarantee)

Key is a non-standard type (pair, struct, vector)?
    → map<K, V> with operator<, OR
    → unordered_map<K, V> with custom hash (Section 6.7)

Common Pitfall — operator[] Creates Entries

unordered_map<string, int> cnt;
 
// ❌ This inserts "missing_key" with value 0 if it doesn't exist
int val = cnt["missing_key"];   // Now cnt has a spurious entry
 
// ✅ Use find() for safe lookup
auto it = cnt.find("missing_key");
if (it != cnt.end()) {
    int val = it->second;
}
 
// ✅ Or use count() before accessing
if (cnt.count("missing_key")) {
    int val = cnt["missing_key"];
}

6.4 Frequency Counting — The Master Pattern

Frequency counting is the foundation of almost every hashing problem. The idea: build a map from each element to its count, then answer questions about counts.

Three Implementations by Use Case

// ── Option 1: Fixed array — O(1) space for bounded character set ────────────
int freq[26] = {};                      // Lowercase a-z
int freq128[128] = {};                  // All ASCII characters
for (char c : s) freq[c - 'a']++;
 
// ── Option 2: unordered_map — General purpose, any hashable type ─────────────
unordered_map<int, int> cnt;
for (int x : nums) cnt[x]++;
 
// ── Option 3: sorted + count — When you need sorted traversal of results ─────
map<int, int> sortedCnt;
for (int x : nums) sortedCnt[x]++;

Pattern 1: First Non-Repeating Character

int firstUniqChar(string s) {
    int freq[26] = {};
    for (char c : s) freq[c - 'a']++;
    for (int i = 0; i < (int)s.size(); i++)
        if (freq[s[i] - 'a'] == 1) return i;
    return -1;
}
// Time: O(n), Space: O(1)

Pattern 2: Majority Element (Boyer-Moore Voting — No Hash Needed)

// Moore's Voting: if majority exists, it survives cancellation
int majorityElement(vector<int>& nums) {
    int candidate = nums[0], count = 1;
    for (int i = 1; i < (int)nums.size(); i++) {
        if (count == 0) { candidate = nums[i]; count = 1; }
        else count += (nums[i] == candidate) ? 1 : -1;
    }
    return candidate;  // Guaranteed to exist by problem statement
}
// Time: O(n), Space: O(1) — cleverly avoids hashing entirely

Pattern 3: Top K Frequent Elements — Two Approaches

// Approach A: Freq map + Min-Heap — O(n log k)
vector<int> topKFrequent(vector<int>& nums, int k) {
    unordered_map<int,int> cnt;
    for (int x : nums) cnt[x]++;
 
    // Min-heap of size k: {frequency, value}
    priority_queue<pair<int,int>,
                   vector<pair<int,int>>,
                   greater<>> minPQ;
 
    for (auto& [val, freq] : cnt) {
        minPQ.push({freq, val});
        if ((int)minPQ.size() > k) minPQ.pop();  // Keep only top k
    }
 
    vector<int> result;
    while (!minPQ.empty()) {
        result.push_back(minPQ.top().second);
        minPQ.pop();
    }
    return result;
}
// Time: O(n log k), Space: O(n + k)
// Approach B: Bucket Sort — O(n) — frequencies are bounded by n
vector<int> topKFrequent(vector<int>& nums, int k) {
    unordered_map<int,int> cnt;
    for (int x : nums) cnt[x]++;
 
    // Bucket i holds all elements with frequency i
    int n = nums.size();
    vector<vector<int>> buckets(n + 1);
    for (auto& [val, freq] : cnt) buckets[freq].push_back(val);
 
    vector<int> result;
    for (int freq = n; freq >= 1 && (int)result.size() < k; freq--) {
        for (int val : buckets[freq]) {
            result.push_back(val);
            if ((int)result.size() == k) break;
        }
    }
    return result;
}
// Time: O(n), Space: O(n)

Pattern 4: Check If Two Frequency Maps Are Equal

bool sameFrequency(const string& s, const string& t) {
    if (s.size() != t.size()) return false;
    int freq[26] = {};
    for (char c : s) freq[c-'a']++;
    for (char c : t) if (--freq[c-'a'] < 0) return false;
    return true;  // All frequencies match
}

6.5 Two Sum & The Complement Lookup Pattern

The Core Insight

For any problem asking "does there exist an element X such that X + current = target?", use a hash set to track elements seen so far and look up target - current in O(1).

Brute force: for each element, scan all others → O(n²)
With hashing: for each element, look up its complement → O(n)

Classic Two Sum (LC 1)

vector<int> twoSum(vector<int>& nums, int target) {
    unordered_map<int,int> seen;  // value → index
 
    for (int i = 0; i < (int)nums.size(); i++) {
        int complement = target - nums[i];
        if (seen.count(complement)) {
            return {seen[complement], i};
        }
        seen[nums[i]] = i;
    }
    return {};
}
// Time: O(n), Space: O(n)
// Why insert AFTER looking up? To handle the case where nums[i]*2 == target.
// We want a different index, not the same element twice.

Variant: Count Pairs with Difference K

// Count pairs (i, j) where i < j and nums[i] - nums[j] == k
int countPairsWithDiff(vector<int>& nums, int k) {
    unordered_map<int,int> freq;
    for (int x : nums) freq[x]++;
 
    int count = 0;
    for (auto& [val, f] : freq) {
        if (freq.count(val + k)) {
            count += f * freq[val + k];  // All pairs between the two groups
        }
    }
    // If k == 0, pairs are within the same group: C(freq, 2) = freq*(freq-1)/2
    if (k == 0) {
        count = 0;
        for (auto& [val, f] : freq)
            count += f * (f - 1) / 2;
    }
    return count;
}

Variant: 4Sum II (LC 454)

Find the number of tuples (i,j,k,l) such that A[i]+B[j]+C[k]+D[l] == 0.

Key idea: Split into two groups of two arrays. Hash all pairwise sums from A+B, then look up their complements -(C+D).

int fourSumCount(vector<int>& A, vector<int>& B,
                 vector<int>& C, vector<int>& D) {
    unordered_map<int,int> abSum;
 
    // Store all A[i] + B[j] sums
    for (int a : A)
        for (int b : B)
            abSum[a + b]++;
 
    int count = 0;
    // For each C[k] + D[l], look for its negation
    for (int c : C)
        for (int d : D)
            count += abSum[-(c + d)];
 
    return count;
}
// Time: O(n²), Space: O(n²)
// Brute force would be O(n⁴) — this is a massive improvement

The Unified Prefix Hash Pattern — Subarray Problems

This is the most powerful hashing pattern for array problems. It converts "find subarray where property P holds" into a hash lookup.

Core formula for SUM-based problems:

prefixSum[j] - prefixSum[i] = k
→ prefixSum[i] = prefixSum[j] - k

At each j, count how many previous i's gave prefixSum[i] = prefixSum[j] - k
→ Store: freq_map[prefixSum] = count of times this prefix sum appeared
→ At each step: answer += freq_map[currentPrefixSum - k]
// Count subarrays with sum = k
int subarraySum(vector<int>& nums, int k) {
    unordered_map<int,int> prefixCount;
    prefixCount[0] = 1;   // Empty prefix
    int prefixSum = 0, count = 0;
    for (int x : nums) {
        prefixSum += x;
        count += prefixCount[prefixSum - k];
        prefixCount[prefixSum]++;
    }
    return count;
}
// Time: O(n), Space: O(n)

Prefix Hash Pattern — XOR Variant

The same idea applies to XOR instead of sum.

// Count subarrays with XOR = k
int subarrayXOR(vector<int>& nums, int k) {
    unordered_map<int,int> prefixCount;
    prefixCount[0] = 1;
    int xorSoFar = 0, count = 0;
    for (int x : nums) {
        xorSoFar ^= x;
        // prefixXOR[j] ^ prefixXOR[i] = k  →  prefixXOR[i] = prefixXOR[j] ^ k
        count += prefixCount[xorSoFar ^ k];
        prefixCount[xorSoFar]++;
    }
    return count;
}
// Time: O(n), Space: O(n)

Prefix Hash Pattern — Binary Array (Equal 0s and 1s)

// LC 525: Longest subarray with equal number of 0s and 1s
// Trick: treat 0 as -1, then find longest subarray with sum = 0
int findMaxLength(vector<int>& nums) {
    unordered_map<int,int> firstSeen;  // prefixSum → first index seen
    firstSeen[0] = -1;                 // Sum 0 seen before index 0
    int prefixSum = 0, maxLen = 0;
 
    for (int i = 0; i < (int)nums.size(); i++) {
        prefixSum += (nums[i] == 1) ? 1 : -1;
 
        if (firstSeen.count(prefixSum)) {
            maxLen = max(maxLen, i - firstSeen[prefixSum]);
        } else {
            firstSeen[prefixSum] = i;  // Store first occurrence only
        }
    }
    return maxLen;
}
// Time: O(n), Space: O(n)

General Prefix Hash Template

// Universal template for "find subarray/longest subarray" with prefix property
//
// For COUNT problems:  map[prefix] = count
// For LENGTH problems: map[prefix] = first index
//
// Seed:  map[identity_value] = 0 (for count) or -1 (for length/index)
//        where identity_value = prefix value representing empty prefix
//
// Variants:
//   Sum = k:   prefixSum + x,  look up (prefixSum - k)
//   XOR = k:   prefixXOR ^ x,  look up (prefixXOR ^ k)
//   Mod = 0:   prefixSum + x,  look up (prefixSum % k) same bucket

6.6 Anagram Detection & Grouping

Two Strings are Anagrams If and Only If

  1. They have the same length, AND
  2. They have the same character frequency map

There are two ways to represent the "signature" of a string:

  • Sorted string: "listen" → "eilnst"
  • Frequency tuple: "listen" → (0,0,0,...,1,1,1,0,...) (26 integers)
// Sorted string signature (simpler, slightly slower: O(k log k) per string)
string signature(const string& s) {
    string sorted_s = s;
    sort(sorted_s.begin(), sorted_s.end());
    return sorted_s;
}
 
// Frequency tuple signature (O(k) per string, but harder to use as map key)
array<int,26> freqSignature(const string& s) {
    array<int,26> freq = {};
    for (char c : s) freq[c-'a']++;
    return freq;  // Can use as key in map<array<int,26>, ...>
}

Group Anagrams (LC 49)

vector<vector<string>> groupAnagrams(vector<string>& strs) {
    unordered_map<string, vector<string>> groups;
 
    for (const string& s : strs) {
        string key = s;
        sort(key.begin(), key.end());   // Sorted string as key
        groups[key].push_back(s);
    }
 
    vector<vector<string>> result;
    for (auto& [key, group] : groups) {
        result.push_back(group);
    }
    return result;
}
// Time: O(n · k log k) where n = number of strings, k = max string length
// Space: O(n · k)
 
/*
  Input: ["eat","tea","tan","ate","nat","bat"]
  Sorted keys:
    "eat" → "aet" → group with "tea","ate"
    "tan" → "ant" → group with "nat"
    "bat" → "abt" → alone
 
  Output: [["eat","tea","ate"],["tan","nat"],["bat"]]
*/

Isomorphic Strings (LC 205)

Two strings are isomorphic if the characters in s can be replaced to get t. Requires a bijective mapping — one-to-one AND onto.

bool isIsomorphic(string s, string t) {
    if (s.size() != t.size()) return false;
 
    unordered_map<char,char> sToT, tToS;
 
    for (int i = 0; i < (int)s.size(); i++) {
        char sc = s[i], tc = t[i];
 
        if (sToT.count(sc)) {
            if (sToT[sc] != tc) return false;   // Inconsistent mapping
        } else {
            if (tToS.count(tc) && tToS[tc] != sc) return false;  // Not bijective
            sToT[sc] = tc;
            tToS[tc] = sc;
        }
    }
    return true;
}
// Time: O(n), Space: O(1) — at most 128 ASCII chars stored
 
/*
  s = "egg", t = "add"
  i=0: e→a, a→e  ✓
  i=1: g→d, d→g  ✓
  i=2: g→d  ✓ (consistent)
  → true
 
  s = "foo", t = "bar"
  i=0: f→b, b→f  ✓
  i=1: o→a, a→o  ✓
  i=2: o→? but o is already mapped to a, t[2]='r' ≠ 'a' → false
*/

Word Pattern (LC 290)

// Check if pattern "abba" matches word string "dog cat cat dog"
bool wordPattern(string pattern, string s) {
    vector<string> words;
    stringstream ss(s);
    string word;
    while (ss >> word) words.push_back(word);
 
    if (pattern.size() != words.size()) return false;
 
    unordered_map<char,   string> charToWord;
    unordered_map<string, char>   wordToChar;
 
    for (int i = 0; i < (int)pattern.size(); i++) {
        char   p = pattern[i];
        string w = words[i];
 
        if (charToWord.count(p) && charToWord[p] != w) return false;
        if (wordToChar.count(w) && wordToChar[w] != p) return false;
        charToWord[p] = w;
        wordToChar[w] = p;
    }
    return true;
}
// Time: O(n), Space: O(n)

6.7 Custom Hashing for Pairs & Structs

By default, unordered_map only supports types for which std::hash is defined: int, long long, string, double, etc. For pair<int,int>, vector<int>, or custom structs, you must provide a hash function.


Approach 1: Custom Hash Struct

// Hash for pair<int, int>
struct PairHash {
    size_t operator()(const pair<int,int>& p) const {
        // Cantor pairing or bit-mixing
        size_t h1 = hash<int>{}(p.first);
        size_t h2 = hash<int>{}(p.second);
        // Combine: the 0x9e3779b9 constant is derived from golden ratio
        return h1 ^ (h2 << 32) ^ (h2 >> 32);
    }
};
 
unordered_map<pair<int,int>, int, PairHash> pairMap;
pairMap[{1, 2}] = 42;
pairMap[{3, 4}] = 99;

Approach 2: Hash Combine (Boost-Style) — Most Robust

// Template hash combiner (equivalent to boost::hash_combine)
template <class T>
void hashCombine(size_t& seed, const T& v) {
    seed ^= hash<T>{}(v) + 0x9e3779b9 + (seed << 6) + (seed >> 2);
}
 
// Hash for pair<int,int>
struct PairHash {
    size_t operator()(const pair<int,int>& p) const {
        size_t seed = 0;
        hashCombine(seed, p.first);
        hashCombine(seed, p.second);
        return seed;
    }
};
 
// Hash for vector<int>
struct VectorHash {
    size_t operator()(const vector<int>& v) const {
        size_t seed = v.size();
        for (int x : v) hashCombine(seed, x);
        return seed;
    }
};
 
// Usage
unordered_map<pair<int,int>, int, PairHash> memo;
unordered_map<vector<int>,   int, VectorHash> vecMap;

Approach 3: Encode Key as String (Simple but Slower)

// For pair<int,int>: encode as "a,b"
auto encode = [](int a, int b) {
    return to_string(a) + "," + to_string(b);
};
unordered_map<string, int> mp;
mp[encode(1, 2)] = 42;
 
// For small values, pack into long long
auto packLL = [](int a, int b) {
    return (long long)a * 1000000007LL + b;  // Assumes b < 1e9+7
};
unordered_map<long long, int> mp2;
mp2[packLL(1, 2)] = 42;

Approach 4: Use map with Custom Comparator (Avoids Hashing)

// When you need a pair/struct as a key and don't want to write a hash:
// Use map<pair<int,int>, int> — it uses operator< by default for pairs
map<pair<int,int>, int> pairMap;
pairMap[{1, 2}] = 42;   // O(log n) but always correct

6.8 Rolling Hash — Rabin-Karp Introduction

The Problem: Substring Search in O(n + m)

Naive substring search is O(n·m). Rolling hash enables O(n + m) by computing the hash of each window in O(1) from the previous window's hash.

Polynomial Rolling Hash

Hash of window s[i..i+k-1]:
  H = s[i]·B^(k-1) + s[i+1]·B^(k-2) + ... + s[i+k-1]·B^0   (mod M)

Slide window by one:
  Remove s[i]:  H = H - s[i]·B^(k-1)
  Shift left:   H = H · B
  Add s[i+k]:   H = H + s[i+k]
  All mod M.
// Rabin-Karp: find first occurrence of pattern in text
int rabinKarp(const string& text, const string& pattern) {
    int n = text.size(), m = pattern.size();
    if (m > n) return -1;
 
    const long long BASE = 31, MOD = 1e9 + 7;
 
    // Compute BASE^(m-1) mod MOD
    long long highPow = 1;
    for (int i = 0; i < m - 1; i++) highPow = highPow * BASE % MOD;
 
    // Hash of pattern
    long long patHash = 0;
    for (char c : pattern) patHash = (patHash * BASE + (c - 'a' + 1)) % MOD;
 
    // Hash of first window
    long long winHash = 0;
    for (int i = 0; i < m; i++)
        winHash = (winHash * BASE + (text[i] - 'a' + 1)) % MOD;
 
    // Slide window
    for (int i = 0; i <= n - m; i++) {
        if (winHash == patHash) {
            // Hash match — verify (handles false positives)
            if (text.substr(i, m) == pattern) return i;
        }
        if (i < n - m) {
            // Roll: remove text[i], add text[i+m]
            winHash = (winHash - (text[i] - 'a' + 1) * highPow % MOD + MOD) % MOD;
            winHash = (winHash * BASE + (text[i + m] - 'a' + 1)) % MOD;
        }
    }
    return -1;
}
// Time: O(n + m) average, O(n·m) worst case (hash collisions)
// Use double hashing (two different MODs) to make collisions astronomically rare

Application: Detect All Duplicate Substrings of Length K

// Find all substrings of length k that appear more than once
vector<string> findDuplicateSubstrings(const string& s, int k) {
    const long long BASE = 31, MOD = 1e9 + 7;
    long long highPow = 1;
    for (int i = 0; i < k - 1; i++) highPow = highPow * BASE % MOD;
 
    unordered_map<long long, vector<int>> hashToIdx;  // hash → starting indices
    long long h = 0;
    for (int i = 0; i < k; i++) h = (h * BASE + (s[i] - 'a' + 1)) % MOD;
    hashToIdx[h].push_back(0);
 
    for (int i = k; i < (int)s.size(); i++) {
        h = (h - (s[i-k] - 'a' + 1) * highPow % MOD + MOD) % MOD;
        h = (h * BASE + (s[i] - 'a' + 1)) % MOD;
        hashToIdx[h].push_back(i - k + 1);
    }
 
    unordered_set<string> seen;
    vector<string> result;
    for (auto& [hash, indices] : hashToIdx) {
        if (indices.size() > 1) {
            string sub = s.substr(indices[0], k);
            if (!seen.count(sub)) {
                seen.insert(sub);
                result.push_back(sub);
            }
        }
    }
    return result;
}
// Time: O(n), Space: O(n)

Application: Repeated DNA Sequences (LC 187)

// Find all 10-letter DNA sequences that appear more than once
vector<string> findRepeatedDnaSequences(string s) {
    if (s.size() <= 10) return {};
 
    unordered_map<string, int> seen;
    vector<string> result;
 
    for (int i = 0; i + 10 <= (int)s.size(); i++) {
        string sub = s.substr(i, 10);
        if (++seen[sub] == 2) result.push_back(sub);
    }
    return result;
}
// Time: O(n · 10) = O(n), Space: O(n)
 
// Optimized: encode each letter as 2 bits (A=0, C=1, G=2, T=3)
// Entire 10-letter window fits in a 20-bit integer
vector<string> findRepeatedDnaSequences_Optimized(string s) {
    unordered_map<char,int> enc = {{'A',0},{'C',1},{'G',2},{'T',3}};
    unordered_map<int,int> seen;
    vector<string> result;
    int n = s.size();
    int mask = (1 << 20) - 1;  // 20-bit mask
 
    int hash = 0;
    for (int i = 0; i < 10; i++) hash = (hash << 2) | enc[s[i]];
    seen[hash]++;
 
    for (int i = 10; i < n; i++) {
        hash = ((hash << 2) | enc[s[i]]) & mask;  // Slide window
        if (++seen[hash] == 2) result.push_back(s.substr(i - 9, 10));
    }
    return result;
}
// Time: O(n), Space: O(n)

6.9 Sample Problems


Problem 1 — Two Sum (Easy)

LeetCode 1

vector<int> twoSum(vector<int>& nums, int target) {
    unordered_map<int,int> seen;
    for (int i = 0; i < (int)nums.size(); i++) {
        int comp = target - nums[i];
        if (seen.count(comp)) return {seen[comp], i};
        seen[nums[i]] = i;
    }
    return {};
}
// Time: O(n), Space: O(n)
// Key: insert AFTER lookup to avoid using same element twice when nums[i]*2 = target

Problem 2 — First Unique Character in a String (Easy)

LeetCode 387

int firstUniqChar(string s) {
    int freq[26] = {};
    for (char c : s) freq[c-'a']++;
    for (int i = 0; i < (int)s.size(); i++)
        if (freq[s[i]-'a'] == 1) return i;
    return -1;
}
// Time: O(n), Space: O(1)

Problem 3 — Isomorphic Strings (Easy)

LeetCode 205

bool isIsomorphic(string s, string t) {
    int sMap[128] = {}, tMap[128] = {};
    for (int i = 0; i < (int)s.size(); i++) {
        if (sMap[(int)s[i]] != tMap[(int)t[i]]) return false;
        sMap[(int)s[i]] = tMap[(int)t[i]] = i + 1;  // i+1 so 0 means "unseen"
    }
    return true;
}
// Time: O(n), Space: O(1) — two fixed 128-element arrays

Problem 4 — Group Anagrams (Medium)

LeetCode 49

vector<vector<string>> groupAnagrams(vector<string>& strs) {
    unordered_map<string, vector<string>> groups;
    for (const string& s : strs) {
        string key = s;
        sort(key.begin(), key.end());
        groups[key].push_back(s);
    }
    vector<vector<string>> result;
    for (auto& [key, grp] : groups) result.push_back(grp);
    return result;
}
// Time: O(n·k log k), Space: O(n·k)  — n strings, max length k

Problem 5 — Top K Frequent Elements (Medium)

LeetCode 347

vector<int> topKFrequent(vector<int>& nums, int k) {
    unordered_map<int,int> cnt;
    for (int x : nums) cnt[x]++;
 
    // Bucket sort: bucket[freq] = list of values with that frequency
    int n = nums.size();
    vector<vector<int>> buckets(n + 1);
    for (auto& [val, freq] : cnt) buckets[freq].push_back(val);
 
    vector<int> result;
    for (int f = n; f >= 1 && (int)result.size() < k; f--)
        for (int val : buckets[f]) {
            result.push_back(val);
            if ((int)result.size() == k) return result;
        }
    return result;
}
// Time: O(n), Space: O(n)

Problem 6 — Longest Consecutive Sequence (Medium)

LeetCode 128

Find the length of the longest consecutive sequence (unsorted input, O(n) required).

Key insight: Only start counting from the beginning of a sequence (where num - 1 does NOT exist in the set). This avoids redundant work.

int longestConsecutive(vector<int>& nums) {
    unordered_set<int> numSet(nums.begin(), nums.end());
    int maxLen = 0;
 
    for (int num : numSet) {
        // Only start a sequence from its first element
        if (!numSet.count(num - 1)) {
            int curr = num, len = 1;
            while (numSet.count(curr + 1)) { curr++; len++; }
            maxLen = max(maxLen, len);
        }
    }
    return maxLen;
}
// Time: O(n) amortized — each number is visited at most twice
// Space: O(n)
 
/*
  nums = [100, 4, 200, 1, 3, 2]
  numSet = {100, 4, 200, 1, 3, 2}
 
  num=100: no 99 in set → start sequence: 100 → length 1
  num=4:   3 is in set  → NOT a start, skip
  num=200: no 199       → start: 200 → length 1
  num=1:   no 0         → start: 1→2→3→4 → length 4  ✅
  num=3:   2 is in set  → skip
  num=2:   1 is in set  → skip
  Answer: 4
*/

Problem 7 — Subarray Sum Equals K (Medium)

LeetCode 560

int subarraySum(vector<int>& nums, int k) {
    unordered_map<int,int> prefixCnt;
    prefixCnt[0] = 1;
    int prefixSum = 0, count = 0;
    for (int x : nums) {
        prefixSum += x;
        count += prefixCnt[prefixSum - k];
        prefixCnt[prefixSum]++;
    }
    return count;
}
// Time: O(n), Space: O(n)

Problem 8 — Contiguous Array (Medium)

LeetCode 525

Find the max length subarray with equal number of 0s and 1s.

int findMaxLength(vector<int>& nums) {
    unordered_map<int,int> firstSeen;
    firstSeen[0] = -1;
    int balance = 0, maxLen = 0;
 
    for (int i = 0; i < (int)nums.size(); i++) {
        balance += (nums[i] == 1) ? 1 : -1;
        if (firstSeen.count(balance))
            maxLen = max(maxLen, i - firstSeen[balance]);
        else
            firstSeen[balance] = i;
    }
    return maxLen;
}
// Time: O(n), Space: O(n)
// Treat 0 as -1: equal 0s and 1s ↔ subarray sum = 0 ↔ same prefix balance

Problem 9 — 4Sum II (Medium)

LeetCode 454

int fourSumCount(vector<int>& A, vector<int>& B,
                 vector<int>& C, vector<int>& D) {
    unordered_map<int,int> abSum;
    for (int a : A) for (int b : B) abSum[a + b]++;
    int count = 0;
    for (int c : C) for (int d : D) count += abSum[-(c + d)];
    return count;
}
// Time: O(n²), Space: O(n²)  — n = array length (all same size)

Problem 10 — Repeated DNA Sequences (Medium)

LeetCode 187

vector<string> findRepeatedDnaSequences(string s) {
    unordered_map<string,int> cnt;
    vector<string> result;
    for (int i = 0; i + 10 <= (int)s.size(); i++) {
        string sub = s.substr(i, 10);
        if (++cnt[sub] == 2) result.push_back(sub);
    }
    return result;
}
// Time: O(n), Space: O(n)

Problem 11 — Longest Palindrome from Characters (Easy)

LeetCode 409

Given a string, find the length of the longest palindrome you can build.

int longestPalindrome(string s) {
    int freq[128] = {};
    for (char c : s) freq[(int)c]++;
 
    int length = 0;
    bool hasOdd = false;
    for (int f : freq) {
        length += f / 2 * 2;  // Use as many pairs as possible
        if (f % 2 == 1) hasOdd = true;
    }
    return length + (hasOdd ? 1 : 0);  // One odd-freq char can be in the center
}
// Time: O(n), Space: O(1)

Problem 12 — Minimum Number of Pushes to Type Word II (Medium)

LeetCode 3016 (Google interview favorite)

Reassign 26 letters to 8 keys on a phone keyboard. Find the minimum total key presses to type a word.

int minimumPushes(string word) {
    int freq[26] = {};
    for (char c : word) freq[c-'a']++;
 
    // Sort frequencies in descending order
    vector<int> freqs(freq, freq+26);
    sort(freqs.rbegin(), freqs.rend());
 
    int totalPresses = 0;
    for (int i = 0; i < 26; i++) {
        if (freqs[i] == 0) break;
        // Letters assigned to positions 1-8, 9-16, 17-24 require 1,2,3 presses
        int presses = i / 8 + 1;
        totalPresses += freqs[i] * presses;
    }
    return totalPresses;
}
// Time: O(n + 26 log 26) = O(n), Space: O(1)

6.10 LeetCode Problem Set

# Problem Difficulty Core Technique Link
1 Two Sum Easy Complement lookup LC 1
2 First Unique Character Easy Frequency array LC 387
3 Group Anagrams Medium Sorted key hashing LC 49
4 Top K Frequent Elements Medium Freq map + bucket sort LC 347
5 Longest Consecutive Sequence Medium Hash set + sequence start LC 128
6 Subarray Sum Equals K Medium Prefix sum + hash map LC 560
7 Substring with Concatenation of All Words Hard Fixed window + hash map LC 30
8 Longest Duplicate Substring Hard Rolling hash + binary search LC 1044

Suggested order: Master #1 (Two Sum) then immediately generalize to the prefix hash pattern (#6). Then #3 and #4 (grouping problems). #5 (Longest Consecutive) is the trickiest medium — the "only start from sequence begins" insight is non-obvious. #7 and #8 are hard — attempt after fully completing all earlier chapters.


← PreviousChapter 5: Two Pointers & Sliding WindowNext →Chapter 7: Linked Lists
Chapter 6 — Progress
0 / 32 COMPLETE
Hash Function Internals
  • Explain what a hash function does and name the four properties of a good one
  • Know that `m = prime` reduces clustering; name the two primes used in competitive programming
  • Know the polynomial rolling hash formula for strings
  • State the Pigeonhole Principle and why collisions are unavoidable
Collision Handling
  • Explain separate chaining (linked list at each bucket)
  • Explain open addressing and linear probing
  • Define load factor and when rehashing triggers
  • Know that `unordered_map` uses separate chaining
  • Use `reserve(n)` before bulk-inserting n elements
unordered_map Usage
  • Know that `operator[]` creates a default entry if key is absent — use `find()` for safe lookup
  • Know when to use `map` vs `unordered_map` (sorted order / adversarial input → map)
  • Use `count()` or `contains()` (C++20) to check existence without inserting
Frequency Counting
  • Implement Top K Frequent Elements using bucket sort in O(n)
  • Implement Top K Frequent Elements using min-heap in O(n log k)
  • Implement First Non-Repeating Character in O(n) with O(1) space
  • Implement Majority Element using Boyer-Moore Voting in O(n) O(1) space
Two Sum & Prefix Hash
  • Implement Two Sum with hash map, explaining why lookup precedes insert
  • State the prefix hash formula: `count += map[prefixSum - k]`
  • Implement Subarray Sum Equals K from memory
  • Implement Contiguous Array (equal 0s and 1s) using balance trick
  • Apply prefix XOR: `prefixXOR ^ k` as the lookup key
Anagram & Isomorphism
  • Implement Group Anagrams using sorted string as key
  • Implement Isomorphic Strings using two-directional maps
  • Know both anagram signature representations: sorted string vs freq array
Custom Hashing
  • Write a `PairHash` struct using hash combine for `pair<int,int>` keys
  • Know the hash combine formula: `seed ^= hash(v) + 0x9e3779b9 + (seed<<6) + (seed>>2)`
  • Know the simple alternative: pack two ints into one `long long`
  • Know when to use `map<pair<int,int>, V>` instead (avoids custom hash)
Rolling Hash
  • State the polynomial hash formula and the window-slide update formula
  • Implement Rabin-Karp string matching with the rolling hash
  • Know why you verify on hash match (false positives exist)
  • Use bit-packing (2 bits per DNA letter) as an alternative to string hashing
Notes▸