Hash Tables: Functions, Collisions, Load Factor

Hash Table — function, collision

Read: ~35 min Intermediate 7 practice problems Live code runner

1. The Big Idea — Constant-Time Lookup

A hash table answers the question: "Given a key, where does the value live?" — in expected O(1) time. The trick is a hash function h(k) that turns a key (a string, integer, anything) into an index into a fixed-size array. If two different keys land on the same slot, we have a collision, and we resolve it using one of two families: separate chaining or open addressing.

একটি hash table এই প্রশ্নের উত্তর দেয়: "একটি key দিলে তার value কোথায় আছে?" — গড়ে O(1) সময়ে। মূল কৌশল হলো একটি hash function h(k), যা যেকোনো key (string, integer যেকোনো কিছু) কে একটি নির্দিষ্ট সাইজের array-এর index-এ রূপান্তর করে। দুটি ভিন্ন key যদি একই slot-এ পড়ে যায়, সেটিকে collision বলে — এবং সেটি সমাধানের দুটি প্রধান পরিবার আছে: separate chaining ও open addressing।
Key insight একটি দুর্বল hash function performance-কে O(1) থেকে O(n)-এ নামিয়ে দেয়।

A weak hash function quietly drags performance from O(1) all the way down to O(n) — the same as a linear scan.

2. Designing a Hash Function

A good hash function is fast, deterministic, and spreads keys uniformly across slots. Three classical recipes:

MethodFormulaNotes
Divisionh(k) = k mod mPick m as a prime far from a power of 2.
Multiplicationh(k) = floor(m * (k*A mod 1))A ≈ (√5 − 1)/2 (Knuth's constant).
Universalh(k) = ((a*k + b) mod p) mod mRandom a, b from a family — defeats adversarial inputs.
একটি ভালো hash function দ্রুত, deterministic এবং সব slot-এ key গুলো সমানভাবে ছড়িয়ে দেয়। সবচেয়ে নিরাপদ পদ্ধতি হলো universal hashing — প্রতি বার random a, b বেছে নেওয়া হয়, ফলে adversary বুঝতে পারে না কোন key গুলো সব collision তৈরি করবে।
Warning — adversarial inputs ২০১১ সালে hash-flooding attack-এ কেউ কেউ ইচ্ছাকৃতভাবে কোটি কোটি collision তৈরি করে web server-কে বসিয়ে দিয়েছিল। সেই থেকে Python, Java সবাই default-এ randomized hash seed ব্যবহার করে।

3. Two Worlds — Chaining vs Open Addressing

Separate Chaining [0] [1] → [2] → [3] [4] → "cat" "act" "dhk" "bd" "db" "abd" Each slot points to a linked list of all keys that hash there. Open Addressing (Linear Probe) "act"[0] "cat"[1] — empty —[2] "bd"[3] "db"[4] "abd"[5] On collision, scan forward to the next free slot. Hash function: h("cat") = h("act") = 1 → collision! Both keys want slot 1; chaining stores both, open addressing pushes "act" forward. Figure 21.1 — Separate chaining বনাম open addressing-এর তুলনামূলক গঠন।
Separate chaining: প্রতিটি slot আসলে একটি linked list — collision হলে নতুন key সেই list-এর শেষে যোগ হয়। Open addressing: পুরো hash table নিজেই array — collision হলে একটি নির্দিষ্ট নিয়মে পরের খালি slot খুঁজে বের করে সেখানে রেখে দেয়।

4. Probing Strategies in Open Addressing

Probei-th tryPros / Cons
Linear(h(k) + i) mod mCache-friendly; suffers from primary clustering.
Quadratic(h(k) + c1·i + c2·i²) mod mBreaks primary clustering; secondary clustering remains.
Double Hashing(h1(k) + i·h2(k)) mod mBest distribution; needs h2(k) coprime to m.
Load factor α = n / m
Chaining stays usable up to α ≈ 1; open addressing must rehash before α reaches ~0.7, otherwise probe length explodes.

Chaining-এ α ≈ 1 পর্যন্ত ভালো চলে; কিন্তু open addressing-এ α 0.7-এর বেশি হলে probe লম্বা হতে শুরু করে — তখন rehash করতেই হয়।

5. Live Code — Hash Map with Separate Chaining

We build a HashMap<string,int> from scratch using a vector of linked lists. String hash uses the polynomial rolling method (base 31, the same constant used by Java's String.hashCode).

chained_hashmap.cpp
#include <bits/stdc++.h>
using namespace std;

// HashMap<string,int> using separate chaining.
class HashMap {
    struct Node { string key; int val; };
    vector<list<Node>> tbl;
    int n = 0;

    size_t hashKey(const string& s) const {
        size_t h = 0;
        for (char c : s) h = h * 31 + (unsigned char)c;
        return h % tbl.size();
    }
    void rehash() {
        vector<list<Node>> old = move(tbl);
        tbl.assign(old.size() * 2, {});
        n = 0;
        for (auto& bucket : old)
            for (auto& node : bucket) put(node.key, node.val);
    }
public:
    HashMap(int cap = 8) : tbl(cap) {}

    void put(const string& k, int v) {
        auto& b = tbl[hashKey(k)];
        for (auto& nd : b) if (nd.key == k) { nd.val = v; return; }
        b.push_back({k, v});
        ++n;
        if ((double)n / tbl.size() > 0.75) rehash();
    }
    int get(const string& k, int def = -1) {
        for (auto& nd : tbl[hashKey(k)])
            if (nd.key == k) return nd.val;
        return def;
    }
    bool erase(const string& k) {
        auto& b = tbl[hashKey(k)];
        for (auto it = b.begin(); it != b.end(); ++it)
            if (it->key == k) { b.erase(it); --n; return true; }
        return false;
    }
    int size() const { return n; }
};

int main() {
    HashMap mp;
    mp.put("Dhaka", 9);
    mp.put("Chittagong", 5);
    mp.put("Sylhet", 3);
    mp.put("Dhaka", 10); // overwrite

    cout << "Dhaka -> " << mp.get("Dhaka") << "\n";
    cout << "Khulna -> " << mp.get("Khulna") << "\n";
    mp.erase("Sylhet");
    cout << "size = " << mp.size() << "\n";
    return 0;
}
উপরের কোডে আমরা polynomial rolling hash ব্যবহার করেছি (h = h*31 + c) — Java-র String.hashCode-ও ঠিক একই formula ব্যবহার করে। load factor 0.75 ছাড়িয়ে গেলে আমরা table-এর size দ্বিগুণ করে rehash করছি।

6. Live Code — Crashing unordered_map with Collisions

C++'s std::unordered_map<long long,int> uses a fixed integer hash. By inserting only multiples of its bucket count, we force every key into the same bucket — turning O(1) into O(n) per operation. This is literally how the 2011 hash-flooding DoS attack worked.

collision_attack.cpp
#include <bits/stdc++.h>
using namespace std;
using ll = long long;

int main() {
    unordered_map<ll, int> mp;
    const int N = 2000;
    ll step = mp.bucket_count();   // every multiple lands in bucket 0

    auto t1 = chrono::high_resolution_clock::now();
    for (int i = 1; i <= N; ++i) mp[i * step] = i;
    auto t2 = chrono::high_resolution_clock::now();

    cout << "Inserted " << N << " colliding keys.\n";
    cout << "Bucket count = " << mp.bucket_count() << "\n";
    cout << "Largest bucket size = ";
    size_t mx = 0;
    for (size_t b = 0; b < mp.bucket_count(); ++b)
        mx = max(mx, mp.bucket_size(b));
    cout << mx << "\n";
    cout << "Time: "
         << chrono::duration<double, milli>(t2 - t1).count()
         << " ms\n";
    return 0;
}
Defense — Competitive programmers wrap their hash with a random seed: auto h = [](ll x){ static uint64_t r = chrono::steady_clock::now().time_since_epoch().count(); x ^= r; x = (x ^ (x >> 30)) * 0xbf58476d1ce4e5b9ULL; return x; };

7. Cheat Sheet — Complexity

Average case (গড়)

  • Insert: O(1)
  • Lookup: O(1)
  • Delete: O(1)

Worst case (সবথেকে খারাপ)

  • All collisions → O(n) per op
  • Open addressing rehash → O(n) amortized
  • Adversarial keys → DoS

8. Practice Problems

Each problem ships with a runnable C++ answer. Try first, peek later.

প্রতিটি প্রশ্নের সাথে সরাসরি চালানো-যোগ্য C++ কোড দেওয়া আছে। আগে নিজে চেষ্টা করুন, তারপর দেখুন।
  1. Two-Sum: given an array and a target, return indices of two numbers that add up to the target — in O(n).
    একটি array ও target দেওয়া আছে; এমন দুটি index ফেরত দিন যাদের যোগফল target — O(n) সময়ে।
    Show Answer (উত্তর দেখুন)
    two_sum.cpp
    #include <bits/stdc++.h>
    using namespace std;
    int main() {
        vector<int> a = {2, 7, 11, 15};
        int target = 9;
        unordered_map<int, int> seen;
        for (int i = 0; i < (int)a.size(); ++i) {
            int need = target - a[i];
            if (seen.count(need)) {
                cout << seen[need] << " " << i << "\n";
                return 0;
            }
            seen[a[i]] = i;
        }
    }
  2. Group Anagrams: cluster strings whose letters are permutations of each other.
    যেসব string-এর অক্ষরগুলো একে অপরের permutation, সেগুলো একসাথে গ্রুপ করুন।
    Show Answer
    anagrams.cpp
    #include <bits/stdc++.h>
    using namespace std;
    int main() {
        vector<string> v = {"eat","tea","tan","ate","nat","bat"};
        unordered_map<string, vector<string>> mp;
        for (auto& s : v) {
            string k = s;
            sort(k.begin(), k.end());
            mp[k].push_back(s);
        }
        for (auto& [k, g] : mp) {
            for (auto& w : g) cout << w << " ";
            cout << "\n";
        }
    }
  3. Longest substring without repeating characters — sliding window + hash map.
    পুনরাবৃত্তি ছাড়া সবচেয়ে দীর্ঘ substring বের করুন।
    Show Answer
    longest_uniq.cpp
    #include <bits/stdc++.h>
    using namespace std;
    int main() {
        string s = "abcabcbb";
        unordered_map<char, int> last;
        int best = 0, l = 0;
        for (int r = 0; r < (int)s.size(); ++r) {
            if (last.count(s[r]) && last[s[r]] >= l) l = last[s[r]] + 1;
            last[s[r]] = r;
            best = max(best, r - l + 1);
        }
        cout << best << "\n";
    }
  4. Number of contiguous subarrays whose sum equals K.
    এমন কয়টি contiguous subarray আছে যাদের যোগফল K-এর সমান?
    Show Answer
    subarray_sum_k.cpp
    #include <bits/stdc++.h>
    using namespace std;
    int main() {
        vector<int> a = {1,2,3,-2,5};
        int K = 5, sum = 0, ans = 0;
        unordered_map<int, int> cnt; cnt[0] = 1;
        for (int x : a) {
            sum += x;
            ans += cnt[sum - K];
            cnt[sum]++;
        }
        cout << ans << "\n";
    }
  5. First non-repeating character in a stream of characters — print after each insertion.
    একটি character stream-এ প্রতিটি character যোগের পর প্রথম non-repeating character প্রিন্ট করুন।
    Show Answer
    first_unique.cpp
    #include <bits/stdc++.h>
    using namespace std;
    int main() {
        string s = "aabcbd";
        unordered_map<char, int> cnt;
        list<char> q;
        unordered_map<char, list<char>::iterator> pos;
        for (char c : s) {
            if (++cnt[c] == 1) { q.push_back(c); pos[c] = prev(q.end()); }
            else if (pos.count(c)) { q.erase(pos[c]); pos.erase(c); }
            cout << (q.empty() ? '#' : q.front()) << " ";
        }
        cout << "\n";
    }
  6. Design a HashSet<int> using only an array of vectors. Support add / remove / contains.
    শুধু array of vectors দিয়ে একটি HashSet<int> ডিজাইন করুন — add, remove, contains সাপোর্ট সহ।
    Show Answer
    my_hashset.cpp
    #include <bits/stdc++.h>
    using namespace std;
    class MySet {
        vector<vector<int>> b;
        int M = 1009;
    public:
        MySet() : b(M) {}
        void add(int k) { if (!contains(k)) b[k % M].push_back(k); }
        void remove(int k) {
            auto& v = b[k % M];
            v.erase(remove(v.begin(), v.end(), k), v.end());
        }
        bool contains(int k) {
            for (int x : b[k % M]) if (x == k) return true;
            return false;
        }
    };
    int main() {
        MySet s;
        s.add(5); s.add(1014); // 1014 % 1009 == 5 → collision
        cout << s.contains(5) << " " << s.contains(1014) << " " << s.contains(42) << "\n";
        s.remove(5);
        cout << s.contains(5) << "\n";
    }
  7. LRU Cache — implement get / put in O(1) using hash map + doubly linked list.
    hash map ও doubly linked list ব্যবহার করে O(1) সময়ে get / put-সহ একটি LRU cache বানান।
    Show Answer
    lru.cpp
    #include <bits/stdc++.h>
    using namespace std;
    class LRU {
        int cap;
        list<pair<int, int>> dq;
        unordered_map<int, list<pair<int, int>>::iterator> mp;
    public:
        LRU(int c) : cap(c) {}
        int get(int k) {
            if (!mp.count(k)) return -1;
            dq.splice(dq.begin(), dq, mp[k]);
            return mp[k]->second;
        }
        void put(int k, int v) {
            if (mp.count(k)) { mp[k]->second = v; dq.splice(dq.begin(), dq, mp[k]); return; }
            if ((int)dq.size() == cap) { mp.erase(dq.back().first); dq.pop_back(); }
            dq.push_front({k, v});
            mp[k] = dq.begin();
        }
    };
    int main() {
        LRU c(2);
        c.put(1,10); c.put(2,20);
        cout << c.get(1) << "\n";
        c.put(3,30); // evicts 2
        cout << c.get(2) << "\n";
    }

Summary — Module 21

A hash table buys you average O(1) operations by trading a clever hash function against a collision strategy. Choose chaining for simplicity and dynamic load; choose open addressing for cache friendliness; rehash before the load factor hurts you; and randomize your hash to defeat adversarial keys.

Hash table গড়ে O(1) সময়ে কাজ করে — তবে দুটি জিনিস ঠিক রাখতেই হবে: একটি ভালো hash function, এবং একটি দ্রুত collision-resolution কৌশল। load factor সীমা ছাড়ালে rehash করুন, এবং production-এ randomized seed ব্যবহার করুন।

Next Module → Disjoint Set Union (Union-Find) — গ্রাফের connected component, Kruskal এবং offline query সমস্যা সমাধানে DSU।