哈希表的定义
无论是在顺序查找、二分查找、索引顺序查找,还算二叉排序树中,我们查找元素时,都需要逐个对比。那么,我们可不可以构造一种函数H,根据我们要查找关键字key和这个函数,可以直接确定查找值所在位置,直接找到数据,提升效率呢?
答案是肯定的,这就是我们这篇文章的主角,哈希表。
我们先引入一些定义:
散列(Hashing):通过把记录的关键字映射到表中的一个存储位置,实现根据关键字的值直接访问数据元素的过程
散列函数:把取值范围较大的关键字映射到某确定的存储位置的函数 ,也叫哈希函数。
哈希表:采用散列技术存放和访问记录的存储结构,也称散列表
槽:哈希表中的一个存储位置,也称散列地址或哈希地址
(哈希就是散列的音译,所以两者基本上是等价的)
来看一个例子:
关键字集合:S={apple, april, james, jeff, moon, safe, sail, sophia}
表长要求:30
哈希函数:H( k e y i key_i keyi ) = S[i][0] – ‘a’
计算散列地址:H(apple) = H(april) = 0
H(james)= H(jeff) = 9
H(moon) = 12
H(safe) = H(sail) = H(sophia) = 18
可以看到,上面的哈希函数过于简单,只通过首字母计算哈希地址,造成了很多重复。这显然是不好的,让我们换个哈希函数。
关键字集合:S={apple, april, james, jeff, moon, safe, sail, sophia}
表长要求:30
哈希函数:H(keyi ) = (S[i][0] + S[i][len-1]) mod 30
计算散列地址:
H(apple) = 18,
H(april) = 25
H(james) = 11
H(jeff) = 28
H(moon) = 9
H(safe) = 6
H(sail) = 13
H(sophia) = 2
很好!每个元素都有了哈希地址。
我们把上面的“重复”称之为“冲突”。
冲突:不同关键字通过相同的散列函数计算得到同一地址的现象,即 H(key1)=H(key2)的现象
哈希表的严格定义:根据设定的哈希函数和处理冲突的方法为一组记录建立 的一种存储结构或查找方式
冲突发生时,不同的元素被分到同一个哈希地址,我们称之为堆积。
堆积:不同基地址的元素争夺同一单元的现象,也称聚集,实质是在处理同 义词之间冲突时产生的非同义词之间的冲突
哈希表的操作:
CreateHashTable (&H); //创建哈希表
DestroyHashTable (&H); //销毁哈希表
SearchHash(H, kval, &p, &c); //哈希表上的查找
InsertHash(&H, e); //插入数据元素e至哈希表
DeleteHash(&H, kval); //删除给定键值的数据元素
哈希函数的构造方法
哈希函数H(key)的设计要求:
- ①计算开销小:对关键字做相对简单高效的算术或逻辑运算
- ②最少的冲突
- ③散列地址分布均匀:将大范围取值的关键字均匀映射到哈希表地址空间
- ④充分使用关键字信息:关键字长度、分布、散列表长度、元素查找频率等
- ⑤相对高的存储效率:散列表存储的元素个数与散列表长度的比值较大
常用的有以下几种方法:
直接定址法:Hash(key)=key 或 Hash(key)=a*key + b 其中a, b为常数。
如适龄人口统计表可以年龄作为关键字
平方取中法:Hash(key)=mid(keykey)
其中mid函数取key平方值的中间几位数字。如key=2346,mid(23462346)=mid(5503716)=037
折叠法:Hash(key)= k e y 1 + k e y 2 + … + k e y K key_1+key_2+…+key_K key1+key2+…+keyK
其中假设key可分为K段,且移位叠加时舍弃进位。如key=234688953327160,
K=4, 则Hash(key) = 2346+8895+3271+60=4572
除留余数法:Hash(key)=key mod p (p<=m) 其中p为不大于表长m且最接近m 的最大素数。如m=1000, p=997。(至于为什么选取素数,这牵涉到数论和统计学相关知识,在此不多做探讨)
可以看到,除留余数法分配的哈希地址堆积得均匀。
处理冲突的方法
哈希函数的定义域通常比值域大得多,所以冲突是不可避免的。
我们接下里介绍几种处理冲突的方法。
开放定址法 (闭散列法)
定义
探查序列:一组能够存放记录的散列地址序列,其中第一项为关键字的基位置
开放定址法:由哈希地址Hash(key)求得一个探查序列 H 1 , H 2 , … , H k , 0 < = k < = m − 1 H_1,H_2,…, H_k, 0<=k<=m-1 H1,H2,…,Hk,0<=k<=m−1, 冲突解决策略为依次检测该序列,找出第一个尚未被占用的地址作为新记录的存放地址
开放的含义:散列表中每个地址对所有数据元素均开放,允许其占用
闭散列的含义:散列表数组空间是封闭的,发生冲突时不再使用额外 的存储单元
探查序列的计算
H i = ( H a s h ( k e y ) + d i ) m o d m , i = 1 , 2 , . . . , k , k < = m − 1 H_i=(Hash(key)+d_i) mod m, i=1,2,...,k, k<=m-1 Hi=(Hash(key)+di)modm,i=1,2,...,k,k<=m−1其中di为递增序列,有三种取法:
① 线性探测法: d i = 1 , 2 , . . . , m − 1 d_i=1, 2, ..., m-1 di=1,2,...,m−1
② 二次探测法: d i = 1 2 , − 1 2 , 2 2 ,- 2 2 . . . , k 2 ,- k 2 , ( k < = ⌊ m / 2 ⌋ ) d_i=1^2 ,-1^2 ,2^2,-2^2 ...,k^2,-k^2,(k<=⌊m/2⌋) di=12,−12,22,-22...,k2,-k2,(k<=⌊m/2⌋)
③ 双重散列法: d i = ( d + i × H 2 ( k e y ) ) m o d m , 1 < = i < = m − 1 d_i=(d+i×H_2(key)) mod m, 1<=i<=m-1 di=(d+i×H2(key))modm,1<=i<=m−1
例如:
设一组关键字(07,15,20,31,48,53,64,76,82,99),试构建哈希表, 并利用线性探测法处理冲突,其中取m=11, Hash(key)=key mod 11
则,我们得到如下哈希表:
平均查找长度(成功):(34+13+32+31)/10=2.4
平均查找长度(失败):(9+8+7+6+5+4+3+2+1+11+10)/11=6
算法实现
typedef struct Hash {
ElemType *elem; // 记录存储基址
int size; // 记录数
int capacity; // 哈希表容量
} HashTable;
查找
/*哈希表上的查找*/
Status SearchHash(HashTable H, KeyType K, int &p, int &c)
{
c = 0;
p = Hash(K); // 求得哈希地址
while (H.elem[p].key != NULLKEY && !equal(K, H.elem[p].key))
{ // 该位置非空且发生冲突
collision(p, ++c); // 求得下一探查地址p
}
if (equal(K, H.elem[p].key))
return SUCCESS; // 查找成功,p返回待查数据元素位置
else
return FAIL; //查找不成功
} //SearchHash
插入
/*哈希表上的插入*/
Status InsertHash(HashTable &H, ElemType e) {
int c = 0, p = 0;
if (SearchHash(H, e.key, p, c) == SUCCESS )
return DUPLICATE; //表中已有与e相同的元素
if (c < H.capacity) {
//冲突次数c未达到上限(阀值c可调)
H.elem[p] = e;
++H.size;
return SUCCESS; //插入e
}
else {
RecreateHashTable(H); //重建哈希表
return FAIL;
}
} //InsertHash
拉链法(开散列法)
拉链法:将所有关键字为同义词(即具有相同的哈希函数值)的记录存贮在同一线性链表中,而哈希表中下标为i的分量存储哈希函数值为i的链表的头指针,也称链地址法或开散列法。
开散列的含义:指链表结点可动态申请,存储空间充足,不会发生存储溢出。
算法实现
存储结构
typedef struct {
ElemType data;
LHNode *next; //后继指针
}LHNode, *LHNodeptr;
typedef struct {
LHNodeptr *elem;
int size; //记录数
int capacity; //哈希表容量
} LHashTable;
查找
/*哈希表上的查找*/
Status SearchHash(LHashTable H, KeyType kval,
LHNodeptr &p, int &c) {
int c = 0;
p = H.elem[Hash(kval)];
while (p && p->data.key != kval) {
q = p; //q紧随p
p = p->next;
c++;
}
if (p)
return SUCCESS;
else {
p = q;
return FAIL;
}
} //SearchHash
插入
/*哈希表上的插入*/
Status InsertHash(LHashTable &H, ElemType e) {
if(SearchHash(H, e.key, p, c) == SUCCESS)
return DUPLICATE;
else if (c < hashsize[H.sizeindex] / 2) {
s = new LHNode;
s->data = e;
p->next = s;
H.size++;
return OK;
}
else
recreateHashTable(H); //重建哈希表
} //InsertHash
性能对比
我们先定义装填系数:
α
=
n
/
m
其中
m
为哈希表分配空间,
n
为填入的记录数
.
α=n/m\\ 其中m为哈希表分配空间,n为填入的记录数.
α=n/m其中m为哈希表分配空间,n为填入的记录数.