浅谈哈希表(HashTable)

概述

散列表(Hash table,也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。
给定表M,存在函数f(key),对任意给定的关键字值key,代入函数后若能得到包含该关键字的记录在表中的地址,则称表M为哈希(Hash)表,函数f(key)为哈希(Hash) 函数。

哈希表是一种通过哈希函数将特定的键映射到特定值的一种数据结构,他维护者键和值之间一一对应关系。

  • 键(key):又称为关键字。唯一的标示要存储的数据,可以是数据本身或者数据的一部分。
  • 槽(slot/bucket):哈希表中用于保存数据的一个单元,也就是数据真正存放的容器。
  • 哈希函数(hash function):将键(key)映射(map)到数据应该存放的槽(slot)所在位置的函数。
  • 哈希冲突(hash collision):哈希函数将两个不同的键映射到同一个索引的情况。

这么讲真的太太太抽象了,我举个例子吧



Example1这里面的h函数即为哈希函数,由算法很容易知道为x对5取余数 h(47)明显为2
Example2中的函数同理

访问键值k

对于一个存储n个数据的哈希表,则哈希表中为HT[0]~HT[N-1],但我们对于访问的键值为2的槽,只需要使用HT[2]即可

Hash Fuction哈希函数

这个有很多,举几个经典的算法

int h1(int x){
  return (x%5);
}

int h2(char* x){
  int i,sum;
  for(sum=0, i=0; x[i] != '\0'; i++)
    sum += (int)x[i];
  return (sum%5);
}

int ELFhash(char*key)
{
    unsigned long h=0;
    while(*key)
    {
        h = (h << 4) + *key++;
        unsigned long g = h & 0xF0000000L;
        if(g)
            h ^= g >> 24;
        h &= ~g;
    }
    return h % MOD;
}

ELFhash这个字符串hash函数中涉及许多位运算,有兴趣的可以上网查查,这里就不详述了。

Hash Collision哈希冲突


显而易见,哈希冲突就是指不同的键值k1,k2在哈希函数h(x)映射下到了相同的slot
自然,有冲突就会有解决方案。

Open Hashing 拉链法

名词解释:叫拉链,是因为哈希冲突后,用链表去延展来解决。


这个图中h(x) = x mod 10 ;是一个哈希函数,可见代入x=91x=1的时候都会到映射到键值为1的slot,那么这样就会引发冲突,在拉链法中,用链表延展去存储同键值的数据。
优点

  1. 拉链法处理冲突简单,且无堆积现象,即非同义词决不会发生冲突,因此平均查找长度较短;
  2. 在用拉链法构造的散列表中,删除结点的操作易于实现

缺点
在对链表进行存储空间分配的时候,会降低整个程序的运行速率

Closed Hashing 开地址法 (Open Addressing)

名词解释:叫Closed,是因为哈希冲突后,并不会在本身之外开拓新的空间,而是继续顺延下去某个位置来存放,所以是一个密闭的空间,所以叫“Closed”,至于开地址(Open Addressing),这个应该相对于那种通过链表来开拓新空间,它是在本身地址上,另外找个位置。所以叫开地址。

以下介绍常用的开地址法

  1. Bucket Hashing哈希桶
  2. Probing 探测
Bucket Hashing 哈希桶

这一页PPT应该很好理解,采用的是哈希桶(Bucket Hashing),每个桶的大小为2,一共有5个桶,所以一共有5*2=10个槽(slot)
插入的算法很简单

  1. 把数据放到经过哈希函数后得到key值第一个slot里
  2. 若slot已经被占用,用下一个slot
  3. 如果bucket满了,把数据放到overflow里面

查找算法也很简单

  1. 先得到键值key
  2. 再在bucket中找第一个第二个
  3. 若找不到再在overflow中找
Probing探测

p(k,i)探测函数。其值为第i次探测时相对h(k)的便宜

Pos(i)=(h(k) + p(k ,i)) % M;


Linear Probing 线性探测

p(i) = i ;
p(i) = i * c ;

注意这里的c必须与表的大小M互质

但是线性探测有一个很明显的缺点,就是数据很可能会聚集在一块
Quadratic Probing 平方探测


例子

p(1)=1;
p(2)=4;
p(2)=9;
对于h(k1)=3
h(k1)+p(1)=h(k1)+1=4
h(k1)+p(2)=h(k1)+4=7
h(k1)+p(3)=h(k1)+9=12

Random Probing 随机探测

p(k,i) = random();

但是并不存在真随机,如果存在真随机,会出现slot无法被哈希函数搜索到
Pseudo-Random Probing 伪随机探测


看图中的
探测序列为r1,r2,r3...即

p(1)=r1;
p(2)=r2;
p(2)=r3;
对于h(k1)=30
h(k1)+p(1)=h(k1)+2=32
h(k1)+p(2)=h(k1)+5=35
h(k1)+p(3)=h(k1)+32=62

Double Hashing 双重散列
这是用于开放寻址法的最好方法之一,具有随机选择排列的许多特性



h2(k)的值必须要与表的大小M互质

删除

  1. Tombstones墓碑
  2. Local Reorganization
  3. Rehash

where $\mu$ is the mean value, $\sigma^2$ is standard deviation

参考

  • 百度百科
  • 哈希表概述
  • Open Hash
  • 算法导论第三版
  • 华工计算机科学与工程学院数据结构课程PPT

推荐阅读更多精彩内容