在C++标准模板库(STL)中,无序容器(unordered_set/unordered_map)是基于哈希表实现的高效数据结构。与红黑树实现的有序容器(set/map)相比,它们提供了平均O(1)时间复杂度的查找性能,但牺牲了元素的有序性。
哈希表的核心原理是通过哈希函数将键(key)映射到数组的特定位置(桶)。理想情况下,不同键会映射到不同位置,但实际中会出现哈希冲突。常见的冲突解决方法包括:
STL采用的是链地址法,这也是我们实现时需要重点考虑的部分。哈希表的性能关键取决于:
首先定义基础的哈希节点结构,这是链地址法的基础单元:
cpp复制template <typename T>
struct HashNode {
T data;
HashNode* next;
HashNode(const T& val) : data(val), next(nullptr) {}
};
对于unordered_map需要存储键值对:
cpp复制template <typename Key, typename Value>
struct HashNode {
std::pair<const Key, Value> data;
HashNode* next;
HashNode(const Key& k, const Value& v)
: data(std::make_pair(k, v)), next(nullptr) {}
};
哈希表的主体结构包含以下核心组件:
cpp复制template <typename Key, typename Value, typename Hash = std::hash<Key>>
class HashTable {
private:
std::vector<HashNode<Key, Value>*> buckets; // 桶数组
size_t element_count = 0; // 元素总数
float max_load_factor = 1.0; // 最大负载因子
Hash hasher; // 哈希函数对象
// 其他辅助方法...
};
关键参数说明:
buckets:存储链表头指针的动态数组element_count:当前存储的元素数量max_load_factor:触发rehash的阈值(元素数/桶数)hasher:用于计算键哈希值的函数对象桶位置通过哈希函数计算得到:
cpp复制size_t bucket_index(const Key& key) const {
return hasher(key) % buckets.size();
}
注意:实际实现中应考虑空表情况,且模运算在桶数为2的幂次时可优化为位操作
插入操作需要考虑键是否已存在:
cpp复制bool insert(const Key& key, const Value& value) {
// 检查是否需要rehash
if (need_rehash()) {
rehash(buckets.size() * 2);
}
size_t index = bucket_index(key);
HashNode<Key, Value>* current = buckets[index];
// 检查键是否已存在
while (current) {
if (current->data.first == key) {
return false; // 键已存在
}
current = current->next;
}
// 创建新节点并插入链表头部
HashNode<Key, Value>* new_node = new HashNode<Key, Value>(key, value);
new_node->next = buckets[index];
buckets[index] = new_node;
++element_count;
return true;
}
查找操作相对直接:
cpp复制Value* find(const Key& key) {
size_t index = bucket_index(key);
HashNode<Key, Value>* current = buckets[index];
while (current) {
if (current->data.first == key) {
return &(current->data.second);
}
current = current->next;
}
return nullptr;
}
删除操作需要维护链表结构:
cpp复制bool erase(const Key& key) {
size_t index = bucket_index(key);
HashNode<Key, Value>* current = buckets[index];
HashNode<Key, Value>* prev = nullptr;
while (current) {
if (current->data.first == key) {
if (prev) {
prev->next = current->next;
} else {
buckets[index] = current->next;
}
delete current;
--element_count;
return true;
}
prev = current;
current = current->next;
}
return false;
}
负载因子是触发rehash的关键指标:
cpp复制float load_factor() const {
return static_cast<float>(element_count) / buckets.size();
}
bool need_rehash() const {
return !buckets.empty() &&
load_factor() > max_load_factor;
}
rehash过程需要重建整个哈希表:
cpp复制void rehash(size_t new_size) {
if (new_size <= buckets.size()) return;
std::vector<HashNode<Key, Value>*> new_buckets(new_size, nullptr);
for (auto head : buckets) {
while (head) {
HashNode<Key, Value>* next = head->next;
size_t new_index = hasher(head->data.first) % new_size;
head->next = new_buckets[new_index];
new_buckets[new_index] = head;
head = next;
}
}
buckets.swap(new_buckets);
}
提示:实际STL实现中,桶数量通常选择质数或2的幂次,以减少哈希冲突
哈希表迭代器需要能够遍历所有桶中的所有元素:
cpp复制template <typename Key, typename Value>
class HashIterator {
using Node = HashNode<Key, Value>;
using BucketArray = std::vector<Node*>;
BucketArray* buckets; // 指向桶数组的指针
size_t bucket_index; // 当前桶索引
Node* current; // 当前节点
public:
// 迭代器常规操作...
};
实现operator++需要处理跨桶遍历:
cpp复制HashIterator& operator++() {
if (current) {
current = current->next;
if (current) return *this;
}
// 当前桶已遍历完,寻找下一个非空桶
for (++bucket_index;
bucket_index < buckets->size();
++bucket_index) {
current = (*buckets)[bucket_index];
if (current) break;
}
return *this;
}
实际STL实现会使用内存池技术:
cpp复制// 使用自定义分配器减少内存碎片
template <typename T>
class NodeAllocator {
std::vector<T*> blocks;
T* free_list = nullptr;
public:
T* allocate() {
if (free_list) {
T* node = free_list;
free_list = free_list->next;
return node;
}
// 分配新内存块...
}
void deallocate(T* node) {
node->next = free_list;
free_list = node;
}
};
针对常见键类型提供特化版本:
cpp复制template <>
struct Hash<std::string> {
size_t operator()(const std::string& s) const {
size_t h = 0;
for (char c : s) {
h = (h * 131) + c;
}
return h;
}
};
基础线程安全实现:
cpp复制template <typename Key, typename Value>
class ConcurrentHashTable {
std::vector<std::mutex> bucket_locks;
void lock_bucket(size_t index) {
bucket_locks[index % bucket_locks.size()].lock();
}
void unlock_bucket(size_t index) {
bucket_locks[index % bucket_locks.size()].unlock();
}
public:
// 在操作前后加锁解锁...
};
验证核心操作正确性:
cpp复制void test_insert_find() {
HashTable<std::string, int> table;
table.insert("apple", 1);
table.insert("banana", 2);
assert(*table.find("apple") == 1);
assert(table.find("orange") == nullptr);
}
与STL实现对比:
cpp复制void benchmark() {
const int N = 1000000;
std::unordered_map<int, int> std_map;
HashTable<int, int> our_map;
// 插入性能测试
auto start = std::chrono::high_resolution_clock::now();
for (int i = 0; i < N; ++i) {
std_map.insert({i, i});
}
auto end = std::chrono::high_resolution_clock::now();
// 输出时间...
// 查找性能测试...
}
测试不同负载因子下的性能:
cpp复制void test_load_factor() {
HashTable<int, int> table;
table.max_load_factor(0.5); // 设置较低的负载因子
// 测量插入时间...
// 观察rehash触发点...
}
哈希函数选择:
std::hash内存使用:
线程安全:
异常安全:
性能调优:
实现完整STL风格容器还需要考虑更多细节:分配器支持、类型特征、异常安全保证等。但以上核心实现已经展示了unordered_map/unordered_set的关键技术要点。