Hash Tables: Functions, Collisions, Load Factor
Hash Table — function, collision
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.
O(1) সময়ে। মূল কৌশল হলো একটি hash function h(k), যা যেকোনো key (string, integer যেকোনো কিছু) কে একটি নির্দিষ্ট সাইজের array-এর index-এ রূপান্তর করে। দুটি ভিন্ন key যদি একই slot-এ পড়ে যায়, সেটিকে collision বলে — এবং সেটি সমাধানের দুটি প্রধান পরিবার আছে: separate chaining ও open addressing।
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:
| Method | Formula | Notes |
|---|---|---|
| Division | h(k) = k mod m | Pick m as a prime far from a power of 2. |
| Multiplication | h(k) = floor(m * (k*A mod 1)) | A ≈ (√5 − 1)/2 (Knuth's constant). |
| Universal | h(k) = ((a*k + b) mod p) mod m | Random a, b from a family — defeats adversarial inputs. |
a, b বেছে নেওয়া হয়, ফলে adversary বুঝতে পারে না কোন key গুলো সব collision তৈরি করবে।
3. Two Worlds — Chaining vs Open Addressing
4. Probing Strategies in Open Addressing
| Probe | i-th try | Pros / Cons |
|---|---|---|
| Linear | (h(k) + i) mod m | Cache-friendly; suffers from primary clustering. |
| Quadratic | (h(k) + c1·i + c2·i²) mod m | Breaks primary clustering; secondary clustering remains. |
| Double Hashing | (h1(k) + i·h2(k)) mod m | Best distribution; needs h2(k) coprime to 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).
#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;
}
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.
#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;
}
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.
-
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; } } -
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"; } } -
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"; } -
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"; } -
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"; } -
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"; } -
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.