1. 项目背景与核心目标
在C++标准库中,unordered_map和unordered_set是两种极为重要的关联容器,它们基于哈希表实现,提供了O(1)时间复杂度的查找性能。这次我们要做的,就是自己动手实现这两个容器的简化版本——myunordered_map和myunordered_set。
为什么要做这个轮子?原因有三:
- 深入理解STL容器的底层实现机制
- 掌握哈希表这一经典数据结构的具体应用
- 学习如何用C++模板和迭代器来封装复杂数据结构
2. 哈希表基础实现
2.1 哈希表结构设计
我们先从最基础的哈希表开始。一个典型的哈希表需要包含以下几个关键部分:
cpp复制template <typename T>
class HashTable {
private:
std::vector<std::list<T>> buckets; // 桶数组
size_t bucket_count; // 桶数量
size_t element_count; // 元素总数
float max_load_factor; // 最大负载因子
// 哈希函数
size_t hash_function(const T& key) const {
return std::hash<T>{}(key) % bucket_count;
}
// 扩容机制
void rehash(size_t new_bucket_count) {
// 实现略...
}
};
2.2 关键操作实现
插入操作的实现需要考虑多种情况:
cpp复制bool insert(const T& value) {
// 检查是否需要扩容
if (load_factor() > max_load_factor) {
rehash(bucket_count * 2);
}
size_t bucket_index = hash_function(value);
auto& bucket = buckets[bucket_index];
// 检查是否已存在
if (std::find(bucket.begin(), bucket.end(), value) != bucket.end()) {
return false;
}
bucket.push_back(value);
element_count++;
return true;
}
查找和删除操作也需要类似的哈希计算和链表遍历过程。
3. 封装unordered_map
3.1 map的键值对存储
unordered_map需要存储键值对,我们首先定义一个pair结构:
cpp复制template <typename Key, typename Value>
struct KeyValuePair {
Key key;
Value value;
bool operator==(const KeyValuePair& other) const {
return key == other.key;
}
};
然后为这个pair特化哈希函数:
cpp复制namespace std {
template <typename Key, typename Value>
struct hash<KeyValuePair<Key, Value>> {
size_t operator()(const KeyValuePair<Key, Value>& kv) const {
return hash<Key>{}(kv.key);
}
};
}
3.2 map接口实现
基于之前的HashTable,我们可以实现map的基本接口:
cpp复制template <typename Key, typename Value>
class myunordered_map {
private:
HashTable<KeyValuePair<Key, Value>> table;
public:
// 插入元素
bool insert(const Key& key, const Value& value) {
return table.insert(KeyValuePair<Key, Value>{key, value});
}
// 访问元素
Value& operator[](const Key& key) {
auto it = find(key);
if (it == end()) {
insert(key, Value{});
it = find(key);
}
return it->value;
}
// 查找元素
iterator find(const Key& key) {
KeyValuePair<Key, Value> dummy{key, Value{}};
return table.find(dummy);
}
};
4. 封装unordered_set
4.1 set的简化实现
set的实现相对简单,因为只需要存储值本身:
cpp复制template <typename Value>
class myunordered_set {
private:
HashTable<Value> table;
public:
bool insert(const Value& value) {
return table.insert(value);
}
bool contains(const Value& value) const {
return table.contains(value);
}
size_t erase(const Value& value) {
return table.erase(value);
}
};
4.2 迭代器实现
为了让我们的容器更完整,需要实现迭代器功能:
cpp复制template <typename Table>
class HashIterator {
Table* table;
size_t bucket_index;
typename std::list<typename Table::value_type>::iterator list_it;
public:
HashIterator(Table* t, size_t bi,
typename std::list<typename Table::value_type>::iterator li)
: table(t), bucket_index(bi), list_it(li) {}
// 前置++
HashIterator& operator++() {
++list_it;
while (list_it == table->buckets[bucket_index].end() &&
bucket_index < table->bucket_count - 1) {
++bucket_index;
list_it = table->buckets[bucket_index].begin();
}
return *this;
}
// 解引用
typename Table::value_type& operator*() {
return *list_it;
}
// 比较
bool operator!=(const HashIterator& other) const {
return list_it != other.list_it;
}
};
5. 性能优化与测试
5.1 负载因子与扩容策略
哈希表的性能很大程度上取决于负载因子的控制。我们可以在HashTable类中添加:
cpp复制float load_factor() const {
return static_cast<float>(element_count) / bucket_count;
}
void set_max_load_factor(float ml) {
max_load_factor = ml;
if (load_factor() > max_load_factor) {
rehash(bucket_count * 2);
}
}
5.2 测试用例
编写测试用例验证我们的实现:
cpp复制void test_myunordered_map() {
myunordered_map<std::string, int> map;
map["apple"] = 5;
map["banana"] = 3;
assert(map["apple"] == 5);
assert(map.find("banana") != map.end());
assert(map.find("orange") == map.end());
}
void test_myunordered_set() {
myunordered_set<int> set;
set.insert(1);
set.insert(2);
assert(set.contains(1));
assert(!set.contains(3));
assert(set.erase(1) == 1);
}
6. 实现中的关键问题与解决方案
6.1 哈希冲突处理
我们采用了链地址法(separate chaining)来处理冲突,这是最常用的方法之一。在实际实现中需要注意:
- 链表不宜过长,否则会退化为线性查找
- 当链表长度超过阈值时,可以考虑转换为平衡二叉树(如Java 8的HashMap实现)
6.2 迭代器失效问题
哈希表的插入操作可能导致rehash,从而使所有迭代器失效。我们需要:
- 在文档中明确说明哪些操作会导致迭代器失效
- 在修改操作后,让现有的迭代器能够检测到失效状态
cpp复制// 在HashTable中添加修改计数器
size_t modification_count;
// 在迭代器中保存创建时的计数器值
size_t initial_modification_count;
// 在迭代器操作前检查
void check_validity() const {
if (initial_modification_count != table->modification_count) {
throw std::runtime_error("Iterator invalidated by modification");
}
}
6.3 自定义哈希函数支持
为了增强灵活性,应该允许用户提供自定义哈希函数:
cpp复制template <typename Key, typename Value,
typename Hash = std::hash<Key>,
typename KeyEqual = std::equal_to<Key>>
class myunordered_map {
private:
Hash hasher;
KeyEqual key_equal;
size_t hash_function(const Key& key) const {
return hasher(key) % bucket_count;
}
};
7. 进阶优化方向
7.1 内存池优化
频繁的链表节点分配会影响性能,可以使用内存池来优化:
cpp复制template <typename T>
class ListNodeAllocator {
private:
std::vector<std::unique_ptr<T[]>> memory_blocks;
size_t current_block_pos = 0;
static constexpr size_t BLOCK_SIZE = 1024;
public:
T* allocate() {
if (memory_blocks.empty() || current_block_pos >= BLOCK_SIZE) {
memory_blocks.emplace_back(new T[BLOCK_SIZE]);
current_block_pos = 0;
}
return &memory_blocks.back()[current_block_pos++];
}
};
7.2 开放寻址法实现
除了链地址法,还可以尝试开放寻址法:
cpp复制template <typename T>
class OpenAddressingHashTable {
private:
std::vector<std::optional<T>> table;
size_t element_count = 0;
size_t probe(const T& value, size_t attempt) const {
return (hash_function(value) + attempt) % table.size();
}
public:
bool insert(const T& value) {
if (element_count >= table.size() * max_load_factor) {
rehash(table.size() * 2);
}
for (size_t attempt = 0; attempt < table.size(); ++attempt) {
size_t index = probe(value, attempt);
if (!table[index] || *table[index] == value) {
table[index] = value;
element_count++;
return true;
}
}
return false;
}
};
8. 与STL容器的对比
8.1 功能差异
我们的实现与STL相比缺少了一些高级功能:
- 不支持分配器自定义
- 缺少一些高级查找方法如equal_range
- 桶接口不完整
- 异常安全性保证不足
8.2 性能对比
通过简单的性能测试可以发现:
- 小数据量时,我们的实现与STL性能接近
- 大数据量时,由于缺少优化,性能差距可能达到2-3倍
- 内存使用效率通常不如STL实现
9. 实际应用建议
9.1 何时使用自定义实现
虽然STL的实现已经非常优秀,但在以下场景可能需要自定义实现:
- 需要特殊的内存管理策略
- 需要特定的哈希算法或冲突解决策略
- 在嵌入式等受限环境中需要更精简的实现
- 用于教学目的
9.2 生产环境建议
对于生产环境:
- 优先使用标准库实现
- 如果确实需要自定义,考虑继承或组合标准库实现
- 必须进行充分的测试和性能评估
10. 扩展思考
10.1 并发版本实现
要实现线程安全的哈希表,可以考虑:
- 细粒度锁(每个桶一个锁)
- 读写锁优化
- 无锁编程技术
cpp复制template <typename T>
class ConcurrentHashTable {
private:
std::vector<std::mutex> bucket_locks;
public:
bool insert(const T& value) {
size_t bucket_index = hash_function(value);
std::lock_guard<std::mutex> lock(bucket_locks[bucket_index]);
// 剩余插入逻辑...
}
};
10.2 其他哈希表变种
值得探索的其他哈希表变种包括:
- 布谷鸟哈希(Cuckoo Hashing)
- 罗宾汉哈希(Robin Hood Hashing)
- 跳房子哈希(Hopscotch Hashing)
每种方法都有其独特的优势和适用场景,可以根据具体需求选择合适的实现方式。