4.数据结构:散列表

散列存储的特性

散列存储:散列表,采用的存储方式是散列存储。那么何为散列存储呢?散列存储是根据元素的关键字直接计算出该元素的存储地址,又称哈希(Hash)存储。采用散列存储的方式存储数据时,具备的优点是在散列表中检索、增加和删除结点的操作很快;相反,它的缺点也相对比较明显,在插入结点的过程中,若散列函数选择不好,就可能在散列表中出现元素存储单元的冲突,解决冲突会额外的时间和空间开销,费时费力。

什么是散列表

散列表:散列表是根据数据元素的关键字而直接进行访问的数据结构。通俗地讲,就是散列表建立了关键字和存储地址之间地一种直接映射关系。这种直接映射关系通过选择的散列函数来完成。

什么是散列函数

散列函数:散列函数又是什么呢?散列函数其实是一个把查找表中的关键字映射成该关键字对应的存储地址的函数,记为: Hash(key) = Adr(地址可以是数组下标、索引或内存地址等)。

什么是冲突

冲突:何为冲突?冲突其实是通过选择的散列函数,对于两个或两个不同的关键字映射到同一个存储地址中。对于这些发生碰撞的不同关键字称为同义词。
如何解决冲突:解决冲突的发生有两个方面,一方面,是设计好的散列函数尽量去减少冲突的发生;另一方面,由于冲突不可避免,可以设计好处理冲突的方法。

理想状况下,散列表进行查找的时间复杂度为O(1),

散列函数的构造方法

在构造散列函数时,应该要注意以下几点:
(1)散列函数的定义域(存放关键字的存储单元)必须包含全部存储的关键字,而值域的范围则依赖于散列表的大小或地址范围。
(2)散列函数计算出来的地址是等概率的、均匀分布在整个地址空间中,减少冲突的发生。
(3)散列函数尽量简单,能够在较短时间内计算出任一个关键字的散列地址。

1. 直接定址法
  • 直接取关键字的某个线性函数值为散列地址,散列函数为:
    H(key) = key 或 H(key) = a *key + b,式中,a 和 b 是常数,计算方便,不会产生冲突。
    适用于:关键字的分布基本连续,例如:3,4,5,7,8;若关键字分布不连续,空位较多,会造成存储空间的浪费。
2. 除留余数法
  • 除留余数法是一种最简单、最常用的方法,简单介绍除留余数法是如何使用的。
    假设散列表表长为 m,取一个不大于 m 但近视接近或等于 m 的质数 p,利用除留余数法的散列函数把关键字转换成散列地址。散列函数为:H(key) = key % p 。
    采用除留余数法关键是选好 p,这样就使得每个关键字通过该散列函数转换后等概率地映射到散列空间上地任一地址。
3. 数字分析法
  • 数字分析法基本上不常用,不做介绍。不过数字分析法这种方法适合于已知的关键字集合,若换了关键字,则需要重新构造新的散列函数。
4. 平方取中法
  • 顾名思义,这种方法取关键字的平方值的中间几位作为散列地址。该方法不太适用,且操作相对较麻烦,不过多介绍。

处理冲突的方法

  • 冲突,顾名思义,就是不同的关键字经过 hash 函数的计算可能得到同一个 hash 地址,即 key1 不等于 key2 时,H(key1 ) = H(key2 ),出现的这种现象便叫做冲突。
  • 解决冲突,即在关键字发生冲突时,为产生冲突的关键字寻找下一个“空”的 Hash 地址。利用探测方法进行探测,若探测到的 Hash 地址仍然产生冲突,就继续探测下一个地址,直到为该关键字找到不产生冲突的存储地址即可。
  • 处理冲突的方法有两种:开放定址法和拉链法(链接法)。
1. 开放定址法

所谓开放定址法,是指可存放新表项的空间地址既向它的同义词开放,又向它的非同义词表项开放。其数学递推公式为:Hi = (H(key)+di)%m,式中,H(key) 为散列函数;i = 0,1,2,…,k(k<=m-1);m 表示 散列表表长;di为增量序列。通常有以下 4 种取法,分别是线性探测法、平方探测法、再散列法和伪随机序列法,简单介绍线性探测法和平方探测法。

  • 线性探测法
    当 di =0,1,2,…,m-1时,称为线性探测法。
    特点:冲突发生时,顺序查看表中下一个单元(探测到表尾地址 m-1时,下一个探测地址是表首地址 0),直到找到一个空闲单元(当表未满时一定能找到一个空闲单元)或查遍全表。
    缺点:线性探测法可能使第 i 个散列地址的同义词存入第 i+1 个散列地址,将本该存入第 i+1 个散列地址的元素就争夺第 i+2 个散列地址的元素地址,以此类推,从而照成大量元素在相邻的散列地址上“聚集”(或堆积)起来,大大降低了查找效率。

  • 平方探测法(简单了解即可)
    当 di = 0^2, 1^2, -1^2 , 2^2, -2^2,… ,k^2, -k^2 时,称为平方探测法,其中 k<=m/2,散列表长度 m 必须是一个可以表示成 4K+3 的素数,又称二次探测法。
    特点:平方探测法是一种较好的处理冲突的方法,可以避免出现“堆积”问题。
    缺点:不能探测到散列表上的所有单元,只能探测到散列表上的一半单元。

2. 拉链法(链接法)
  • 对于不同的关键字可能会通过散列函数映射到同一地址,为避免非同义词发生冲突,把所有的同义词存在在一个线性链表中,线性链表由其散列地址唯一标识。
  • 适用:拉链法常用于进行插入和删除的情况。
  • 关键字序列为 {15,16,29,37,48,12,25,56,67,47,22,34},应用拉链法处理冲突的散列表如下图所示。
    4.数据结构:散列表_第1张图片
3. 拉链法查找和性能分析
  • 在利用拉链法进行查找和处理冲突时,通常也会去计算它的性能情况,下面分别计算利用拉链法处理冲突时各个关键字的查找成功的平均查找长度和查找不成功的平均查找长度。
  • 通过上述利用拉链法来进行冲突处理的图片可知,利用拉链法查找关键字的查找成功的平均查找长度如下:
    ASL(成功)=( 1 * 9 + 2 * 3 ) / 12 = 15 / 12 = 1.25
  • 简单理解方法:可以把拉链表看成是由 12 个具有头结点的单链表组成。1 * 9(从头结点下的第一个结点开始计数)可看成是头结点的下一个元素是否为空,刚好头结点下的结点不为空的有 9 个。 同理,便可知道 2*3从何而来。除以12中12表示有12个关键字,根据关键字个数来判断除以多少。
  • 计算出查找不成功的平均查找长度如下:
    ASL(不成功) = (3+3+1+2+2+2+1+2+2+1+3+2)/12=24/12=2
    其中第一个3表示第一个链表中查找3次都没有查到指定的关键字,第2个3同理,依次查找,可以得出上述公式,除以12表示具有12个单链表存储单元。
4. 线性探测法处理冲突
  • 关键字序列{19,14,23,01,68,20,84,27,55,11,10,79}按散函数 H(key)=key%13 和线性探测法处理冲突所构造的散列表如下图所示。
    在这里插入图片描述

  • 该散列表是通过散列函数和线性探测处理冲突的方法得来的。例如19,通过19对13的余数可得余数为6,则19的散列地址为6,存入散列表中地址为6的存储单元;14除以13余数为1,存入1号地址;23除以13余数为10,存入 10号地址;01除以13余数为1,本来需要存入1号地址,但1号地址已经存入14,故向后探测一个存储单元,可见2号存储单元为空,存入其中即,同理可得上述散列表。

  • 利用哈希函数和线性探测法处理冲突后,查找关键字得查找成功得平均查找长度和查找不成功得平均查找长度如下:

  • 查找成功的平均查找长度
    查找各关键字的比较次数如下图所示。
    在这里插入图片描述

  • 查找成功平均查找长度:
    ASL(成功)=(1 * 6 +2 * 1+3 * 3+4+9)/12 =30/12=2.5
    查找失败平均查找长度:
    ASL=(13+12+11+10+9+8+7+6+5+4+3+2)/13=90/13

注意:对同一组关键字,设定相同的散列函数,不同的处理方法得到的散列表不同,它的平均查找长度也不同。散列表在查找过程中的时间复杂度为 O(1),平均查找长度 ASL=1,实际上由于冲突的存在,其ASL的值会比1大。

  • 散列表查找效率影响因素:散列函数、处理冲突的方法和装填因子(表的装满程度)
    散列表装填因子记为 a,定义一个表的装满程度,即:
    a = (表中记录数n)/ (散列表长度 m),散列表的平均查找长度依赖于散列表的装填因子a,而不直接依赖于 n 或 m 。直观地看,表示装填地记录越“满”,发生冲突的可能性越大,反之发生冲突的可能性越小。

更多知识可点击博主主页进行查看,在这里你会学到很多知识,希望大家的支持和关注。

你可能感兴趣的:(数据结构与算法,数据结构,散列表,经验分享)