自 C++11 开始,STL 引入了基于 hash table 的 unordered_set、unordered_map 等容器,正如其名它们是无序容器。一定数量范围(据说有测试数据是10000000)元素时无序容器的性能要比对应的有序容器优。
一、容器数据结构
unordered_set、unordered_map 等容器的 Hash Table(哈希表/散列表)结构通常是:桶(bucket) + 线性表,bucket 一般用 vector 实现,线性表通常采用链表。当插入元素时通过哈希函数计算其哈希值,以哈希值作为 vector 的下标索引,取得元素要插入的链表头,把元素插入进去。也就是说通过 链地址法(Separate Chaining) 解决哈希冲突。
二、哈希函数
哈希函数是这些容器的核心,也是保证它们高性能的关键。C++11 为基本类型:int、float、double、char、string 提供了哈希函数。 但是要在这些容器中添加自定义类型的元素就必须提供特定的哈希函数。
没有为自定义类提供哈希函数
无论是 clang++、还是 g++、msvc++ 都无法正确编译如下代码,原因有两个:
- 缺失计算 Person 对象哈希值的函数;
- 没有提供元素的“相等”比较运算符;
#include <string>
#include <unordered_set>
class Person {
public:
Person(const std::string &name, int age) : _name(name), _age(age) {}
private:
std::string _name;
int _age;
};
int main(int argc, char *argv[]) {
std::unordered_set<Person> persons;
}
为自定义类添加哈希函数
查看 unordered_set 的声明、 g++ 的 /usr/include/c++/9/bits/functional_hash.h 可知:
- unordered_set 的模板参数 Hash 默认设置为 std::hash<Key>,std::hash 是一个类模板;
- 通过特化 std::hash,支持了诸如 int、double 基本类型的哈希函数;
- 每个特化版本中重载了函数调用运算符
template<
class Key,
class Hash = std::hash<Key>,
class KeyEqual = std::equal_to<Key>,
class Allocator = std::allocator<Key>
> class unordered_set;
#define _Cxx_hashtable_define_trivial_hash(_Tp) \
template<> \
struct hash<_Tp> : public __hash_base<size_t, _Tp> \
{ \
size_t \
operator()(_Tp __val) const noexcept \
{ return static_cast<size_t>(__val); } \
};
/// Explicit specialization for int.
_Cxx_hashtable_define_trivial_hash(int)
/// Explicit specialization for bool.
_Cxx_hashtable_define_trivial_hash(bool)
/// Explicit specialization for char.
_Cxx_hashtable_define_trivial_hash(char)
综述,为 Person 类定义函数对象类实现哈希函数功能
#include <stdint.h>
#include <string>
#include <unordered_set>
struct Person {
Person(const std::string &name, int age, uint32_t id) :
_name(name), _age(age), _id(id) {}
///【新加代码】 重载了 “==” 这是容器规定
bool operator==(const Person& ref) const {
return this->_id == ref._id;
}
std::string _name;
int _age;
uint32_t _id;
};
/// 【新加代码】
struct PersonHash
{
size_t operator()(const Person &person) const noexcept
{
/// 这里仅仅为了编译通过,实际意义并不大
return static_cast<size_t>(person._age + person._name.length());
}
};
int main(int argc, char *argv[])
{
std::unordered_set<Person, PersonHash> persons;
persons.insert({"yaoyuan", 30, 1});
persons.insert({"yaoming", 40, 2});
/// 获取篮子(bucket)位置数目
std::cout << "insert 2 elems,bucket count=" << persons.bucket_count() << std::endl;
/// 获取每个篮子位置挂接元素个数
for (size_t i = 0; i < persons.bucket_count(); ++i) {
std::cout << "bucket[" << i << "]Chaining=" << persons.bucket_size(i) << std::endl;
}
return 0;
}
优化哈希函数
对于上述哈希函数的实现,用侯捷老师的话说就是“过于天真”。侯捷老师使用可变模板和 Boost 哈希算法实现了一个万能且比较可靠的哈希函数解决方案;使用类的一个数据成员生成种子“seed”,然后依次使用其它数据成员迭代更新该种子,生成最终的哈希值,代码如下所示:
//[0] 万能的哈希函数
#pragma once
template<typename __T>
inline void hash_combine(size_t &seed, const __T &val) {
seed ^= std::hash<__T>()(val) + 0x9e3779b9 + (seed << 6) + (seed >> 2);
}
template<typename __T>
inline void hash_val(size_t &seed, const __T &val)
{
hash_combine(seed, val);
}
template<typename __T, typename... __Tys>
inline void hash_val(size_t &seed, const __T &val, const __Tys&... args)
{
hash_combine(seed, val);
hash_val(seed, args...);
}
template<class... __Tys>
inline size_t hash_val(const __Tys&... args) {
size_t seed = 0;
hash_val(seed, args...);
return seed;
}
/// [1] 修改后的哈希对象类
struct PersonHash
{
size_t operator()(const Person &person) const noexcept
{
// return static_cast<size_t>(person._age + person._name.length());
return ::hash_val(person._age, person._name, person._id);
}
};
三、总结
无序容器采用一个哈希函数和元素类型的 == 运算符来组织元素,在元素没有明显次序关系的情况下,无序容器是非常有用的。通常维护元素的次序代价非常高,这时就可以考虑无序容器。