散列表、常见散列函数及处理冲突的方法


散列表的概念

散列法

散列法又称哈希法或杂凑法,它在元素的存储位置与元素关键码之间建立一个确定的对应函数关系Hash(),使得每个关键码与结构中的一个唯一的存储位置相对应:
A d d r e s s = H a s h ( k e y ) Address = Hash(key) Address=Hash(key)

 在插入时,依此函数计算存储位置并按此位置存放。
 在查找时,对元素的关键码进行同样的函数计算,把求得的函数值当作元素的存储位置,在结构中按此位置取元素比较,若关键码相等,则查找成功。这种方法就称作散列法

散列函数

 在散列法中使用的转换函数叫做散列函数

散列表

 按照散列法构造出来的表或结构就叫做散列表
 散列表是根据关键字直接进行访问的数据结构,也就是说,散列表建立了关键字和存储位置之间的一种直接映射关系


常见的散列函数

<1> 直接定址法

对关键码做一个线性计算,把计算结果当作散列地址。
H ( k e y ) = a × k e y + b H(key) = a × key + b H(key)=a×key+b
其中, a a a b b b都是常数。
这种方式计算简单,且不会产生冲突。它适合关键字的分布基本连续的情况;若关键分布不连续,空位较多,则会造成存储空间的浪费。

<2> 除留余数法

设散列表的表长为 m m m,取一个不大于 m m m但最接近或等于 m m m的质数 p p p,利用以下公式将关键字转换成散列地址:
H ( k e y ) = k e y   %   p H(key) = key\ \%\ p H(key)=key % p
其中, % \% %是整数除法取余的运算。

除留余数法的关键是选好 p p p,使得每个关键字通过该函数转换后等概率地映射到散列空间上地任一地址,从而尽可能减少冲突的可能性。

<3> 数字分析法

设关键字是r进制数,而r个数码在各位上出现的频率不一定相同,可能在某些位上分布均匀一些,每种数码出现的机会均等;而在某些位上分布不均匀,只有某几种数码经常出现,此时应选取数码分布较均匀的若干位作为散列地址。

这种方法适合于已知的关键字集合,若更换了关键字,则需要重新构造新的散列函数

<4> 平方取中法

这种方法取关键字的平方值的中间几位作为散列地址。

这种方法得到的散列地址与关键字的每位都有关系,因此使得散列地址分布比较均匀,适用于关键字的每位取值不够均匀或均小于散列地址所需的位数。

<5> 折叠法

将关键字分割成位数相同的几部分(最后一部分的位数可以短一些),然后取这及部分的叠加和作为散列地址,这种方法称为折叠法。

当关键字位数很多,而且关键字中的每位上数字分布大致均匀时,可以采用折叠法得到散列地址。


处理冲突的方法

 可以注意到,任何设计出来的散列函数都不可能绝对地避免冲突。为此,必须考虑在发生冲突时应该如何处理,即为产生冲突的关键字寻找下一个“空”的Hash地址


处理冲突的开放地址法

所谓开放地址法,是指可存放新元素的空闲位置既向它的同义词元素开放,又向它的非同义词元素开放。就是说,所有元素都放在一个散列表的基本空间之内。这里的同义词指的是那些散列地址相同的不同关键码

其数学递归公式是:
H i = (   H ( k e y ) + d i   )   %   m H_i = (\ H(key) + d_i\ )\ \% \ m Hi=( H(key)+di ) % m
其中 H i H_i Hi 表示发生冲突后第 i i i 次探测的散列地址; i = 0 , 1 , 2 , . . . , k ( k ≤ m − 1 ) i=0,1,2,...,k(k≤m-1) i=0,1,2,...,k(km1) m m m表示散列表表长; d i d_i di为增量序列

确定某一增量序列后,对应的处理方法就是确定的。通常由以下4中取法:

<1> 线性探测法

d i = 0 , 1 , 2 , . . . , m − 1 d_i = 0,1,2,...,m-1 di=0,1,2,...,m1时,称为线性探测法

这种方法的特点:冲突发生时,顺序查看表中下一个单元(探测到表尾地址m-1时,下一个探测地址是表首地址0),直到找出一个空闲单元(当表未填满时一定能找到一个空闲单元)或查遍全表

缺点:线性探测法可能使第 i i i个散列地址的同义词存入第 i + 1 i+1 i+1个散列地址,这样本应存入第 i + 1 i+1 i+1个散列地址的元素就争夺第 i + 2 i+2 i+2个散列地址的元素的地址……从而造成大量元素在相邻的散列地址上“聚集”起来,大大降低了查找效率。


例如,关键码为 { 37 , 25 , 14 , 36 , 49 , 68 , 57 , 11 } \{37,25,14,36,49,68,57,11\} {3725143649685711},散列函数为 H a s h ( k e y ) = k e y % 11 Hash(key) = key \% 11 Hash(key)=key%11,则各关键码计算出的地址为:
H a s h ( 37 ) = 4 Hash(37)=4 Hash(37)=4 H a s h ( 25 ) = 3 Hash(25)=3 Hash(25)=3 H a s h ( 14 ) = 3 Hash(14)=3 Hash(14)=3 H a s h ( 36 ) = 3 Hash(36)=3 Hash(36)=3
H a s h ( 49 ) = 5 Hash(49)=5 Hash(49)=5 H a s h ( 68 ) = 2 Hash(68)=2 Hash(68)=2 H a s h ( 57 ) = 2 Hash(57)=2 Hash(57)=2 H a s h ( 11 ) = 0 Hash(11)=0 Hash(11)=0

得到的散列结果如图所示:

散列地址01234567891011
关键字1168253714364957
比较次数11113437

那么 A S L 成 功 = 1 8 × ( 1 + 1 + 1 + 1 + 3 + 4 + 3 + 7 ) = 21 8 ASL_{成功} = \frac{1}{8} × (1+1+1+1+3+4+3+7) = \frac{21}{8} ASL=81×(1+1+1+1+3+4+3+7)=821


<2> 平方探测法

d i = 0 2 , 1 2 , − 1 2 , 2 2 , − 2 2 , . . . , k 2 , − k 2 d_i = 0^2, 1^2,-1^2,2^2,-2^2,...,k^2,-k^2 di=02,12,12,22,22,...,k2,k2时,称为平方探测法,其中 k ≤ m / 2 k≤m/2 km/2,散列表长度必须是一个可以表示成 4 k + 3 4k+3 4k+3的素数,又称为二次探测法

平方探测法是一种较好的处理冲突的方法,可以避免出现“堆积”问题,它的缺点不能探测到散列表上的所有单元,但至少能探测到一半单元

例如,关键码为 { 37 , 25 , 14 , 36 , 49 , 68 , 57 , 11 } \{37,25,14,36,49,68,57,11\} {3725143649685711},散列函数为 H a s h ( k e y ) = k e y % 19 Hash(key) = key \% 19 Hash(key)=key%19,则各关键码计算出的地址为:
H a s h ( 37 ) = 18 Hash(37)=18 Hash(37)=18 H a s h ( 25 ) = 6 Hash(25)=6 Hash(25)=6 H a s h ( 14 ) = 14 Hash(14)=14 Hash(14)=14 H a s h ( 36 ) = 17 Hash(36)=17 Hash(36)=17
H a s h ( 49 ) = 11 Hash(49)=11 Hash(49)=11 H a s h ( 68 ) = 11 Hash(68)=11 Hash(68)=11 H a s h ( 57 ) = 0 Hash(57)=0 Hash(57)=0 H a s h ( 11 ) = 11 Hash(11)=11 Hash(11)=11

得到的散列结果如图所示:

散列地址0123456789101112131415161718
关键字5725114968143637
比较次数11312111

那么 A S L 成 功 = 1 8 × ( 1 + 1 + 3 + 1 + 2 + 1 + 1 + 1 ) = 11 8 ASL_{成功} = \frac{1}{8} × (1+1+3+1+2+1+1+1) = \frac{11}{8} ASL=81×(1+1+3+1+2+1+1+1)=811

<3> 再散列法

d i = H a s h 2 ( k e y ) d_i = Hash_2(key) di=Hash2(key)时,称为再散列法,又称为双散列法

需要使用两个散列函数,当通过第一个散列函数 H a s h ( k e y ) Hash(key) Hash(key)得到的地址发生冲突时,则利用第二个散列函数 H a s h 2 ( k e y ) Hash_2(key) Hash2(key)计算该关键字的地址增量。
它的具体散列函数形式如下:
H i = (   H ( k e y ) + i   × H a s h 2 ( k e y )   )   %   m H_i = (\ H(key) + i\ × Hash_2(key)\ )\ \% \ m Hi=( H(key)+i ×Hash2(key) ) % m
初始探测位置 H 0 = H a s h ( k e y ) % m H_0 = Hash(key)\%m H0=Hash(key)%m i i i是冲突次数,初始为0。

在散列法中,最多经过 m − 1 m-1 m1次探测就会遍历表中的所有位置,回到 H 0 H_0 H0位置。

例如,关键码为 { 37 , 25 , 14 , 36 , 49 , 68 , 57 , 11 } \{37,25,14,36,49,68,57,11\} {3725143649685711},散列函数为 H a s h ( k e y ) = k e y % 11 Hash(key) = key \% 11 Hash(key)=key%11,则各关键码计算出的地址为:
H a s h ( 37 ) = 4 Hash(37)=4 Hash(37)=4 H a s h ( 25 ) = 3 Hash(25)=3 Hash(25)=3 H a s h ( 14 ) = 3 Hash(14)=3 Hash(14)=3 H a s h ( 36 ) = 3 Hash(36)=3 Hash(36)=3
H a s h ( 49 ) = 5 Hash(49)=5 Hash(49)=5 H a s h ( 68 ) = 2 Hash(68)=2 Hash(68)=2 H a s h ( 57 ) = 2 Hash(57)=2 Hash(57)=2 H a s h ( 11 ) = 0 Hash(11)=0 Hash(11)=0

取第二个散列函数为 H a s h 2 ( k e y ) = 7 − ( k e y % 7 ) Hash_2(key) = 7-(key\%7) Hash2(key)=7(key%7),各关键码相关的地址增量为
H a s h 2 ( 37 ) = 5 Hash_2(37)=5 Hash2(37)=5 H a s h 2 ( 25 ) = 3 Hash_2(25)=3 Hash2(25)=3 H a s h 2 ( 14 ) = 7 Hash_2(14)=7 Hash2(14)=7 H a s h 2 ( 36 ) = 6 Hash_2(36)=6 Hash2(36)=6
H a s h 2 ( 49 ) = 7 Hash_2(49)=7 Hash2(49)=7 H a s h 2 ( 68 ) = 2 Hash_2(68)=2 Hash2(68)=2 H a s h 2 ( 57 ) = 6 Hash_2(57)=6 Hash2(57)=6 H a s h 2 ( 11 ) = 3 Hash_2(11)=3 Hash2(11)=3

得到的散列结果如图所示:

散列地址012345678910
关键字1168253749573614
比较次数11111222

那么 A S L 成 功 = 1 8 × ( 1 + 1 + 1 + 1 + 1 + 2 + 2 + 2 ) = 11 8 ASL_{成功} = \frac{1}{8} × (1+1+1+1+1+2+2+2) = \frac{11}{8} ASL=81×(1+1+1+1+1+2+2+2)=811

<4> 伪随机序列法

d i = 伪 随 机 数 序 列 d_i = 伪随机数序列 di=时,称为伪随机序列法。

注意!!!!

 在开放定址的情形下,不能随便物理删除表中已有元素,因为若删除元素,则会截断其他具有相同散列地址的元素的查找位置。
 因此,要删除一个元素时,可给它作一个删除标记,进行逻辑删除。但这样做的副作用是,执行多次删除后,表面上看起来散列表很满,实际上有许多个位置未利用,因此需要定期维护散列表,要把删除标记的元素物理删除。
 故当散列表经常变动时,最好用拉链法来处理冲突


处理冲突的拉链法

显然,对于不同的关键字可能会通过散列函数映射到同一地址,为了避免非同义词发生冲突,可以把所有的同义词存储在一个线性链表中,这个线性链表由其散列地址唯一标识。
假设散列地址为 i i i 的同义词链表的头指针存放在散列表的第 i i i 个单元中,因而查找、插入和删除操作主要在同义词链中进行。
拉链法适用于经常进行插入和删除的情况。

例如,关键字序列为 { 12 , 15 , 16 , 22 , 25 , 29 , 34 , 37 , 47 , 48 , 56 , 67 } \{12,15,16,22,25,29,34,37,47,48,56,67\} {121516222529343747485667},散列函数为 H a s h ( k e y ) = k e y % 13 Hash(key) = key \% 13 Hash(key)=key%13,用拉链法处理冲突,建立的表如下图所示:
在这里插入图片描述

散列查找及性能分析

散列表的查找过程与构造散列表的过程基本一致。
对于一个给定的关键字key,根据散列函数可以计算出其散列地址,执行步骤如下:
初始化: A d d r = H a s h ( k e y ) Addr = Hash(key) Addr=Hash(key)
① 检查表中地址为 A d d r Addr Addr的位置上是否有记录,若无记录,返回查找失败;若有记录,比较它与 k e y key key的值,若相等,则返回查找成功标志,否则执行步骤②
② 用给定的处理冲突方法计算“下一个散列地址”,并把 A d d r Addr Addr置为此地址,转入步骤①。

散列表的查找效率取决于三个因素:散列函数处理冲突的方法装填因子

装填因子

散列表的装填因子一般记为 α \alpha α,定义为一个表的装满程度,即
α = 表 中 记 录 数 n 散 列 表 长 度 m \alpha = \frac{表中记录数n}{散列表长度m} α=mn
散列表的平均查找长度依赖于散列表的装填因子 α \alpha α,而不直接依赖于 n n n m m m
直观来看, α \alpha α越大,表示装填的记录越“满”,发生冲突的可能性越大,反之发生冲突的可能性越小。

  • 8
    点赞
  • 35
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
C++语言中的二次探测散列法是一种常见散列表实现方法,用于解决哈希冲突问题。其基本思想是,当哈希函数计算出的位置已经被占用时,按照一定的规则在散列表中查找下一个空闲位置,直到找到为止。 具体实现过程如下: 1. 对于给定的关键字,通过哈希函数计算出它在散列表中的位置。 2. 如果该位置已经被占用,则按照二次探测法的规则,在散列表中查找下一个空闲位置。具体规则为,从当前位置开始,依次查找 $(1^2, 2^2, 3^2, \ldots)$ 个位置,直到找到一个空闲位置为止。 3. 如果散列表已经满了,则无法插入新的关键字。 4. 在查找时,如果找到了与待查找关键字相等的关键字,则说明该关键字已经存在于散列表中。 下面是二次探测散列法的C++代码实现: ```cpp const int MAX_SIZE = 10007; // 哈希表中的每个元素结构体 struct HashNode { int key; // 关键字 int value; // 值 }; // 哈希表类 class HashTable { public: HashTable(); ~HashTable(); void Insert(int key, int value); void Remove(int key); int Find(int key); private: HashNode* data; // 哈希表数据 int size; // 哈希表大小 int count; // 哈希表中元素个数 int HashFunc(int key); // 哈希函数 int GetNextPos(int pos, int i); // 获取下一个空闲位置 }; HashTable::HashTable() { size = MAX_SIZE; count = 0; data = new HashNode[size]; } HashTable::~HashTable() { delete[] data; } void HashTable::Insert(int key, int value) { int pos = HashFunc(key); int i = 0; while (data[pos].key != -1 && i < size) { pos = GetNextPos(pos, i); i++; } if (i >= size) { cout << "HashTable is full." << endl; return; } data[pos].key = key; data[pos].value = value; count++; } void HashTable::Remove(int key) { int pos = HashFunc(key); int i = 0; while (data[pos].key != key && i < size) { pos = GetNextPos(pos, i); i++; } if (i >= size) { cout << "Key not found." << endl; return; } data[pos].key = -1; data[pos].value = -1; count--; } int HashTable::Find(int key) { int pos = HashFunc(key); int i = 0; while (data[pos].key != key && i < size) { pos = GetNextPos(pos, i); i++; } if (i >= size) { cout << "Key not found." << endl; return -1; } return data[pos].value; } int HashTable::HashFunc(int key) { return key % size; } int HashTable::GetNextPos(int pos, int i) { return (pos + i * i) % size; } ``` 在上面的代码中,`HashTable` 类封装了二次探测散列法的实现细节,提供了插入、删除和查找操作。其中,`data` 数组存储了哈希表中的元素,`size` 表示哈希表的大小,`count` 表示哈希表中元素的个数。`HashFunc` 函数实现了哈希函数,`GetNextPos` 函数实现了获取下一个空闲位置的规则。在插入、删除和查找操作中,都需要通过哈希函数计算出待操作关键字在哈希表中的位置,然后按照二次探测法的规则查找下一个空闲位置或者待操作关键字所在的位置。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值