【C# 数据结构与算法】哈希函数 hash

概览

一、引言

散列表(Hash Table),哈希表是一种组合的数据结构,特点是:数据元素的关键字与其存储地址直接相关,它通常的实现方式是数组加链表,或者数组加红黑树。哈希表是一种牺牲空间去换取时间的数据结构,需要在空间与时间上有取舍,哈希表是时间和空间之间的平衡。哈希表的核心是哈希函数,哈希表最关键的问题哈希冲突也是取决于哈希函数的设计。

散列表(Hash table,也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构。 也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。 这个映射函数叫做散列函数,存放记录的数组叫做散列表

二、哈希函数

1、什么是哈希函数

哈希函数是一种将“”转换为“索引”的逻辑规则。它的设计好坏对哈希表的性能影响巨大。优良的哈希函数能够最大程度的减少哈希冲突,使得哈希表中的元素分布得尽可能的均匀,离散程度更大,这样哈希表就会性能优越;较差的哈希函数设计,带来的可能是一场灾难,哈希冲突严重,空间利用率低,时间复杂度呈线性恶化,造成频繁的扩容操作。

hash算法是对二进制进行操作,所以只要一个字符发生变化,都会改变哈希的结果。

 下图就是Hash函数的一个简单说明,任意长度的数据通过HashFunc映射到一个较短的数据集中。

 

 

2、哈希函数的设计

对于哈希函数的设计,以下举了一些简单的例子,都基本类型转换成整型处理,并不是唯一的方法,仅供参考。以下简单列了几条设计原则。

  • 一致性:如果a == b,则hash(a) == hash(b)(hash函数)
  • 高效性:哈希函数计算高效简便
  • 均匀行:哈希值均匀分布(质数)
  •  

     

当然了同时设计满足这三个条件的哈希函数是专家们的事。于是C#令所有数据类型都继承了父类object并且实现了一个能返回一个int型的GetHashCode()方法,我们C#程序员在使用时只需要调用GetHashCode()方法即可,我们没有理由不相信它们。

3、哈希函数特点

单向:不可能,不能根据hashcode推出key

压缩:

定长:输入可以任意长度,输出是固定长度

碰撞/冲突:防碰撞特性(Collisionresistance)

高灵敏:改一点点,都能产生完全不一样的hashcode

速度快:计算hash值的速度比较快


 

哈希操作

1、整型

  • 小范围正整数可直接使用
  • 小范围负整数进行偏移

 

 

 

  • 大整数
  • 将整数散列最常用的方法就是取余。选择大小为M的数组(素数),数值%M =数组下标,这样可以将键有效的散布在从0到M-1的范围之内。。素数能减少哈希冲突的次数,哈希冲突是无法避免的。

 

 

 

说明:上图(左)与一个不合适的合数取模,获得的索引冲突严重,不可取;而上图(右)与一个质数取模,明显获得的索引分布更均匀,离散程度更好。点击获取合适的哈希表素数

2、浮点型

  • 浮点型转换成整型处理。如果键是o到1之间的小数,我们可以将键表示为二进制整数,然后再进行取余。
  • 注意:(浮点数的算法不是固定的,可以自行通过研究不同浮点型hash算法)

 

 

 

3、字符串型.

  字符串的算法不是固定的,可以自行可以通过研究不同字符串hash算法

  • 字符串型转换成整型处理,方法不是固定,可以通过研究不同字符串hash算法


如果键是字符串类型,我们同样将键表示为整数,然后再进行取余。

“123”=1*10^2+2*10^1+3*10^0
“abc”=a*2612 + b*26^1+c*26^Oabc = 97*2612+98*26^1+99*2610
=26*(97*26+98)+99
=68219

 

 

说明:其中B为一个常数,M为一个合适的质数。上图将一个字符串型加入一些规则映射成了一个整型。

4、日期类型

  • 日期类型转换成整型处理

 

 

三、哈希冲突的处理

1、链地址法(拉链法 Separate Chaining)

也叫开散列方法,将取模后相同地址,存储在这个地址所指向的链表中。

拉链法是解决哈希冲突的一种行之有效的方法,某些哈希地址可以被多个关键字值共享,这样可以针对每个哈希地址建立一个单链表。

在拉链(单链表)的哈希表中搜索一个记录是容易的,首先计算哈希地址,然后搜索该地址的单链表

拉链法散列函数 设计

(1)除留余数法——H(key) = key %b
散列表表长为m,取一个不大于m但最接近或等于m的质数p
(2)直接定址法—— H(key) = key 或 H(key) = a*key + b
其中,a和b是常数。这种方法计算最简单,且不会产生冲突。它适合关键字的分布基本连续的情况,若关键字分布不连续,空位较多,则会造成存储空间的浪费。
例:存储同一个班级的学生信息,班内学生学号一般都是连续的,班内学生学号为(1120112176~1120112205),设计的散列函数如下:

H(key) = key - 1120112176

(3)数字分析法——选聊数码分布较为均匀的若干位作为散列地址
设关键字是r进制数(如十进制数),而r个数码在各位上出现的频率不一定相同,可能在某些位上分布均匀一些,每种数码出现的机会均等;而在某些位上分布不均匀,只有某几种数码经常出现,此时可选取数码分布较为均匀的若干位作为散列地址。这种方法适合于已知的关键字集合,若更换了关键字,则需要重新构造新的散列函数。

 

 

 例:以“手机号码”作为关键字设计散列函数,电话号码的头部分布不均匀,后四位分布均匀。
138XXXX2875
138XXXX1682

138XXXX9125
199xXXX1684

199XXXX1236

 

 (4)平方取中法――取关键字的平方值的中间几位作为散列地址。
具体取多少位要视实际情况而定。这种方法得到的散列地址与关键字的每位都有关系,因此使得散列地址分布比较均匀,适用于关键字的每位取值都不够均匀或均小于散列地址所需的位数。

例︰要存储整个学校的学生信息,以“身份证号”作为关键字设计散列函数,对整个身份证平方,取中间5位

 

 


查找长度

注意:查找长度不包括 索引表

成功的平均查找长度ASL

 

失败的平均查找长度ASLASL失败

 

负载因子与rehash

也叫装载因子=ASL失败,我们可以使用装载因子来衡量散列表的“健康状况”。

装载因子=ASL失败

散列表的负载因子 = 填入表中的元素个数/散列表的长度

散列表负载因子越大,代表空闲位置越少,冲突也就越多,散列表的性能会下降。

对于散列表来说,负载因子过大或过小都不好,负载因子过大,散列表的性能会下降。而负载因子过小,则会造成内存不能合理利用,从而形成内存浪费。因此我们为了保证负载因子维持在一个合理的范围内,要对散列表的大小进行收缩或扩展,即rehash。散列表的rehash过程类似于数组的收缩与扩容。

 

 

应用

C#Dictionary数据结构就是拉链法实现的,Dictionary<Tkey,Tvalue>是Hastbale的泛型实现。两种内部实现差不多。linq对Dictionary操作是否便捷

C#Dictionary用的是拉链法+开放地址法的线性探测,它的转载因子是0.72

 

 

说明:哈希冲突的元素可以用链表这种线性的数据结构保存,当然也可以用平衡树结构保存,Java8中的哈希表实现,当哈希冲突达到一定的程度,会将链表替换成红黑树,前提是哈希表中原本的元素具备可比较性。

2、开放地址法

为产生冲突的地址H(key)求得一个地址
所谓开放定址法,也叫闭散列法,是指可存放新表项的空闲地址既向它的同义词表项开放,又向它的非同义词表项开放。其数学递推公式为:
同义词:散列后相同的数值
H=(H(key) + di) % m

i=0,1,2....,k (k≤m -1) ,m表示散列表表长;di为增量序列;i可理解为“第i次发生冲突”

 

(1)、线性探测

①线性探测法一一 di (增量序列)=0,1,2,3,..., m-1;即发生冲突时,每次往后探测相邻的下一个单元是否为空

说明:当遭遇哈希冲突时,会按照规则顺延往下找空挡位置插入元素,这种方式如果哈希冲突比较严重,会造成寻找空挡位置效率变低。哈希表性能变差。这种方式就是要设置合适的哈希表容量。

缺点:线性探测法很容易造成同义词火非同义亏的“聚集(堆积)”现象,严重影响查找效率,为了解决这个问题,引入了平方探测法。

删除实例:

将删除掉的数字用一个Tag标记,不能直接删除,直接删除会形成空值,会影响查找。

 

 

 

添加实例:

 

 

 

 注意:采用该种方法,不能直接删除元素(直接删除后会产生null 影响元素查找),要用Tag标记为已经删除,如下图:


(2) 平方探测法

当di (增量序列)= 02,12,-12,22,-22,..., k2,-k2时,称为平方探测法,又称二次探测法其中k<=m/2,:散列表长度m必须是一个可以表示成4j+3的素数。

 

 非重点小坑:散列表长度m必须是一个可以表示成4j+3的素数(答案在数论中),才能探测到所有位置

当表长为8时候,并不能探测所有的表中元素。

 

 

 

 

说明:这种方式相对于线性探测,加大了寻找空挡位置的步长。

(3)、伪随机序列法

 di(增量序列)是一个伪随机序列,如d= o(5,24,11,..
(4)再散列法(再哈希法)

除了原始的散列函数H(key之外,多准备几个散列函数,当散列函数冲突时,用下一个散列函数计算一个就地址,直到不冲突为止:

 

 

 

 

 

 

说明:当遭遇哈希冲突后,再运行一个哈希函数,对上一次哈希运算结果再计算一次结果,二次运算后得到的索引位置,是一个空挡位置的几率增大。

四、哈希表动态空间处理

 

 

 

说明:当哈希冲突达到一个所能容忍的上界位置时(upperTol),对哈希表进行扩容操作,以减少哈希冲突;当哈希冲突降低到一个下界位置(lowerTol)时,对哈希表进行缩容操作,以节约空间


 

posted @ 2022-06-04 10:56  小林野夫  阅读(1889)  评论(0编辑  收藏  举报
原文链接:https://www.cnblogs.com/cdaniu/