Appearance
哈希通过某种规则把原数据映射为较短的摘要。由于值域通常小于原数据空间,哈希一般不是单射:不同对象可能得到相同的哈希值,这种情况称为哈希冲突。
因此,哈希值不同可以推出原对象不同;哈希值相同通常只能作为原对象相同的必要条件,还需要接受一定的误判概率,或在哈希相同时继续比较原对象。
哈希表:
开放寻址和拉链法只能处理多个键落到同一桶的情况,并不能消除哈希冲突。哈希函数分布较均匀且负载因子受到控制时,插入、删除、查询的期望复杂度为
开放寻址法:
开放寻址法使用定长数组直接保存键值对。
取一个表长
删除元素时,不能直接把位置标为空,否则会截断其它键的探测链。通常使用“墓碑”标记:查询越过墓碑继续探测,插入则可以复用遇到的第一个墓碑。负载因子过高或墓碑过多时,应扩容并重新散列。
拉链法:
拉链法为每个桶维护一个链表、动态数组或其它容器。
取一个模数
事实上,开放寻址法和拉链法可通过修改哈希函数,改善键的分布。
开放寻址法还可以修改步长。拉链法也可修改位置上的数据结构。
基于开放寻址法的哈希表实现:
点击展开代码
cpp
mt19937_64 rnd(chrono::steady_clock::now().time_since_epoch().count());
struct Hash_Map {
enum State : unsigned char { EMPTY, OCCUPIED, DELETED };
State state[N]{};
u64 a[N];
int b[N];
stack<int> q;
void clear() {
while (q.size()) {
auto now = q.top();
q.pop();
state[now] = EMPTY;
}
}
const u64 s1 = rnd(), s2 = rnd() | 1, s3 = rnd() | 1;
u64 f(u64 x) {
x += s1;
x = (x ^ (x >> 33)) * s2;
x = (x ^ (x >> 30)) * s3;
return x;
}
bool find(u64 x) {
int now = f(x) % N;
for (int cnt = 0; cnt < N; cnt++) {
if (state[now] == EMPTY)
return false;
if (state[now] == OCCUPIED && a[now] == x)
return true;
now = (now + 1) % N;
}
return false;
}
int get(u64 x) {
int now = f(x) % N;
int del = -1;
for (int cnt = 0; cnt < N; cnt++) {
if (state[now] == OCCUPIED && a[now] == x)
return now;
if (state[now] == DELETED && del == -1)
del = now;
if (state[now] == EMPTY) {
if (del != -1) {
now = del;
} else {
q.push(now);
}
state[now] = OCCUPIED, a[now] = x, b[now] = 0;
return now;
}
now = (now + 1) % N;
}
if (del != -1) {
state[del] = OCCUPIED, a[del] = x, b[del] = 0;
return del;
}
return -1;
}
bool erase(u64 x) {
int now = f(x) % N;
for (int cnt = 0; cnt < N; cnt++) {
if (state[now] == EMPTY)
return false;
if (state[now] == OCCUPIED && a[now] == x) {
state[now] = DELETED;
return true;
}
now = (now + 1) % N;
}
return false;
}
int &operator[](const u64 x) {
int now = get(x);
if (now == -1)
throw overflow_error("Hash_Map is full");
return b[now];
}
};其中,state 的初值均为 EMPTY,键数组使用 u64,不会截断传入的键。新键对应的值会初始化为 get 最多探测 -1;operator[] 在这种情况下显式报错。模板不自动扩容,使用时应让
多项式模数哈希:
优点:
- 可以将字符串映射到
中,方便存储。 - 模运算具有优秀的性质,可以支持很多其它操作(比如修改字符串中的某一个字符,只需要加减取模即可)。
缺点:
- 映射范围较小容易产生哈希冲突。
自然溢出:
使用 unsigned long long 进行运算,不使用取模,任凭数据在底层自动“溢出”
优点:
- 运算速度快。
缺点:
- 本质是对
取模,模数过于特殊,十分容易卡。
值域与哈希冲突
若哈希函数把较大的输入域映射到
最大公约数只在某些特定映射中能给出精确结论。例如对完整剩余系
有
令
多项式哈希冲突概率:
设哈希值域大小为
当
当
上述估计依赖独立、均匀的随机模型。固定模数、固定底数的多项式哈希可能被针对性构造;随机选择并隐藏底数或种子只能降低被构造的风险,不能提供确定正确性。
双模数多项式哈希:
使用两个模数分别计算多项式哈希,只有两路结果均相同时,才把两个字符串视为可能相同。
若两路计算的是同一个整数多项式 base 和字符映射,且
等价于
若模数不互质,等效模数只能写成
只有在两路哈希可近似看作均匀且相互独立时,单次比较的碰撞概率才可近似相乘,生日界也才可按有效空间
集合哈希:
判断两个序列排序后是否相同,本质上是判断两个多重集是否相同。
哈希做法:
两个多重集相同一定会得到相同摘要,反过来通常不成立。因此,构造确定性摘要可以视作增加用于排除“不相同”的必要条件。
例如此处,
但是也会出现
可以继续增加立方和、四次方和等条件来排除一部分冲突,但只维护固定的少量幂和,尤其是
若多重集大小为
若维护前
随机化
一种更容易分析的随机化方法是:选取大质数
对两个预先固定的不同多重集,若至少一个元素的出现次数之差不被
多项式哈希做法:
update on 2025.4.20
2024 郑州 G 中使用了多项式哈希维护多重集摘要。设值域为
选取质数
sum hash
上文的做法就是一种 Sum Hash。
Sum Hash 可以作为判断集合或多重集是否相同的概率摘要。
对于多重集
其中,
更常见的随机化写法是直接令
update on 2025.4.20:
2024 郑州 G 题引申了只取少量幂和的 sum hash 并不充分:当
隐藏种子后再做随机映射,可以阻止针对固定公开参数预先构造的部分数据,但这是一种概率保证,不能把有限个幂和变成确定充分条件。若需要维护区间加等操作,还必须证明所选摘要能够由区间信息正确更新;静态随机映射的 Sum Hash 一般不能仅由原摘要推出整体加值后的新摘要。
xor hash
xor 运算的一个性质:
所以使用 xor 可以很好地维护数字出现次数的奇偶性。
若某一个多重集内所有数字出现次数均为偶数次,那么
但是,
可以为每个不同的值
xor hash 应用:
排列,是一个特殊的序列,其中每个数在
同一个长度的所有排列的异或和相同,所以可以预处理
概率判定时,必须先确认序列长度为
其中 vis 数组判重。
xor hash 再扩展:
能用 xor hash 出现次数为偶数的情况,是基于
可以把异或推广为各位独立的模
“所有出现次数都是
