哈希表

哈希表

哈希表的关键是哈希函数,以及如何处理冲突。常见的处理冲突的方法有拉链法和开放寻址法。

1)拉链法

使用数组实现的哈希表如图所示,数组中每个槽对应一条单链表。插入操作的基本原理是:根据哈希函数给出的哈希值,判断数组中该位置是否被占用,如果没有,则插入到此位置,如果被占用,则插入到单链表的头部(头插法)。查询操作的基本原理是:根据哈希函数给出的哈希值,如果数组中该位置没被占用,则代表查询的值不存在,如果该位置被占用,则在位置对应的单链表中顺序查找。

算法题中,哈希表一般只有插入和查找操作。如果要实现删除操作的话,不会直接对数组进行删除,一般是再创建一个布尔数组,对需要删除的元素打个标记(如上图所示)。

2)开放寻址法

只需使用一个一维数组来存储,且数组长度需为一个质数,经验上取为输入数据量的2~3倍。插入操作的基本原理是:根据哈希函数的哈希值,判断数组中该位置是否被占用,如果没有,则插入到此位置,如果被占用,就再看下一个位置是否被占用,直到找到一个没被占用的位置插入。查询操作的基本原理是:根据哈希函数的哈希值,判断数组中该位置是否被占用,如果不被占用,则查询的值不存在,如果被占用,则比较该位置的值与查询到值是否相等,相等则表示查询的值存在,不相等的话继续看下一个位置,并重复前述的判断。

如果碰到要实现删除操作,也是不会直接对数组元素进行删除,而是再创建一个布尔数组,对需要删除的元素在新数组的相应位置打上标记。

如果碰到要手写简易哈希表的情形,推荐使用开放寻址法,代码比较简单,不易出错。代码模板如下:

// 数组初始化,应初始化成一个输入中不可能出现的值,下面用null表示
// N为输入数据量的2~3倍
int[] h = new int[N];
for(int i = 0; i < N; i ++) h[i] = null;

// 如果x在哈希表中,返回x所在数组中的下标;如果x不在哈希表中,返回x应该插入的位置
public int find(int x){
    int t = (x % N + N) % N;   // 防止x为负数时取余运算的结果出错
    while(h[t] != null && h[t] != x){
        t ++;
        if(t == N) t = 0;
    }
    return t;
}
字符串哈希

计算出字符串任一子串的哈希值,包括如下内容:

1)求出字符串的哈希值

可将字符串看成一个 P P P进制的数( m o d   Q mod \space Q mod Q,经验上 P P P取131或13231, Q Q Q 2 64 2^{64} 264时出现冲突的概率很低。

例如,对于字符串"abcd",可看作 P P P进制数,转换成十进制数为 ( a ∗ P 3 + b ∗ P 2 + c ∗ P 1 + d ∗ P 0 )   m o d   Q (a*P^3+b*P^2+c*P^1+d*P^0)\space mod \space Q (aP3+bP2+cP1+dP0) mod Q

2)求出字符串的前缀哈希

例如对于字符串"abcd",需预处理出下面这些前缀字符串的哈希值:

子串哈希值
“” h [ 0 ] = 1   m o d   Q h[0] = 1\space mod\space Q h[0]=1 mod Q
“a” h [ 1 ] = a ∗ P 0   m o d   Q h[1] = a*P^0\space mod\space Q h[1]=aP0 mod Q
“ab” h [ 2 ] = ( a ∗ P 1 + b ∗ P 0 )   m o d   Q h[2]=(a*P^1+b*P^0)\space mod \space Q h[2]=(aP1+bP0) mod Q
“abc” h [ 3 ] = ( a ∗ P 2 + b ∗ P 1 + c ∗ P 0 )   m o d   Q h[3]=(a*P^2+b*P^1+c*P^0)\space mod \space Q h[3]=(aP2+bP1+cP0) mod Q
“abcd” h [ 4 ] = ( a ∗ P 3 + b ∗ P 2 + c ∗ P 1 + d ∗ P 0 )   m o d   Q h[4]=(a*P^3+b*P^2+c*P^1+d*P^0)\space mod \space Q h[4]=(aP3+bP2+cP1+dP0) mod Q

3)对于字符串的任意子串,假设起始索引为 l l l,终止索引为 r r r(包括在内),那么,该字符串子串的哈希值为 h [ r ] − h [ l ] ∗ P r − l + 1 h[r]-h[l]*P^{r-l+1} h[r]h[l]Prl+1

有个小技巧是,取模的数用 2 6 4 2^64 264,这样直接用unsigned long long存储,溢出的结果就是取模的结果。由于数值很大通常会溢出,而java中的long类型又为有符号整型,这里给出C++代码。代码模板如下:

// 假设字符串用char类型数组str[1...n]来存储与,起始位置为1,长度为n

typedef unsigned long long ULL;
ULL h[N], p[N];   // h[k]存储字符串前k个字母的哈希值,p[k]存储P^k mod 2^64

// 预处理
p[0] = 1;
for(int i = 1; i <= n; i ++){
    p[i] = p[i - 1] * P;
    h[i] = h[i - 1] * P + str[i];
}

// 计算子串str[l~r]的哈希值
ULL get(int l, int r){
    return h[r] - h[l] * p[r - l + 1];
}

上述字符串哈希看起来可能没啥用,但在有些场景下还是可以应用起来以达到优化的目的:

  • 在经过预处理后,可以在 O ( 1 ) O(1) O(1)的时间内判断字符串的任意子串是否相等。
  • 在一些以String作为key的哈希表中,查询的时候并不是 O ( 1 ) O(1) O(1),而与字符串的长度有关,这一时间复杂度常会被遗漏掉。可以考虑将这些作为key的字符串转换成对应的哈希值,这样的话可将查询的时间复杂度优化至 O ( 1 ) O(1) O(1)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值