Loading W Code...
Master the art of O(1) lookups - Hash functions, collisions, and interview problems.
| Operation | Average | Worst Case | When Best? |
|---|---|---|---|
| Search | O(1) | O(n) | Low load factor |
| Insert | O(1) | O(n) | Good hash function |
| Delete | O(1) | O(n) | Few collisions |
| Space | O(n) | ||
Imagine checking your bags at a hotel. Instead of searching through every single bag in the storage room when you return, the clerk hands you a claim ticket with a number. The clerk goes directly to shelf compartment number 5 to retrieve your bag instantly.
Hashing is a technique that maps keys (like names or student IDs) to specific indices in a table using a mathematical Hash Function.
O(1) for insertions, deletions, and lookups.O(n)) or binary search (O(log n)), hashing accesses records directly without scanning through elements.#include <iostream>
#include <unordered_map>
using namespace std;
int main() {
// Creating a hash table (unordered_map in C++)
unordered_map<string, int> studentMarks;
// Insert: O(1) average
studentMarks["Rahul"] = 85;
studentMarks["Priya"] = 92;
studentMarks["Amit"] = 78;
// Lookup: O(1) average
cout << "Priya's marks: " << studentMarks["Priya"] << endl; // 92
// Check if key exists
if (studentMarks.count("Rahul")) {
cout << "Rahul found!" << endl;
}
// Delete: O(1) average
studentMarks.erase("Amit");
return 0;
}h(key) = key % 7Imagine filing documents in drawers labeled 0 through 9. To file a document with serial number 148, you look at the last digit and place it directly into drawer 8.
A Hash Function takes an input key and maps it to a valid integer index within the boundaries of the hash table.
The most common approach is the division method:
h(key) = key % m
where m is the size of the table (ideally a prime number to reduce collisions).
O(1) time.#include <iostream>
using namespace std;
class SimpleHashTable {
private:
int tableSize;
public:
SimpleHashTable(int size) : tableSize(size) {}
// Division method hash function
int hash(int key) {
return key % tableSize;
}
// For string keys - sum of ASCII values
int hashString(string key) {
int sum = 0;
for (char c : key) {
sum += c; // Add ASCII value
}
return sum % tableSize;
}
};Imagine checking your coat in a locker room. If multiple coats are assigned to the same locker number, you hook them one after another on a chain hangers inside that single locker.
Since the set of possible keys is larger than the hash table size, different keys will eventually hash to the same index. This event is called a Collision.
Each slot in the hash table points to a Linked List (or dynamic array) of all elements that hash to that same index.
O(n) in the worst-case if all elements hash to the same slot.#include <iostream>
#include <vector>
#include <list>
using namespace std;
class HashTableChaining {
private:
int size;
vector<list<int>> table;
int hash(int key) { return key % size; }
public:
HashTableChaining(int s) : size(s), table(s) {}
void insert(int key) {
int idx = hash(key);
table[idx].push_back(key);
}
};Imagine trying to park your car in spot number 12. If you find it occupied, you drive down the row and park in the very next open spot you see (e.g., spot 13, 14, and so on).
In Open Addressing, all element records are stored directly inside the hash table array itself. If a collision occurs, we probe the array using a systematic sequence to find the next empty slot.
h(k, i) = (h'(k) + i) % mh(k, i) = (h'(k) + c1 * i + c2 * i²) % mh(k, i) = (h1(k) + i * h2(k)) % m#include <iostream>
#include <vector>
using namespace std;
class HashTableOpenAddressing {
private:
int size;
vector<int> table;
vector<bool> occupied;
int hash(int key) {
return key % size;
}
public:
HashTableOpenAddressing(int s) : size(s), table(s, -1), occupied(s, false) {}
// Insert with linear probing
void insert(int key) {
int index = hash(key);
int originalIndex = index;
// Linear probing
while (occupied[index]) {
cout << "Collision at " << index << ", probing..." << endl;
index = (index + 1) % size; // Move to next slot
if (index == originalIndex) {
cout << "Table is full!" << endl;
return;
}
}
table[index] = key;
occupied[index] = true;
cout << "Inserted " << key << " at index " << index << endl;
}
// Search with linear probing
int search(int key) {
int index = hash(key);
int originalIndex = index;
while (occupied[index]) {
if (table[index] == key) {
return index; // Found!
}
index = (index + 1) % size;
if (index == originalIndex) break;
}
return -1; // Not found
}
void display() {
cout << "Hash Table: ";
for (int i = 0; i < size; i++) {
if (occupied[i]) {
cout << "[" << i << "]:" << table[i] << " ";
} else {
cout << "[" << i << "]:_ ";
}
}
cout << endl;
}
};
int main() {
HashTableOpenAddressing ht(7);
ht.insert(10); // 10 % 7 = 3
ht.insert(17); // 17 % 7 = 3 → collision → goes to 4
ht.insert(24); // 24 % 7 = 3 → collision → goes to 5
ht.insert(31); // 31 % 7 = 3 → collision → goes to 6
cout << "\n";
ht.display();
cout << "\nSearch 17: index " << ht.search(17) << endl; // 4
return 0;
}Imagine living in a studio apartment. When the apartment gets 75% full of items and furniture, finding anything becomes slow and difficult. To fix this, you move to a larger apartment and rearrange your belongings there.
The Load Factor (α) measures how full a hash table is:
α = (Number of Elements) / (Table Size)
As α approaches 1.0, collision rates increase and performance degrades.
To maintain fast lookups, we monitor α and resize the table once it exceeds a threshold (typically 0.7 or 0.75):
2 * m).While rehashing takes O(n) time, it happens infrequently enough that the cost is spread out, keeping the amortized insertion time at O(1).
#include <iostream>
#include <vector>
#include <list>
using namespace std;
class HashTableWithRehash {
private:
int size;
int count;
double maxLoadFactor;
vector<list<int>> table;
int hash(int key) {
return key % size;
}
// Rehash to a bigger table
void rehash() {
cout << "\n⚡ REHASHING: Load factor exceeded!" << endl;
cout << "Old size: " << size << ", Elements: " << count << endl;
int oldSize = size;
vector<list<int>> oldTable = table;
// Double the size (or use next prime)
size = size * 2;
table = vector<list<int>>(size);
count = 0;
// Re-insert all elements
for (int i = 0; i < oldSize; i++) {
for (int key : oldTable[i]) {
insert(key); // Re-hash into new table
}
}
cout << "New size: " << size << endl << endl;
}
public:
HashTableWithRehash(int s = 7, double mlf = 0.7)
: size(s), count(0), maxLoadFactor(mlf), table(s) {}
void insert(int key) {
int index = hash(key);
table[index].push_back(key);
count++;
// Check load factor
double loadFactor = (double)count / size;
cout << "Inserted " << key << " | Load Factor: " << loadFactor << endl;
if (loadFactor > maxLoadFactor) {
rehash();
}
}
double getLoadFactor() {
return (double)count / size;
}
};
int main() {
HashTableWithRehash ht(7, 0.7); // Size 7, max load 0.7
// Insert elements until rehash triggers
ht.insert(10);
ht.insert(20);
ht.insert(30);
ht.insert(40);
ht.insert(50); // This might trigger rehash!
ht.insert(60);
return 0;
}C++ provides two main hash-based containers in the standard library:
std::unordered_map<Key, Value> (HashMap)std::unordered_set<Key> (HashSet)| Feature | unordered_map | unordered_set |
|---|---|---|
| Stores | Key-Value pairs | Only Keys |
| Duplicates | No duplicate keys | No duplicates |
| Access | map[key] | — |
| Insert | map[k] = v | set.insert(k) |
| Check | map.count(k) | set.count(k) |
| Use Case | Frequency, Lookup | Existence Check |
#include <iostream>
#include <unordered_map>
#include <unordered_set>
#include <vector>
using namespace std;
int main() {
// ========== UNORDERED_MAP ==========
cout << "=== unordered_map (HashMap) ===" << endl;
unordered_map<string, int> freq;
// Insert / Update
freq["apple"] = 5;
freq["banana"] = 3;
freq["apple"]++; // Now apple = 6
// Access
cout << "apple count: " << freq["apple"] << endl; // 6
// Check existence (SAFE way)
if (freq.count("mango") == 0) {
cout << "mango not found" << endl;
}
// Iterate
cout << "All items: ";
for (auto& p : freq) {
cout << p.first << ":" << p.second << " ";
}
cout << endl << endl;
// ========== UNORDERED_SET ==========
cout << "=== unordered_set (HashSet) ===" << endl;
unordered_set<int> seen;
// Insert
seen.insert(10);
seen.insert(20);
seen.insert(10); // Duplicate - ignored!
cout << "Size: " << seen.size() << endl; // 2 (not 3!)
// Check existence
if (seen.count(20)) {
cout << "20 exists!" << endl;
}
// Remove duplicates from array using set
vector<int> arr = {1, 2, 2, 3, 3, 3, 4};
unordered_set<int> unique(arr.begin(), arr.end());
cout << "Unique elements: ";
for (int x : unique) cout << x << " ";
cout << endl;
return 0;
}Given an array of integers and a target sum, find the indices of the two numbers that add up to the target.
Check every pair in the array. This takes O(n²) time.
O(n) Time):As you iterate through the array, check if the complement (target - current_value) is already in your hash map.
#include <iostream>
#include <vector>
#include <unordered_map>
using namespace std;
// Returns indices of two numbers that add up to target
vector<int> twoSum(vector<int>& nums, int target) {
unordered_map<int, int> seen; // value -> index
for (int i = 0; i < nums.size(); i++) {
int complement = target - nums[i];
// Check if complement was seen before
if (seen.count(complement)) {
return {seen[complement], i}; // Found!
}
// Store current number with its index
seen[nums[i]] = i;
}
return {}; // No solution found
}
// Variation: Just check if pair exists (no indices needed)
bool hasTwoSum(vector<int>& nums, int target) {
unordered_set<int> seen;
for (int x : nums) {
if (seen.count(target - x)) {
return true;
}
seen.insert(x);
}
return false;
}
int main() {
vector<int> arr = {2, 7, 11, 15};
int target = 9;
vector<int> result = twoSum(arr, target);
if (!result.empty()) {
cout << "Indices: [" << result[0] << ", " << result[1] << "]" << endl;
cout << "Values: " << arr[result[0]] << " + " << arr[result[1]]
<< " = " << target << endl;
} else {
cout << "No solution found" << endl;
}
return 0;
}Given a string, find the first character that does not repeat.
1.For lowercase English letters, a simple integer array of size 26 can replace the hash map, reducing memory overhead and lookup times.
#include <iostream>
#include <unordered_map>
#include <string>
using namespace std;
char firstNonRepeating(const string& s) {
// Step 1: Count frequency of each character
unordered_map<char, int> freq;
for (char c : s) {
freq[c]++;
}
// Step 2: Find first character with frequency 1
for (char c : s) {
if (freq[c] == 1) {
return c;
}
}
return '$'; // No non-repeating character
}
// Optimized: Using array instead of map (for lowercase only)
char firstNonRepeatingOptimized(const string& s) {
int freq[26] = {0}; // For 'a' to 'z'
// Count frequencies
for (char c : s) {
freq[c - 'a']++;
}
// Find first with count 1
for (char c : s) {
if (freq[c - 'a'] == 1) {
return c;
}
}
return '$';
}
int main() {
cout << firstNonRepeating("aabbccd") << endl; // d
cout << firstNonRepeating("leetcode") << endl; // l
cout << firstNonRepeating("aabbcc") << endl; // $
cout << firstNonRepeating("z") << endl; // z
return 0;
}Given an array of integers (which can include negative numbers) and a target value K, find the total number of continuous subarrays that sum up to K.
Using a naive nested loop takes O(n²) time. We can optimize this to O(n) using a running prefix sum combined with a hash map.
If the difference between two prefix sums equals K:
prefixSum[i] - prefixSum[j] = K
then the sum of elements from index j + 1 to i is exactly K.
currentSum).currentSum - K has been encountered. If it has, add its frequency count to your total.{0: 1} to handle subarrays starting at index 0.#include <iostream>
#include <vector>
#include <unordered_map>
using namespace std;
int subarraySum(vector<int>& nums, int k) {
unordered_map<int, int> prefixCount;
prefixCount[0] = 1; // Important! Empty prefix has sum 0
int currentSum = 0;
int count = 0;
for (int num : nums) {
currentSum += num; // Running prefix sum
// Check if (currentSum - k) was seen before
// If yes, those many subarrays end here with sum k
int need = currentSum - k;
if (prefixCount.count(need)) {
count += prefixCount[need];
}
// Add current prefix sum to map
prefixCount[currentSum]++;
}
return count;
}
// Returns true if ANY subarray sums to k (simpler version)
bool hasSubarraySum(vector<int>& nums, int k) {
unordered_set<int> prefixSums;
prefixSums.insert(0); // Empty prefix
int currentSum = 0;
for (int num : nums) {
currentSum += num;
if (prefixSums.count(currentSum - k)) {
return true;
}
prefixSums.insert(currentSum);
}
return false;
}
int main() {
vector<int> arr1 = {1, 1, 1};
cout << "Subarrays summing to 2: " << subarraySum(arr1, 2) << endl; // 2
vector<int> arr2 = {1, 2, 3, -3, 1, 1, 1, 4, 2, -3};
cout << "Subarrays summing to 3: " << subarraySum(arr2, 3) << endl; // 7
return 0;
}Given an array of strings, group all anagrams (words that contain the exact same characters in different orders) together.
All anagrams yield the exact same sorted string:
"eat" → "aet""tea" → "aet""ate" → "aet"We can use the sorted string as a key in a hash map (unordered_map<string, vector<string>>) to group strings together.
Instead of sorting each string (which takes O(k log k) time), we can count the frequency of each character (size 26) and convert this count array into a string key (taking O(k) time).
#include <iostream>
#include <vector>
#include <string>
#include <unordered_map>
#include <algorithm>
using namespace std;
vector<vector<string>> groupAnagrams(vector<string>& strs) {
unordered_map<string, vector<string>> groups;
for (string& s : strs) {
// Create key by sorting the string
string key = s;
sort(key.begin(), key.end());
// Add to the group with this key
groups[key].push_back(s);
}
// Collect all groups
vector<vector<string>> result;
for (auto& p : groups) {
result.push_back(p.second);
}
return result;
}
// Faster approach: Use character count as key (O(n*k) vs O(n*k*log(k)))
vector<vector<string>> groupAnagramsFast(vector<string>& strs) {
unordered_map<string, vector<string>> groups;
for (string& s : strs) {
// Create key from character counts
int count[26] = {0};
for (char c : s) {
count[c - 'a']++;
}
// Convert counts to string key
string key;
for (int i = 0; i < 26; i++) {
key += "#" + to_string(count[i]);
}
groups[key].push_back(s);
}
vector<vector<string>> result;
for (auto& p : groups) {
result.push_back(p.second);
}
return result;
}
int main() {
vector<string> words = {"eat", "tea", "tan", "ate", "nat", "bat"};
auto result = groupAnagrams(words);
cout << "Grouped Anagrams:" << endl;
for (auto& group : result) {
cout << "[ ";
for (string& s : group) {
cout << s << " ";
}
cout << "]" << endl;
}
return 0;
}Avoid these common mistakes when using hashing in coding interviews:
m = 10). If many keys share common factors (like ending in 0), they will all map to the same slots, causing frequent collisions.O(1) down to O(n). Keep α <= 0.7.-3 % 7 returns -3. To compute positive hash indices for negative keys, use:
int index = ((key % m) + m) % m;std::map is implemented as a Red-Black Tree (keeping elements sorted, with operations taking O(log n) time).std::unordered_map is a hash table (unsorted, with operations taking average O(1) time).myMap[key] automatically inserts an entry with a default value if the key does not exist. Use myMap.count(key) or myMap.find(key) to check for keys safely.#include <iostream>
#include <unordered_map>
using namespace std;
int main() {
// Using [] on non-existent key creates it!
unordered_map<string, int> mp;
// Creates a "ghost" entry with value 0
if (mp["ghost"] == 0) {
cout << "This creates the key!" << endl;
}
cout << "Size after check: " << mp.size() << endl; // 1!
mp.clear();
// SAFE: Use count() or find()
if (mp.count("ghost") == 0) {
cout << "Key does not exist (no side effect)" << endl;
}
cout << "Size after check: " << mp.size() << endl; // 0
// Negative modulo
int key = -15;
int m = 7;
// - Range of input values?
return 0;
}Hash Table: Maps keys to values via hash function → O(1) average
Hash Function: h(k) = k % m (m should be prime)
Collision: Resolve with chaining or open addressing
Load Factor: α = n/m, keep ≤ 0.7, rehash when exceeded
unordered_map: Key→Value pairs, use for frequency counting
unordered_set: Only keys, use for existence checking
Two Sum Pattern: Check if (target - x) seen before
Subarray Sum: Prefix sum + hash map