细谈散列表系列一共有三篇文章
1、散列表的概述
2、散列函数的作用与构造
3、散列表查找的代码实现
文章目录
上篇文章我们提到,散列函数在存储时会发生冲突,并且也解释了冲突是如何产生的。
但我们先不着急去解决冲突的问题,我们首先来补充一个概念,散列表是基于散列函数建立的一种查找表。
那散列函数在查找中,具体是干什么的呢?
别急,本章内容就带你了解散列函数的作用以及构造
1、查找时的:散列函数?
- 散列函数就是根据key 计算出应该存储位置的位置;
- 根据这个函数和查找关键字key,可以直接确定查找值所在位置;
- 地址index = f(key)。
简单来说就是,存哪去?存哪了?
2、散列函数的构造方法
散列函数的构造方法一共有六种,并且每种方法的适应场景都不一样。
1、直接定址法
- 取关键字的某个线性函数值为散列地址
- 如:f(key) = a * key + b
- 优点:简便、均匀,也不会产生冲突
- 缺点:仅限于地址大小 = 关键字集合的情况
- 简单,但不常用
- 需要事先知道关键字的分布情况
- 适合查找表较小且连续的情况
2、数字分析法
- 抽取
- 抽取方法是使用关键字的一部分来计算散列存储位置的方法。
- 散列函数中常常用到的手段
- 假设关键字集合中的每个关键字key 都由s 位数字组成(k1, k2, k3, ···,kn),分析key 中的全体数据,并从中提取分布均匀的若干位或他们的组合构成全体。
- 分布均匀的意思是,不容易重复或冲突。
- 通常用于处理关键字位数较长的情况
如果事先知道关键字的分布且关键字的若干位分布较均匀,就可以考虑用这个方法。
3、平方取中法
- 关键字的每一位都有某些数字重复出现频率很高的现象,可以先求关键字的平方值,通过平方扩大差异,而后取中间数作为最终存储地址
适合于不知道关键字的分布,而位数又不是很大的情况。
4、折叠法
- 将关键字从左到右分割成位数相等的几部分
- 最后一部分位数不够可以短些
- 将这几部分叠加求和
- 并按照散列表表长,取后几位作为散列地址
- 有时这可能还不能够保证分布均匀,不妨从一端向另外一端来回折叠后对齐相加。
事先不需要知道关键字的分布,适合关键字位数较多的情况。
5、除留余数法
- 常用的构造散列函数方法
- H(key)= key MOD p
- p <= m
- m 为表长
- H(key)= key MOD p
- 如何选取p 为关键
- 本方法的关键在于选取合适的p,p如果选的不好,可能会容易产生同义词
- p 应为小于等于m,最好是接近m 的最小质数
- 质数:大于1的自然数,并且只能被1和本身整除
- 或是不包含小于20 质因子的合数
- 质因子:质数的因子
- 合数: 整数中除了能被1和本身整除外,还能被0除外的其他数整除的数
- 合数的性质:
- 1、所有大于2的偶数都是合数
- 2、所有大于5的奇数中,个位为5的都是合数
- 3、除0以外,所有个位为0的自然数都是合数
这样可以减少地址的重复(冲突)
6、随机数法
- 选择一个随机数,取关键字的随机函数值为它的散列地址
- H(key)= Random(key)
- random 为随机函数
关键字的长度不等,采用该方法比较合适
3、采用散列函数的参考因素:
既然有那么多种方法,那我具体该用哪一种呢?
选择困难症发作······
别着急,我这里有一份参考标准,你只要综合这些因素,就能决策选择哪种散列函数更合适了:
- 计算散列地址所需的时间
- 关键字的长度
- 散列表的大小
- 关键字的分布情况:关键字分布是否均匀,是否有规律可循
- 记录查找的频率
设计的散列函数在满足以上条件的情况下尽量减少冲突
4、解决散列冲突:
讲了这么久的散列函数结构,那么我们接下来的重头戏来了,就算你散列函数设计的再好,但是数据那么多,你难免会遇到冲突。
当我们遇到冲突的时候,程序又不是像你一样,聪明机灵,会懂得去找别的地址。他只是一个憨憨的愣头青,遇到困难只会傻傻的停在那里。所以我们需要为他规划Plan B。
散列表冲突的解决方法有四种:
- 开方定址法
- 再散列函数法
- 链地址法
- 公共溢出区法
接下来我们来详细的讲解四种方法:
1、开方定制法
一旦发生冲突,就去寻找下一个空的散列地址
- 只要散列表足够大,空的散列地址总能找到,并将记录存入
- fi( key) = ( f ( key ) + di ) MOD m
- di = 1, 2, 3, ···, m - 1
例子:
- 散列函数f( key ) = key mod 12
- key = 37 时,发现f ( 37 ) = 1,与25 所在位置发生冲突
- 采用上面公式
- f ( 37) = ( f ( 37 ) + 1 ) mod 12 = 2
- 于是将37 存入下标为2 的位置
解决冲突的三种方法:
-
线性探测法
- 通过不断的增大di 的值来寻找空的散列地址:di = di++
- 堆积
- 如果碰到48 和 37 这种本来不是同义词却需要争夺一个地址的情况,称为堆积
- 堆积的出现,使得我们需要不断处理冲突,无论是存入还是查找效率都会大大降低
-
二次探测法
- 线性是不断的向后探索,但是如果它前面还有一个空位置,可是我们却不断向后求,虽然也能得出结果,但是效率很差。
- 所以我们可以采用双向寻找到可能的位置。
- 二次探测法就是:增加平方运算的目的是为了不让关键字都聚集在某一块区域
- fi ( key ) = ( f ( key ) + di ) MOD m
- di = 12, -12, 22, -22 ···
-
随机探测法
- di 是一组伪随机数列
- 在冲突时,对于位移量di 采用随机函数计算得到
- 这里使用的随机函数是伪随机函数
- 因为使用的随机种子是相同的,所以不断调用随机函数可以生成不会重复的数列。
- 查找
- 若我们的随机种子是相同的,每次得到的数列也是相同的,相同的di 当然可以得到相同的散列地址
- fi ( key ) = ( f ( key ) + di ) MOD m
- di 是一个随机数列
总之,开方定址法只要在散列表未填满时,总能找到不发生冲突的地址。是我们常用的解决冲突的方法。
2、再散列函数法
- 准备若干个散列函数,若该散列函数冲突,则使用下一个
- fi ( key ) = RHi ( key )
- i = 1, 2, ···, k
- RHi:就是不同的散列函数
- fi ( key ) = RHi ( key )
这种方法使得关键字不产生聚集,当然,相应地也增加了计算的时间。
3、链地址法
- 不使用其他空间,直接在原地解决冲突
- 在散列表中只存储所有同义词子表的头指针
- 同义词子表
- 将所有关键字为同义词的记录存储在一个单链表中
- 同义词子表
- 优点:有效的处理了冲突,为其提供了绝不会找不到地址的保障
- 缺点:查找时需要遍历单链表的性能损耗
4、公共溢出区法
- 建立一个特殊存储空间,专门存放冲突的数据
- 在查找时
- 对给定值通过散列函数计算出散列地址后
- 先于基本表的相应位置进行对比
- 相等,查找成功
- 不相等,到溢出表里进行顺序查找
适用于数据和冲突较少的情况
既然我们已经解决了冲突的问题,那么现在就最后补充三个关于查找的知识点。
5、散列表的查找
查找过程和造表过程一致
假设采用开放地址法处理冲突,查找过程为:
- 对于给定的key,计算hash 地址index = f(key)
- 如果数组 arr[index] 的值为空,则查找不成功
- 如果数组 arr[index] == key,则查找成功
否则,使用冲突解决方法求下一个地址,直到
arr[index] == key or arr[index] == null
6、散列表查找算法实现
- 首先是需要定义一个散列表的结构以及一些相关的常数
- HashTable:散列表结构
- 结构中的elem:动态数组
7、散列表的查找效率
决定散列表查找的ASL 因素
- 用的散列函数
- 选用的处理冲突的方法
- 散列表的饱和度,装载因子α = n / m
- n 表示实际装载数据长度
- m 为表长
一般情况下,假设散列函数是均匀的,则在讨论ASL 时可以不考虑它的因素。
散列的ASL 是处理冲突方法和装载因子的函数
散列表的理论到这里就结束了,接下来的那篇,也是我们散列表系列的最后一篇,会具体讲讲散列表如何实现。
以上就是本篇文章的所有内容了,如果觉得有帮助到你的话,
麻烦动动小手,点赞收藏转发!!!
你的每一次点赞都是我更新的最大动力~
我们下期再见!