文章目录
一、哈希映射 hash(x)
我们知道,std::unordered_map是一种非常高效的哈希映射,他相对于红黑树实现的std::map抛弃了有序性,但是提高了效率,插入和查询操作时间复杂度仅仅为 O ( 1 ) O(1) O(1)。
但是,在算法考试、算法竞赛中,std::unordered_map常常因为常数太大,非常容易被出题人卡掉。因此我们需要换一种更加高效的数组模拟的方法。
在看下面的内容之前,请先阅读这篇文章:【AcWing 840. 模拟散列表】,来了解std::unordered_set的数组实现思想。
现在我们把不管是哈希集合还是哈希映射都要用到的核心函数 f i n d find find 先列在这里:
注意:需要强调的是 h [ h a s h ( x ) ] = x h[hash(x)] = x h[hash(x)]=x 数组维护的并不仅仅是某个数 x x x 的 h a s h ( x ) hash(x) hash(x) 值,而是记录一种 x → h a s h ( x ) x →hash(x) x→hash(x) 已存在的事实。
const int null = 0x3f3f3f3f3f, N = 2 * 题目数据规模 + c; // 加c是为了让他成为素数
int h[N];
int find(int x)
{
int k = (x % N + N) % N;
while (h[k] != null && h[k] != x)
{
k++;
if (k == N) k = 0;
}
return k;
}
二、数组实现的unordered_set
通过上面文章的阅读,我们可以理解数组 h h h 本质是记录一个(较大的)数,经过哈希化之后得到另一个(较小的)数, h h h 本质上是在维护这一种 x → h a s h ( x ) x→hash(x) x→hash(x) 的映射关系。
我们最常用的就是将一个数字插入到集合中和在集合中能否查询到这个数字。
2.1 哈希集合的插入操作
下面这个操作,与 unordered_set::insert(x)
函数逻辑功能完全一致。
...
int x;
scanf("%d", &x); // 输入要插入unordered_set中的数据x
h[find(x)] = x; // 插入集合(用维护映射关系模拟插入)
2.2 哈希集合的查询
下面这个操作,与 unordered_set::count(x)
函数逻辑功能完全一致。
int x;
scanf("%d", &x); // 输入想要在集合中查询的数字
if (h[find(x)] != null) // 与if (unordered_set.count(x) != 0) 一致
....
else
....
三、数组实现的unordered_map
通过刚刚的学习,我们发现其实哈希集合本质就是将 x → h a s h ( x ) x→hash(x) x→hash(x) 这么一个步骤。那么如果我们想要实现一个哈希映射该如何做呢?
假设我们要将 x x x 映射成 y y y,即 x → y x→y x→y。我们可以通过多一个中间步骤实现映射操作: x → h a s h ( x ) → y x→hash(x)→y x→hash(x)→y 来实现。
【例子】:现在假设我们想要将集合 A = { 5 , 7 , 9 , 6 , 8 } A=\{5,7,9,6,8\} A={5,7,9,6,8} 映射成集合 B = { 0 , 1 , 2 , 3 , 4 } B={\{0,1,2,3,4\}} B={0,1,2,3,4}:
3.1 哈希映射的插入
如果我们使用的是std::unordered_map,将 A A A 集合映射为 B B B 集合。
#include <unordered_map>
...
unordered_map<int, int> book;
int A[5] = {6, 7, 9, 6, 8}, B[5] = {0, 1, 2, 3, 4};
...
for (int i = 0; i < 5; i++) book[A[i]] = B[i]; // 映射的插入
现在看看数组模拟的方法:
int book[N];
...
for (int i = 0; i < 5; i++)
{
int k = find(A[i]); // k 就是 hash(x)
h[k] = A[i]; // 完成 x → hash(x) 标记
book[k] = B[i]; // 完成 hash(x) → y,也就是插入
}
3.2 哈希映射的查询
如果我们使用的是std::unordered_map,查询 A [ 2 ] A[2] A[2] 对应的映射结果:
...
printf("%d\n", book[A[2]]);
...
现在看看数组模拟的方法:
...
printf("%d\n", book[find(A[2])]);
...
再次注意:需要强调的是 h [ h a s h ( x ) ] = x h[hash(x)] = x h[hash(x)]=x 数组维护的并不仅仅是某个数 x x x 的 h a s h ( x ) hash(x) hash(x) 值,而是记录一种 x → h a s h ( x ) x →hash(x) x→hash(x) 已存在的事实。
如果说查询操作经常使用,我们完全可以在创建一个数组 h c o d e [ x ] hcode[x] hcode[x],数组里面维护的是 x x x 的 f i n d ( x ) find(x) find(x) 结果,也就是 h a s h ( x ) hash(x) hash(x) 的值,这样就不用频繁重复调用 f i n d ( x ) find(x) find(x) 函数。
三、字符串的映射方法
我们上面介绍的数组模拟的方法都是基于unordered_set<int>或者unodered_map<int, int>,那么,如果我们想要处理unordered_set<string>、unordered_map<string, int>或者unordered_map<string, string>这种类型该如何处理呢?
事实上,我们完全可以先将string类型映射到一个较小的int类型的常数,然后将问题转换为我们上文中提到的int相关的set和map的处理。
那么,如何将string类型与int相映射呢?如果凭直觉,你可能会想到使用字符串哈希的方法(如果对字符串哈希不了解的同学可以看这里【AcWing 841. 字符串哈希】)。显而易见,字符串哈希手动模拟比较麻烦,那么有没有更好的处理办法呢?
事实上,我们完全可以将字符串理解为一个很大的数,那么将很大的数映射到一个很小的数完全可以使用离散化操作(本质就是对字符串数组进行二分搜索,建立某个str与其在字符串数组中的下标的关系)。
vector<string> seq;
// 建立seq中的string与该string在seq中下标的映射关系
int hash(string x)
{
int l = 0, r = seq.size();
while (l < r)
{
int mid = l + r >> 1;
if (seq[mid] >= x) r = mid;
else l = mid + 1;
}
return l;
}
int main()
{
...
for (int i = 0; i < n; i ++ )
{
string str;
cin >> str;
// 总之,将所有的str放在seq中
seq.push_back(str);
}
// 为了使用unqiue删除重复的元素,先排序
sort(seq.begin(), seq.end());
seq.erase(unique(seq.begin(), seq.end()), seq.end());
// 尝试进行映射
string str;
cin >> str;
// key就是建立的str到key的映射关系
int key = hash(str);
return 0;
}