关于散列、检索的总结

文章目录

    • 字典线性表
      • 线性表存储
      • 顺序线性表存储
    • 散列
      • 散列函数
      • 内消解
        • 开地址法和探查序列
        • 检索和删除
      • 外消解
        • 溢出区方法
        • 桶散列
      • 散列表的性质
        • 扩大存储区,空间换时间
        • 负载因子和操作效率
        • 可能的技术和实用情况

本文着重讨论静态字典检索相关的散列问题,不讨论动态字典的插入和删除问题。
字典创建的思想是给予关键码的检索。将key与value产生对应关联的一种结构。

字典线性表

检索的平均检索长度(ASL(Average Search Length))计算公式
A S L = ∑ i = 1 n p i ∗ c i ASL=\sum_{i=1}^{n}p_{i}*c_{i} ASL=i=1npici
p i p_i pi 是检索到的概率 c i c_i ci 检索长度

线性表存储

直接将插入元素放入线性表后面,最基本的存储方式。
每次检索都的概率都相同1/n,检索长度和为(1+2+3+…+n),公式计算结果为(n+1))/2
检索用的复杂度为: O ( n ) O(n) O(n)
这种结构导致在动态字典的各种操作中效率不变,也是效率最低操作。

顺序线性表存储

数据线性表采用二分法,可以了解到查找的复杂度为 O ( l o g 2 n ) O(log_2 n) O(log2n)
顺序线性表的插入和删除都需要将数据移动,所以复杂度为 O ( n ) O(n) O(n)
以上两种方式为基本的存储方式,在面临大数据及和动态数据集的时候检索效率较低,解决这个问题主要采用了以下两种结构

  • 基于散列(hash)思想的散列表
  • 基于树型思想的数据存储检索结构

散列

散列的基本思想是:如果关键码不能或者不适合作为数据存储的下标可以考虑通过一个变换,把它映射到一种下标。
通常情况下,散列的key值范围远远大于下标范围。直接导致在散列的过程中会出现散列之后会相同的情况。
对于规模固定的散列表,表中元素越多产生冲突可能性越大,使用参数使用负载因子 α \alpha α进行衡量
负 载 因 子 α = 散 列 表 中 实 际 数 据 数 量 散 列 表 容 量 负载因子\alpha=\frac{散列表中实际数据数量}{散列表容量} α=
α \alpha α越大则冲突可能性越大。 α \alpha α无论大小都会导致散列冲突。

散列函数

优化冲突的首先想到的是采取的是优化散列函数的方式。
散列函数的作用是将关键值映射到指定的下标范围内,散列函数设计的优劣直接影响冲突发生的概率。着重考虑一下三点:

  • 将值尽可能映射到值域的更大范围
  • 尽可能将散列值在值域中均匀分布
  • 散列函数尽可能简单,减少计算消耗

在已知关键码的情况下,可能设计出最合适的散列函数。
以下是几种关于证书关键码的散列方法:
   数字分析法:在给定数据关键码的情况下,分析数据中数字出现的频率,选取分布较好的作为散列方法,如选取十位和百位数据作为散列方法。
   折叠法:将较长的关键字分成几段,通过运算将其合并。如将10位整数按三位拆分,进行加和,再舍弃进位,就可映射到[0, 999]区间了。
   中平方法:想求出关键码的平方,然后取出中间的几位作为散列值。
通用的散列方法只适用于均匀分布假设,以下两种通用的散列函数:

  • 除余法:适用于整数关键码
  • 基数转换法:适用于整数或字符串关键码。

   除余法:使用key除以某个不大于散列长度m的整数p,得到某个余数(或加上某值)作为散列地址。一般m取2的整数次幂。
   基数转换法:整数关键码。将关键码看做是r进制的数值,转换成想要转换的进制,通常取r为素数。若范围不合适,使用以上方法映射到指定区间。如果字符串,可将字符转换成整数编码。
  通过合理的hash函数尽量避免冲突,但解决不了冲突。通常采用两种方式化解冲突。

  • 内消解方法(在基本存储区内部解决冲突)
  • 外消解方法(在基本的存储区之外解决冲突)

处理冲突的两个基本要求:

  1. 保证当前这次存入的数据工作能正常完成(写入正常完成)
  2. 保证字典基本存储性质:在任何时候,从任何以前存入字典后没有删除的关键码出发,都能找到数据。(读取不会出现错误)

内消解

开地址法和探查序列

内消解的基本方法是开地址法。基本思想是插入数据发现冲突时,另行安排一个位置。为此方法设计的位置安排方式,称为探查方式。
内消解法的主要原理是如果遇到散列冲突后,查找其它位置进行填充,这种方式成为探查方式。定义:
D = d 0 , d 1 , d 2 ⋯ D=d_0, d_1, d_2\cdots D=d0,d1,d2
定义探查序列为
H i = ( h ( k e y ) + d i )   m o d   p H_i=(h(key)+d_i)\,mod\,p Hi=(h(key)+di)modp
d i d_i di 增量的设计有多种设计:
1. 线性探查:取连续的探查间隔,D = 0,1,2,…,当有冲突之后一次探查后面位置是否占用
2. 双散列探查:设计散列函数h2,令 d i = i × h 2 ( k e y ) d_i= i×h_2(key) di=i×h2(key)。有冲突后探查 h 2 h_2 h2散列后的整数倍2位置是否被占用。

检索和删除

检索
1. 调用散列函数,算出key值
2. 查看存储位置,若没有数据则不存在
3. 若相等,则存在
4. 若不等则,进行探查,回到2

删除
删除注意问题:可能删除元素在检索路径上,则需在删除后添加标记元素占位。

外消解

溢出区方法

设置另一个存储区,当有冲突时,按顺序将溢出区结果存入存储区。在冲突较少的情况下效果较好。

桶散列

将散列后的index值存放一个引用,每个位置设置为一种数据结构,可使用链表进行实现,插入时可先检索散列后的位置链表是否存在该元素,检索删除对应位置链表元素。
可用于大型字典的实现。

散列表的性质

扩大存储区,空间换时间

无论那种方法,由于散列的基本原理。随着元素的增加都会导致负载因子增大,冲突概率增大。
开地址随着数据增加导致溢出;拉链法会增加链的平均长度。

负载因子和操作效率

采用内部消解技术(开地址)时,负载因子 α \alpha α<=0.7~0.75,检索效率越来越趋于线性;在桶散列技术中,负载因子 α \alpha α就是桶的平均长度。
人们说散列是一种搞笑的字典实现技术,是有些基本假设条件的:

  • 散列函数所散列的值分布均匀
  • 字典散列表的负载因子不应太高(试验证明应在0.7以下)

满足这些条件,散列表检索、插入、删除的时间开销可以看作常量。
散列字典从概率上讲极为高效,但事情也有另一面:

  • 常量时间的操作代价是平均代价,不是每次操作的实际代价。由于不同元素探查序列长度不同,某些情况代价较高的情况。
  • 一般而言关键码冲突是必然发生的情况,导致不同操作的代价之间差异可能很大。而且不能事先确知。
  • 不断插入元素导致负载因子不断增大。为保证操作效率就需要扩大存储,从而导致一次很高代价的插入操作。而且无法预知合适出现
  • 数据排序无法预知

还有可能出现不好情况:

  • 极端情况,可能导致散列到一个或几个散列值,导致探查效率低下。
  • 基于内消解机制,长期使用可能产生很长的已删除序列(删除元素需要添加占位符),影响效率。

可能的技术和实用情况

实际实现中,可能需要考虑许多问题:

  • 给用户提供检查负载因子和主动扩大散列表存储取的操作。这样,用户可以在效率要去较高的计算之前,根据需要首先设定足够大的存储。
  • 开地址散列的情况下,记录、检索被删除项的量或比例达到了一定程度下后,自动整理。最简单的办法是准备另一块存储,重新散列。

你可能感兴趣的:(知识点整理)