哈希表
哈希表
是一种使用哈希函数
组织数据,以支持快速插入和搜索的数据结构。
有两种不同类型的哈希表:哈希集合和哈希映射。
哈希集合
是集合
数据结构的实现之一,用于存储非重复值
。哈希映射
是映射
数据结构的实现之一,用于存储(key, value)
键值对。
在标准模板库
的帮助下,哈希表是易于使用的
。大多数常见语言(如Java,C ++ 和 Python)都支持哈希集合和哈希映射。
通过选择合适的哈希函数,哈希表可以在插入和搜索方面实现出色的性能
。
哈希表的原理
正如我们在介绍中提到的,哈希表
是一种数据结构,它使用哈希函数组织数据,以支持快速插入和搜索
。
哈希表的关键思想是使用哈希函数将键映射到存储桶
。
- 当我们插入一个新的键时,哈希函数将决定该键应该分配到哪个桶中,并将该键存储在相应的桶中;
- 当我们想要搜索一个键时,哈希表将使用相同的哈希函数来查找对应的桶,并只在特定的桶中进行搜索。
示例
在示例中,我们使用 y = x % 5 作为哈希函数。让我们使用这个例子来完成插入和搜索策略:
- 插入:我们通过哈希函数解析键,将它们映射到相应的桶中。
- 例如,1987 分配给桶 2,而 24 分配给桶 4。
- 搜索:我们通过相同的哈希函数解析键,并仅在特定存储桶中搜索。
- 如果我们搜索 1987,我们将使用相同的哈希函数将1987 映射到 2。因此我们在桶 2 中搜索,我们在那个桶中成功找到了 1987。
- 例如,如果我们搜索 23,将映射 23 到 3,并在桶 3 中搜索。我们发现 23 不在桶 3 中,这意味着 23 不在哈希表中。
设计哈希表的关键
在设计哈希表时,你应该注意两个基本因素。
1. 哈希函数
哈希函数是哈希表中最重要的组件,该哈希表用于将键映射到特定的桶。在示例中,我们使用 y = x % 5
作为散列函数,其中 x
是键值,y
是分配的桶的索引。
散列函数将取决于键值的范围
和桶的数量。
下面是一些哈希函数的示例:
哈希函数的设计是一个开放的问题。其思想是尽可能将键分配到桶中,理想情况下,完美的哈希函数将是键和桶之间的一对一映射。然而,在大多数情况下,哈希函数并不完美,它需要在桶的数量和桶的容量之间进行权衡。
2. 冲突解决
理想情况下,如果我们的哈希函数是完美的一对一映射,我们将不需要处理冲突。不幸的是,在大多数情况下,冲突几乎是不可避免的。
例如,在我们之前的哈希函数(y = x % 5)中,1987 和 2 都分配给了桶 2,这是一个冲突
。
冲突解决算法应该解决以下几个问题:
- 如何组织在同一个桶中的值?
- 如果为同一个桶分配了太多的值,该怎么办?
- 如何在特定的桶中搜索目标值?
根据我们的哈希函数,这些问题与桶的容量
和可能映射到同一个桶
的键的数目
有关。
让我们假设存储最大键数的桶有 N
个键。
通常,如果 N 是常数且很小,我们可以简单地使用一个数组将键存储在同一个桶中。如果 N 是可变的或很大,我们可能需要使用高度平衡的二叉树
来代替。
训练
到目前为止,您应该能够实现基本的哈希表。我们为您提供了实现哈希集和哈希映射的练习。阅读需求
,确定哈希函数
并在需要时解决冲突
。
如果你不熟悉哈希集或是哈希映射的概念,可以返回介绍部分找出答案。.
插入
和搜索
是哈希表中的两个基本操作。
此外,还有基于这两个操作的操作。例如,当我们删除元素
时,我们将首先搜索元素,然后在元素存在的情况下从相应位置移除元素。
哈希集 - 用法
哈希集
是集合的实现之一,它是一种存储不重复值
的数据结构。
#include <unordered_set> // 0. include the library
int main() {
// 1. initialize a hash set
unordered_set<int> hashset;
// 2. insert a new key
hashset.insert(3);
hashset.insert(2);
hashset.insert(1);
// 3. delete a key
hashset.erase(2);
// 4. check if the key is in the hash set
if (hashset.count(2) <= 0) {
cout << "Key 2 is not in the hash set." << endl;
}
// 5. get the size of the hash set
cout << "The size of hash set is: " << hashset.size() << endl;
// 6. iterate the hash set
for (auto it = hashset.begin(); it != hashset.end(); ++it) {
cout << (*it) << " ";
}
cout << "are in the hash set." << endl;
// 7. clear the hash set
hashset.clear();
// 8. check if the hash set is empty
if (hashset.empty()) {
cout << "hash set is empty now!" << endl;
}
}
使用哈希集查重
插入新值并检查值是否在哈希集中是简单有效的。因此,通常使用哈希集来检查该值是否已经出现过。
让我们来看一个例子:
给定一个整数数组,查找数组是否包含任何重复项。
你可以简单地迭代每个值并将值插入集合中。 如果值已经在哈希集中,则存在重复。
在这里,我们为你提供了解决此类问题的模板:
/*
* Template for using hash set to find duplicates.
*/
bool findDuplicates(vector<Type>& keys) {
// Replace Type with actual type of your key
unordered_set<Type> hashset;
for (Type key : keys) {
if (hashset.count(key) > 0) {
return true;
}
hashset.insert(key);
}
return false;
}
哈希映射 - 用法
哈希映射
是用于存储 (key, value)
键值对的一种实现。
#include <unordered_map> // 0. include the library
int main() {
// 1. initialize a hash map
unordered_map<int, int> hashmap;
// 2. insert a new (key, value) pair
hashmap.insert(make_pair(0, 0));
hashmap.insert(make_pair(2, 3));
// 3. insert a new (key, value) pair or update the value of existed key
hashmap[1] = 1;
hashmap[1] = 2;
// 4. get the value of a specific key
cout << "The value of key 1 is: " << hashmap[1] << endl;
// 5. delete a key
hashmap.erase(2);
// 6. check if a key is in the hash map
if (hashmap.count(2) <= 0) {
cout << "Key 2 is not in the hash map." << endl;
}
// 7. get the size of the hash map
cout << "the size of hash map is: " << hashmap.size() << endl;
// 8. iterate the hash map
for (auto it = hashmap.begin(); it != hashmap.end(); ++it) {
cout << "(" << it->first << "," << it->second << ") ";
}
cout << "are in the hash map." << endl;
// 9. clear the hash map
hashmap.clear();
// 10. check if the hash map is empty
if (hashmap.empty()) {
cout << "hash map is empty now!" << endl;
}
}
场景 I - 提供更多信息
使用哈希映射的第一个场景是,我们需要更多的信息
,而不仅仅是键。然后通过哈希映射建立密钥与信息之间的映射关系
。
让我们来看一个例子:
给定一个整数数组,返回两个数字的索引,使它们相加得到特定目标。
在这个例子中,如果我们只想在有解决方案时返回 true,我们可以使用哈希集合来存储迭代数组时的所有值,并检查 target - current_value
是否在哈希集合中。
但是,我们被要求返回更多信息
,这意味着我们不仅关心值,还关心索引。我们不仅需要存储数字作为键,还需要存储索引作为值。因此,我们应该使用哈希映射而不是哈希集合。
更重要的是
在某些情况下,我们需要更多信息,不仅要返回更多信息,还要帮助我们做出决策
。
在前面的示例中,当我们遇到重复的键时,我们将立即返回相应的信息。但有时,我们可能想先检查键的值是否可以接受。
在这里,我们为您提供了解决此类问题的模板:
/*
* Template for using hash map to find duplicates.
* Replace ReturnType with the actual type of your return value.
*/
ReturnType aggregateByKey_hashmap(vector<Type>& keys) {
// Replace Type and InfoType with actual type of your key and value
unordered_map<Type, InfoType> hashtable;
for (Type key : keys) {
if (hashmap.count(key) > 0) {
if (hashmap[key] satisfies the requirement) {
return needed_information;
}
}
// Value can be any information you needed (e.g. index)
hashmap[key] = value;
}
return needed_information;
}
场景 II - 按键聚合
另一个常见的场景是按键聚合所有信息
。我们也可以使用哈希映射来实现这一目标。
这是一个例子:
给定一个字符串,找到它中的第一个非重复字符并返回它的索引。如果它不存在,则返回 -1。
解决此问题的一种简单方法是首先计算每个字符的出现次数
。然后通过结果找出第一个与众不同的角色。
因此,我们可以维护一个哈希映射,其键是字符,而值是相应字符的计数器。每次迭代一个字符时,我们只需将相应的值加 1。
更重要的是
解决此类问题的关键是在遇到现有键时确定策略
。
在上面的示例中,我们的策略是计算事件的数量。有时,我们可能会将所有值加起来。有时,我们可能会用最新的值替换原始值。策略取决于问题,实践将帮助您做出正确的决定。
在这里,我们为您提供了解决此类问题的模板:
/*
* Template for using hash map to find duplicates.
* Replace ReturnType with the actual type of your return value.
*/
ReturnType aggregateByKey_hashmap(vector<Type>& keys) {
// Replace Type and InfoType with actual type of your key and value
unordered_map<Type, InfoType> hashtable;
for (Type key : keys) {
if (hashmap.count(key) > 0) {
update hashmap[key];
}
// Value can be any information you needed (e.g. index)
hashmap[key] = value;
}
return needed_information;
}
设计键
在以前的问题中,键的选择相对简单。不幸的是,有时你必须考虑在使用哈希表时设计合适的键
。
我们来看一个例子:
给定一组字符串,将字母异位词组合在一起。
众所周知,哈希映射可以很好地按键分组信息。但是我们不能直接使用原始字符串作为键。我们必须设计一个合适的键来呈现字母异位词的类型。例如,有字符串 “eat” 和 “ate” 应该在同一组中。但是 “eat” 和 “act” 不应该组合在一起。
解决方案
实际上,设计关键
是在原始信息和哈希映射使用的实际键之间建立映射关系
。设计键时,需要保证:
- 属于同一组的所有值都将映射到同一组中。
- 需要分成不同组的值不会映射到同一组。
此过程类似于设计哈希函数,但这是一个本质区别。哈希函数满足第一个规则但可能不满足第二个规则
。但是你的映射函数应该满足它们。
在上面的示例中,我们的映射策略可以是:对字符串进行排序并使用排序后的字符串作为键。也就是说,“eat” 和 “ate” 都将映射到 “aet”。
设计键 - 总结
- 当字符串 / 数组中每个元素的顺序不重要时,可以使用
排序后的字符串 / 数组
作为键。 - 如果只关心每个值的偏移量,通常是第一个值的偏移量,则可以使用
偏移量
作为键。 - 在树中,你有时可能会希望直接使用
TreeNode
作为键。 但在大多数情况下,采用子树的序列化表述
可能是一个更好的主意。 - 在矩阵中,你可能希望使用
行索引
或列索引
作为键。 - 在数独中,可以将行索引和列索引组合来标识此元素属于哪个
块
。
每个值的偏移量,通常是第一个值的偏移量,则可以使用偏移量
作为键。[外链图片转存中…(img-kVHyOmRM-1589299271190)] - 在树中,你有时可能会希望直接使用
TreeNode
作为键。 但在大多数情况下,采用子树的序列化表述
可能是一个更好的主意。[外链图片转存中…(img-xKP4SGlm-1589299271191)] - 在矩阵中,你可能希望使用
行索引
或列索引
作为键。 - 在数独中,可以将行索引和列索引组合来标识此元素属于哪个
块
。[外链图片转存中…(img-cqeZQjVr-1589299271193)] - 有时,在矩阵中,您可能希望将值聚合在
同一对角线
中。