Hash 散列

解决动态查找问题,字符串管理:-
如:编译器中变量名的管理(插入,查找)
将字符串 --> 数值 : 进行数值比较

散列函数的设计

评价标准和设计原则

越随机,越没有规律越好

  1. 确定性(determinism):同一关键码总是映射到同一地址
  2. 快速(efficiency):expected - O(1)
  3. 满射(surjection):尽可能充分地覆盖满整个散列空间
  4. 均匀(uniformity):均匀散列(概率平均),避免聚集(Clustering)

Key 是 整型

  1. 直接定址法
    h(key) = a * key + b

  2. 除留余数法
    h(key) = key mod p

(p 一般取素数 )
证明:数据具有局部性(Locality)-> 假设步长为S
gcd(S, p) 只有== 1时才能取遍整个散列表,p对任意S成立,所以p为素数 --> 禅的哲学(生命周期:M = 13,17…,与天敌的生命周期S避开)

缺点:

  1. 不动点0点
  2. 零阶均匀:平均分配到M个桶,但相邻关键码的散列地址必相邻 (有些场合不需要高阶和均匀性:计算几何中)

-> 改进:MAD方法(设表长为M,取素数)
has(key) = (a * key + b) % M

  1. 数字分析法

希望结果受到更多位的影响,增加随机性

选中key的若干digit构成地址

缺点:未体现均匀性(未选中的digit对散列地址无贡献)

  1. 折叠法

拆分,相加, 如123456 = 12 + 34 + 56

  1. 平方取中法

取key2 居中的若干digit:
原理:使得构成原key的digit尽可能对散列结果产生影响(平方=若干左移操作+求和 --> 类似于区间贪心:中间digit覆盖了更多的区间,由更多的原digit累加得)

  • 其他散列函数:
  1. 折叠法:分组 再求和(自左向右,往复折返…) 作为散列地址
  2. XOR法:二进制数分组,再按位异或 得到散列地址
  3. 伪随机数法
    通过伪随机数发生器 产生散列地址
    缺点:移植性差

Key 是 字符串

  1. 多项式法:按位 x “权值” 累加起来得到散列地址
    eg. abc = a100+b10+c

缺点:过多乘法运算
–> 改进:近似计算方法,每次累加后将对应二进制数的低5位和高27位交换
h = (h<<5) | (h>>27);

  1. ASCII码和求余: 类似折叠法
    h(key) = ∑ key[i] mod TableSize;

缺点:频繁冲突(加法满足交换律)

  1. 前3位移位法
    h(key) = (key[0] * 272 + key[1] * 27 + key[2]) mod TableSize;

  2. 移位法
    看作32进制数,转化为10进制,再取余

冲突处理方法

多槽位法

将每个bucket 分成若干个slot,存放彼此冲突的词条

缺点:slot划分无法确认,可能冲突或浪费空间
–>改进:独立链 (separate chaining)

链地址法(Separate Chaining)

缺点:

  1. 指针需要额外空间
  2. 节点需要动态申请(差102倍)
  3. 空间不连续,Cache失效

–> 改进:开放定址法

开放地址法(Open Address)

沿设计的查找链(Probing Chianing)直到找到冲突位置

注意TableSize 为散列表大小(实际物理大小), MSize(散列函数取模大小,散列表的逻辑大小,一般取比TableSize小一点的素数)

h i (key) = ( h(key) + d i ) mod TableSize;
※ (1 <= i < TableSize)

根据d i 的不同,设计为:

  1. 线性探测(Linear Probing)
    d i = i;

增量序列:1, 2, … TableSize-1;

缺点: 试探位置相邻,易产生冲突

  1. 平方探测

  2. 双向平方探测 (Quadratic Probing)

d i = ± i 2;

增量序列:12, - 12 , 2 2 , - 2 2 … q 2 , - q 2
且:q <= floor(TableSize / 2)

  • 缺点:
  1. 破坏了数据的局部性,若涉及外存IO将激增
  2. 会出现有空位置,但探测不到
    解决:
    -> 定理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)的每一素因子均为偶数次方

  1. 双散列

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];

分块思想

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值