一、简介
哈希表
是一种使用哈希函数
组织数据,以支持快速插入和搜索的数据结构。
有两种不同类型的哈希表:哈希集合和哈希映射。
哈希集合
是集合
数据结构的实现之一,用于存储非重复值
。哈希映射
是映射
数据结构的实现之一,用于存储(key, value)
键值对。
在标准模板库
的帮助下,哈希表是易于使用的
。大多数常见语言(如Java,C ++ 和 Python)都支持哈希集合和哈希映射。
通过选择合适的哈希函数,哈希表可以在插入和搜索方面实现出色的性能
。
PS:下文三个实际应用先纯文字记录下,到时候做了题再把代码加进去。
二、哈希表
哈希表的关键思想是使用哈希函数将键映射到存储桶
。
- 当我们插入一个新的键时,哈希函数将决定该键应该分配到哪个桶中,并将该键存储在相应的桶中;
- 当我们想要搜索一个键时,哈希表将使用相同的哈希函数来查找对应的桶,并只在特定的桶中进行搜索。
举个栗子:
我们使用 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 是可变的或很大,我们可能需要使用高度平衡的二叉树
来代替.。
三、实际应用-哈希集合
哈希集
是集合的实现之一,它是一种存储不重复值
的数据结构。
我们知道,插入新值并检查值是否在哈希集中是简单有效的。因此,通常,使用哈希集来检查该值是否已经出现过。
举个栗子,
问题:给定一个整数数组,查找数组是否包含任何重复项。
这是一个典型的问题,可以通过哈希集来解决。 你可以简单地迭代每个值并将值插入集合中。 如果值已经在哈希集中,则存在重复。
/*
* 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;
}
四、实际应用-哈希映射
(1) 场景一:
使用哈希映射的第一个场景是,我们需要更多的信息
,而不仅仅是键。然后通过哈希映射建立密钥与信息之间的映射关系
。
举个栗子,
问题:给定一个整数数组,返回两个数字的索引,使它们相加得到特定目标。
要求返回更多信息
,这意味着我们不仅关心值,还关心索引。我们不仅需要存储数字作为键,还需要存储索引作为值。因此,我们应该使用哈希映射而不是哈希集合。
在某些情况下,我们需要更多信息,不仅要返回更多信息,还要帮助我们做出决策
。
在前面的示例中,当我们遇到重复的键时,我们将立即返回相应的信息。但有时,我们可能想先检查键的值是否可以接受。
(2)场景二:
另一个常见的场景是按键聚合所有信息
。我们也可以使用哈希映射来实现这一目标。
举个栗子,
问题:给定一个字符串,找到它中的第一个非重复字符并返回它的索引。如果它不存在,则返回 -1。
解决此问题的一种简单方法是首先计算每个字符的出现次数
。然后通过结果找出第一个与众不同的角色。
因此,我们可以维护一个哈希映射,其键是字符,而值是相应字符的计数器。每次迭代一个字符时,我们只需将相应的值加 1。
解决此类问题的关键是在遇到现有键时确定策略
。
在上面的示例中,我们的策略是计算事件的数量。有时,我们可能会将所有值加起来。有时,我们可能会用最新的值替换原始值。策略取决于问题,实践将帮助您做出正确的决定。
五、实际应用- 设计键
有时你必须考虑在使用哈希表时设计合适的键
。
问题:给定一组字符串,将字母异位词组合在一起。
此处不能直接使用原始字符串作为键。我们必须设计一个合适的键来呈现字母异位词的类型。
实际上,设计关键
是在原始信息和哈希映射使用的实际键之间建立映射关系
。设计键时,需要保证:
- 属于同一组的所有值都将映射到同一组中。
- 需要分成不同组的值不会映射到同一组。
此过程类似于设计哈希函数,但这是一个本质区别。哈希函数满足第一个规则但可能不满足第二个规则
。但是你的映射函数应该满足它们。
在上面的问题中,我们的映射策略可以是:对字符串进行排序并使用排序后的字符串作为键。也就是说,“eat” 和 “ate” 都将映射到 “aet”。
这里有一些如何设计键的建议:
- 当字符串 / 数组中每个元素的顺序不重要时,可以使用
排序后的字符串 / 数组
作为键。 - 如果只关心每个值的偏移量,通常是第一个值的偏移量,则可以使用
偏移量
作为键。 - 在树中,你有时可能会希望直接使用
TreeNode
作为键。 但在大多数情况下,采用子树的序列化表述
可能是一个更好的主意。 - 在矩阵中,你可能希望使用
行索引
或列索引
作为键。 - 在数独中,可以将行索引和列索引组合来标识此元素属于哪个
块
。 - 有时,在矩阵中,您可能希望将值聚合在
同一对角线
中。