Hash
解决动态查找问题,字符串管理:-
如:编译器中变量名的管理(插入,查找)
将字符串 --> 数值 : 进行数值比较
散列函数的设计
评价标准和设计原则
越随机,越没有规律越好
- 确定性(determinism):同一关键码总是映射到同一地址
- 快速(efficiency):expected - O(1)
- 满射(surjection):尽可能充分地覆盖满整个散列空间
- 均匀(uniformity):均匀散列(概率平均),避免聚集(Clustering)
Key 是 整型
-
直接定址法
h(key) = a * key + b -
除留余数法
h(key) = key mod p
(p 一般取素数 )
证明:数据具有局部性(Locality)-> 假设步长为S
gcd(S, p) 只有== 1时才能取遍整个散列表,p对任意S成立,所以p为素数 --> 禅的哲学(生命周期:M = 13,17…,与天敌的生命周期S避开)
缺点:
- 不动点0点
- 零阶均匀:平均分配到M个桶,但相邻关键码的散列地址必相邻 (有些场合不需要高阶和均匀性:计算几何中)
-> 改进:MAD方法(设表长为M,取素数)
has(key) = (a * key + b) % M
- 数字分析法
希望结果受到更多位的影响,增加随机性
选中key的若干digit构成地址
缺点:未体现均匀性(未选中的digit对散列地址无贡献)
- 折叠法
拆分,相加, 如123456 = 12 + 34 + 56
- 平方取中法
取key2 居中的若干digit:
原理:使得构成原key的digit尽可能对散列结果产生影响(平方=若干左移操作+求和 --> 类似于区间贪心:中间digit覆盖了更多的区间,由更多的原digit累加得)
- 其他散列函数:
- 折叠法:分组 再求和(自左向右,往复折返…) 作为散列地址
- XOR法:二进制数分组,再按位异或 得到散列地址
- 伪随机数法
通过伪随机数发生器 产生散列地址
缺点:移植性差
Key 是 字符串
- 多项式法:按位 x “权值” 累加起来得到散列地址
eg. abc = a100+b10+c
缺点:过多乘法运算
–> 改进:近似计算方法,每次累加后将对应二进制数的低5位和高27位交换
h = (h<<5) | (h>>27);
- ASCII码和求余: 类似折叠法
h(key) = ∑ key[i] mod TableSize;
缺点:频繁冲突(加法满足交换律)
-
前3位移位法
h(key) = (key[0] * 272 + key[1] * 27 + key[2]) mod TableSize; -
移位法
看作32进制数,转化为10进制,再取余
冲突处理方法
多槽位法
将每个bucket 分成若干个slot,存放彼此冲突的词条
缺点:slot划分无法确认,可能冲突或浪费空间
–>改进:独立链 (separate chaining)
链地址法(Separate Chaining)
缺点:
- 指针需要额外空间
- 节点需要动态申请(差102倍)
- 空间不连续,Cache失效
–> 改进:开放定址法
开放地址法(Open Address)
沿设计的查找链(Probing Chianing)直到找到冲突位置
注意TableSize 为散列表大小(实际物理大小), MSize(散列函数取模大小,散列表的逻辑大小,一般取比TableSize小一点的素数)
h i (key) = ( h(key) + d i ) mod TableSize;
※ (1 <= i < TableSize)
根据d i 的不同,设计为:
- 线性探测(Linear Probing)
d i = i;
增量序列:1, 2, … TableSize-1;
缺点: 试探位置相邻,易产生冲突
-
平方探测
-
双向平方探测 (Quadratic Probing)
d i = ± i 2;
增量序列:12, - 12 , 2 2 , - 2 2 … q 2 , - q 2
且:q <= floor(TableSize / 2)
- 缺点:
- 破坏了数据的局部性,若涉及外存IO将激增
- 会出现有空位置,但探测不到
解决:
-> 定理1 -> 散列表长度TableSize 是某个 4 * k + 3 (k为整数)形式的素数时,平方探测法就可以探查到整个散列表的空间 --> 关于4的模余3的素数(另一类为模4余1的素数)
-> 定理2:表长时素数,并且装填因子<0.5就不会出现最坏情况(反证法)
双平方定理(费马):任一素数p若能表示为一对整数的平方和,当且仅当 p % 4 == 1 (借助恒等式:(u2+v2)(s2+t2) = (us+vt)2+(ut-vs)2)
==> 任一自然数n可表示为一对整数的平方和,当且仅当在其素分解中,形如 (M = 4*k + 3)的每一素因子均为偶数次方
- 双散列
d i = i * h 2 (key)
h1找原位置,h2冲突后找下一个位置
增量序列:h2(key) , 2 * h2(key) , 3 * h2(key) …
对任意h2(key) != 0
h2(key) = p - (key mod p)
p < TableSize , p , TableSize 都是素数
词条删除:
可能产生空的bucket,导致查找链断裂
-> 解决方法:Lazy Reomval: 不清空bucket,而打上标记,同时插入时如果碰到标记bucket,可直接插入(替换掉僵尸)
再散列
装填因子过大时(表快满了),将TableSize扩大,源表中所有元素必须按新的TableSize重新装入
散列表性能分析 ASL
成功平均查找长度(ASLs)
散列表中的元素,查找(比较)平均需要的次数,其中 每个元素的查找次数 = 冲突次数 + 1
失败平均查找长度(ASLu)
不在散列表中的元素(即表中的空位置),查找失败平均需要的次数
一般方法:把不在散列表中的查找元素按 可散列的位置h(key) 分类
对散列表中每个位置,按冲突函数不断查找到空值(不在表中石锤了!)的平均查找次数
实现:
unordered_map<int, int> SearchTime;
void InsertTable(int x, int TSize)
{
int pos = x % TSize;
for(int i = 0; i < TSize; ++i)
{
int nex = (pos + i * i) % TSize;
if(table[nex] == 0)
{
table[nex] = x;
SearchTime[x] = i + 1;
return;
}
}
printf("%d cannot be inserted.\n", x);
SearchTime[x] = TSize + 1;
}
int Search(int x, int TSize)
{
int pos = x % TSize;
for(int i = 0; i < TSize; ++i)
{
int nex = (pos + i * i) % TSize;
if(table[nex] == x || table[nex] == 0) //找到了 || 没找到
{
return i + 1;
}
}
return TSize + 1;
}
- while循环形式
unordered_map<int, int> SearchTime;
void InsertTable(int x, int TSize)
{
int pos = x % TSize, nex, step = 0;
while(step < TSize)
{
nex = (pos + step * step) % TSize;
if(table[nex] == 0)
break;
step++;
}
if(step == TSize)
{
printf("%d cannot be inserted.\n", x);
}else
{
table[nex] = x;
}
SearchTime[x] = step + 1;
}
int Search(int x, int TSize)
{
int pos = x % TSize, nex, step = 0;
while(step < TSize)
{
nex = (pos + step * step) % TSize;
if(table[nex] == x || table[nex] == 0)
break;
step++;
}
return step + 1;
}
散列应用: 桶排序算法 / 计数排序算法
不完全取决于待排序序列的规模N,同时取决于待排序序列的范围
- 假设:输入序列长度N,取值范围M ([0, M) )
时间复杂度:O(N+M) / O(max(N,M))
–> 可能有大量重复
- 计数排序:
思路:散列表 accumation() 累计值(从 0 ~ i 的积分值)
sum[i] = num[i] + sum[i-1];
分块思想