哈希函数对象 (Hash Functor) –
std::hash
std::hash 是 C++ 标准库中定义的一个类模板,位于
头文件中。它的主要作用是作为一个函数对象(Functor),为给定的数据类型计算一个唯一的、稳定的哈希值 。
从本质上讲,std::hash
是一个提供了标准哈希计算接口 的工具或者说算法(算子)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 template <class Key > struct hash ;template <>struct hash <int > { size_t operator () (int value) const noexcept { return static_cast <size_t >(value); } }; template <typename T>struct hash <T*> { size_t operator () (T* ptr) const noexcept { return reinterpret_cast <size_t >(ptr); } }; template <>struct hash <std::string> { size_t operator () (const std::string& str) const noexcept { size_t hash_value = 5381 ; for (char c : str) { hash_value = ((hash_value << 5 ) + hash_value) + c; } return hash_value; } };
它的形式如上, 我们可以为不同的数据类型实例化它,
从而实现传入某个类型的数据, 返回计算得到的哈希值(size_t). 它也重载了
operator()并返回一个 size_t
类型的哈希值,使其可以像函数一样被调用。例如,std::hash<std::string>()("hello")
会返回字符串 “hello” 的哈希值。
std::hash<std::string>()("hello")是创建一个临时哈希对象并调用它的operator()函数,
等价于
auto hasher = std::hash<std::string>{}; size_t value = hasher("hello");
如何使用 std::hash
std::hash
的使用分为两种情况:对标准库内置支持的类型使用,以及对自定义类型使用。
对内置支持的类型使用
标准库已经为所有基本数据类型(如 int, char, float, double,
bool)、指针 、以及一些常用库类型(如 std::string,
std::thread::id, std::shared_ptr 等)提供了 std::hash
的特化版本。因此可以直接使用它们,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 #include <iostream> #include <string> #include <functional> int main () { std::string s = "Hello, World!" ; int i = 12345 ; double d = 3.14159 ; std::hash<std::string> string_hasher; std::hash<int > int_hasher; std::hash<double > double_hasher; size_t string_hash = string_hasher (s); size_t int_hash = int_hasher (i); size_t double_hash = double_hasher (d); std::cout << "Hash of \"" << s << "\" is " << string_hash << std::endl; std::cout << "Hash of " << i << " is " << int_hash << std::endl; std::cout << "Hash of " << d << " is " << double_hash << std::endl; return 0 ; }
对自定义类型使用
如果你想将自定义的类或结构体作为 std::unordered_map
的键或者哈希算子的参数,编译器默认是不知道如何计算其哈希值的。这时,你需要为你的自定义类型
特化(Specialize) std::hash 模板。
特化步骤主要是: - 定义你的自定义类型(例如 struct Student)。 - 在
std 命名空间内,为你的类型提供一个 std::hash
的模板特化 版本。 - 在这个特化版本中,实现
operator() ,它接受一个你的自定义类型的常量引用,并返回一个
size_t 类型的哈希值。 - 在实现 operator()
时,你需要设计一种算法,将对象的各个成员的哈希值组合成一个总的哈希值。
可以看出, 获得自定义对象的哈希值比较麻烦,
还需使用者来设计哈希算法.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 struct Student { int id; std::string name; bool operator ==(const Student& other) const { return id == other.id && name == other.name; } }; namespace std{ template <> struct hash <Student> { size_t operator () (const Student& s) const noexcept { size_t id_hash = SimpleHash::hash<int >{}(s.id); size_t name_hash = SimpleHash::hash<std::string>{}(s.name); return id_hash ^ (name_hash << 1 ); } }; }
哈希表(Hash Table) 容器
对于哈希, 我们在平时使用更多的反而是根据哈希函数对象创建的哈希表容器.
这些容器包括:
std::unordered_map
std::unordered_set
std::unordered_multimap
std::unordered_multiset
主要使用的是前两者. multi 版本除了允许重复的键存在,
其他用法和接口基本相同, 因此这里不做赘述.
unordered_set
std::unordered_set
是一个基于哈希表 实现的用于存储唯一元素 的关联容器。它的主要设计目标是提供平均时间复杂度为
O(1)
的元素查找 、插入 和删除 操作。可以把它想象成一个数学上的集合 :一个无序的、包含唯一元素的集合.
与
std::set(基于红黑树 实现,提供有序 存储和
O(log N) 的操作复杂度)不同,std::unordered_set
更侧重于快速访问 ,但不保证元素的顺序 。
相比 unordered_map, 它只存储键, 没有值,
因此它更多用于需要快速判断某个元素是否存在 的场景。
也可以用于去除重复元素 :将一个包含重复元素的列表(如
std::vector)转换为
std::unordered_set,可以快速得到一个不含重复元素的集合。
1 2 3 4 vector<int > numbers = {1 , 2 , 5 , 2 , 1 , 4 }; std::unordered_set<int > uniqueNumbers (numbers.begin(), numbers.end()) ;
函数签名如下:
1 2 3 4 5 6 7 #include <unordered_set> template < class T , class Hash = std::hash<T>, class KeyEqual = std::equal_to<T>, class Allocator = std::allocator<T> > class unordered_set;
使用时一般只需要关注第一个模板参数 T
即可,表示
元素的类型 ,
可以是任何
可哈希的类型 (如 int, std::string,
自定义类型等), vector 等不可哈希类型不能作为元素类型.
基本接口
插入元素:
insert (key):
插入一个新元素。如果元素已存在,则插入失败, 返回一个
pair,包含指向该元素的迭代器和一个布尔值表示插入是否成功。
emplace(args…):
直接在容器内构造一个新元素,避免不必要的拷贝或移动。
查找元素:
find(key): 返回指向该元素的迭代器 ,如果不存在则返回
end()。
contains (key) (C++20):
直接返回一个布尔值 ,表示元素是否存在。
count(key): 返回元素的数量(0 或 1,因为元素是唯一的)。
删除元素:
erase (key):
删除指定值 的元素。
clear(): 清空集合中的所有元素。
获取集合信息:
size(): 返回集合中元素的数量。
empty(): 检查集合是否为空。
迭代访问:
begin() 和 end():
返回指向集合开头和结尾的迭代器,用于遍历所有元素。
范围 for 循环也可以直接用于遍历。 此时每个元素是类型
T,可以直接使用。
下面是一个简单的示例,展示了如何使用 std::unordered_set:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 #include <iostream> #include <unordered_set> #include <string> int main () { std::unordered_set<std::string> fruitSet; fruitSet.insert ("apple" ); fruitSet.insert ("banana" ); fruitSet.insert ("orange" ); auto result = fruitSet.insert ("apple" ); if (!result.second) { std::cout << "Element 'apple' already exists in the set." << std::endl; } std::cout << "Total unique fruits: " << fruitSet.size () << std::endl; std::cout << "Is set empty? " << (fruitSet.empty () ? "Yes" : "No" ) << std::endl; if (fruitSet.find ("banana" ) != fruitSet.end ()) { std::cout << "'banana' is in the set." << std::endl; } if (fruitSet.contains ("grape" )) { std::cout << "'grape' is in the set." << std::endl; } else { std::cout << "'grape' not found." << std::endl; } for (const auto & fruit : fruitSet) { std::cout << fruit << std::endl; } fruitSet.erase ("orange" ); return 0 ; }
unordered_map
std::unordered_map
是一个基于哈希表 实现的关联容器,用于存储键值对
(Key-Value Pairs)。它的设计目标是提供平均时间复杂度为
O(1)
的元素查找 、插入 和删除 操作。
对比
std::map(基于红黑树 实现,提供有序 存储和
O(log N) 的操作复杂度),std::unordered_map
更侧重于快速访问 ,但不保证元素的顺序 。
函数签名如下:
1 2 3 4 5 6 7 8 #include <unordered_map> template < class Key , class T , class Hash = std::hash<Key>, class KeyEqual = std::equal_to<Key>, class Allocator = std::allocator< std::pair<const Key, T> > > class unordered_map;
一般只需要关注前两个模板参数 Key 和 T
即可,分别表示
键的类型 和
值的类型 。哈希表映射的关键就在于键,
因此需要仔细考虑选择什么类型作为键,
以及它是否具有”
唯一性 ”.
并且这里的 key 的类型必须是可哈希的 (Hashable)
和可判等的 (Equality
Comparable),以确保哈希表能够正确地存储和查找键值对, 例如 std::string,
int, char, 指针 等基本类型都是可以的, 但是 vector
不可以作为 key 类型
它的使用场景主要包括需要快速查找 、插入 和删除 键值对的应用。例如,频繁查询某个键对应的值,或者需要动态更新键值对。
基本接口包括: 1. 检查是否存在某个键: - find(key):
返回指向该键值对的迭代器 ,如果不存在则返回 end()。 -
contains (key) (C++20):
直接返回一个布尔值 ,表示键是否存在。 - count(key):
返回键的数量(0 或 1,因为键是唯一的)。 2. 插入或更新键值对: -
insert({key, value}): 插入一个新的键值对,如果键已存在则插入失败。 -
operator[key] :
访问 或插入 键对应的值。如果键不存在,会插入一个默认值。
3. 删除键值对: - erase(key) :
删除指定键 的键值对。 - clear():
清空映射中的所有键值对。 4. 获取映射信息: - size():
返回映射中键值对的数量。 - empty(): 检查映射是否为空。 5. 迭代访问: -
begin() 和 end(): 返回指向映射开头和结尾的迭代器,用于遍历所有键值对。 -
范围 for 循环 也可以直接用于遍历。此时每个元素是一个
std::pair<const Key, T>, 可以通过 first 和 second
访问键和值。
下面是一个简单的示例,展示了如何使用 std::unordered_map:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 #include <iostream> #include <unordered_map> #include <string> int main () { std::unordered_map<std::string, int > wordCount; wordCount["apple" ] = 2 ; wordCount["banana" ] = 3 ; wordCount["apple" ] += 1 ; std::cout << "Total unique words: " << wordCount.size () << std::endl; std::cout << "Is map empty? " << (wordCount.empty () ? "Yes" : "No" ) << std::endl; if (wordCount.find ("banana" ) != wordCount.end ()) { std::cout << "Banana count: " << wordCount["banana" ] << std::endl; } if (wordCount.contains ("orange" )) { std::cout << "Orange count: " << wordCount["orange" ] << std::endl; } else { std::cout << "Orange not found." << std::endl; } for (const auto & pair : wordCount) { std::cout << pair.first << ": " << pair.second << std::endl; } wordCount.erase ("banana" ); return 0 ; }
普通 map 和 set
普通的 std::map 和 std::set
是基于红黑树 实现的关联容器,提供有序存储 和
O(log N)
的操作复杂度。与哈希表容器不同,它们保证元素按照键的顺序排列。
这使得它们适用于需要有序遍历 或范围查询 的场景。例如,按字典顺序存储单词,或者需要查找某个范围内的元素。
他们可以获取元素的最小值和最大值, 以及进行范围查询等操作,
这是哈希表容器所不具备的功能.