C++
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented C++
-
Section 4: Memory Management
-
Section 5: Templates and Generic Programming
-
Section 6: The Standard Template Library
-
Section 7: Modern C++ Features
-
Section 8: Error Handling
-
Section 9: Multithreading
-
Section 10: Operator Overloading and Type Conversion
-
Section 11: Advanced Topics
-
Section 12: Tooling and Build Systems
-
Section 13: Design Patterns in C++
-
Section 14: Interfacing with C and Systems Programming
-
Section 15: Networking and IPC Basics
-
Section 16: Graphics and Game Programming Basics
-
Section 17: The Boost Libraries
-
Section 18: Data Structures and Algorithms in C++
-
Section 19: Practical Projects
-
Section 20: More Concurrency Patterns
-
Section 21: More OOP and Design Practice
-
Section 22: File I/O and Streams
-
Section 23: More Standard Library
-
Section 24: Practice Exercises
-
Section 25: Interview and Algorithm Practice
-
Section 26: Compiler and Language Internals
-
Section 27: GUI and Application Frameworks Overview
-
Section 28: Testing and Quality Practices
-
Section 29: Numerics Library
-
Section 30: Concepts Library (C++20)
-
Section 31: Ranges Library (C++20) In Depth
-
Section 32: More Utility Library
165: Implementing a Hash Table in C++
I've seen a lot of developers try to roll their own hash table for the first time, and almost everyone makes the same mistake. They focus so much on the "hash" part that they completely forget about the "collision" part. Let's look at a snippet of code that looks correct on the surface but will drive you crazy in production.
// BUGGY CODE: Don't do this!
class SimpleMap {
std::string keys[10];
std::string values[10];
int hash(std::string k) {
int h = 0;
for (char c : k) h += c;
return h % 10;
}
public:
void insert(std::string k, std::string v) {
int idx = hash(k);
keys[idx] = k;
values[idx] = v;
}
std::string get(std::string k) {
int idx = hash(k);
return values[idx];
}
};
// Usage
SimpleMap map;
map.insert("Apple", "Red");
map.insert("Banana", "Yellow");
// Wait... what if "Apple" and "Banana" hash to the same index?
The Vanishing Data Problem
In the code above, we have a classic collision. If hash("Apple") and hash("Banana") both return 5, the "Banana" entry simply overwrites "Apple". You didn't get a crash or a compiler error; you just lost your data. This is the most dangerous kind of bug because it's silent. Your tests might pass with a few items, but the moment your dataset grows, your map starts "forgetting" things randomly.
To fix this, we need a way to store multiple items at the same index. The most common way to handle this is called Separate Chaining. Instead of the bucket being a single value, the bucket becomes a list of pairs. When a collision happens, we just append the new key-value pair to that list.
Implementing Separate Chaining
Here is how I would rewrite this to be robust. I'm using std::list for the buckets because it's clear and handles dynamic growth well, though in a high-performance system, you might use a small vector to be more cache-friendly.
#include <iostream>
#include <vector>
#include <list>
#include <string>
class HashTable {
struct Entry {
std::string key;
std::string value;
};
// A vector of lists. Each list is a "bucket".
std::vector<std::list<Entry>> buckets;
int capacity;
int hash(const std::string& k) const {
// Using a slightly better hash than just adding characters
size_t h = std::hash<std::string>{}(k);
return h % capacity;
}
public:
HashTable(int cap = 10) : capacity(cap) {
buckets.resize(capacity);
}
void insert(const std::string& k, const std::string& v) {
int idx = hash(k);
// First, check if the key already exists to update it
for (auto& entry : buckets[idx]) {
if (entry.key == k) {
entry.value = v;
return;
}
}
// If we get here, it's a new key. Add it to the bucket.
buckets[idx].push_back({k, v});
}
std::string get(const std::string& k) {
int idx = hash(k);
for (const auto& entry : buckets[idx]) {
if (entry.key == k) return entry.value;
}
return "NOT_FOUND";
}
};
Choosing a Better Hash Function
You'll notice I swapped the manual character-sum loop for std::hash<std::string>{}. I strongly recommend doing this. Writing your own hash function is a fun academic exercise, but it's a nightmare in practice. A poor hash function creates "clustering," where too many keys land in the same bucket. If every single key hashes to index 5, your $O(1)$ hash table effectively becomes a $O(n)$ linked list, and your performance falls off a cliff.
The Load Factor and Resizing
There's one more thing: the capacity. If you have 10 buckets and you insert 1,000 items, your average list length is 100. That's slow. Professional implementations track the Load Factor (number of items / number of buckets). Once the load factor hits a certain threshold (usually 0.75), the table "resizes."
Resizing isn't as simple as calling vector::resize(). Because the index depends on hash(key) % capacity, changing the capacity changes the index for every single item. You have to create a brand new, larger array and re-hash every single existing element into the new buckets. It's an expensive operation, but because it happens infrequently, the "amortized" cost remains $O(1)$.
📋 Practical Task
Exercise: Building a Rare-Word Frequency Tracker
Your task is to implement a hash table that tracks how many times specific words appear in a text. Instead of mapping string to string, you will map string to int.
Requirements:
- Implement a
WordCounterclass with avoid addWord(std::string word)method and anint getCount(std::string word)method. - Use separate chaining (a vector of lists) to handle collisions.
- The
addWordmethod should increment the count if the word already exists, or create a new entry with a count of 1 if it doesn't. - Use
std::hashfor the hashing logic. - Test your implementation by passing in a list of words where at least two different words hash to the same bucket (you can verify this by printing the bucket index inside the
addWordmethod).
There are no comments for now.