数据结构:哈希表

哈希表的定义

无论是在顺序查找、二分查找、索引顺序查找,还算二叉排序树中,我们查找元素时,都需要逐个对比。那么,我们可不可以构造一种函数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(2346
2346)=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<=m1, 冲突解决策略为依次检测该序列,找出第一个尚未被占用的地址作为新记录的存放地址
开放的含义:散列表中每个地址对所有数据元素均开放,允许其占用
闭散列的含义:散列表数组空间是封闭的,发生冲突时不再使用额外 的存储单元

探查序列的计算

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<=m1其中di为递增序列,有三种取法:
线性探测法 d i = 1 , 2 , . . . , m − 1 d_i=1, 2, ..., m-1 di1,2,...,m1
二次探测法 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=121222,-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<=m1

例如:

设一组关键字(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为填入的记录数.
三种处理冲突方法的性能对比

在这里插入图片描述
在这里插入图片描述

  • 20
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值