文章目录
- 7.4.1 散列表的基本概念
- 7.4.2 散列函数的构造
- 散列函数的构造方法
- 7.4.3 处理冲突的方法
- 1. 开地址法
- 1.1 线性探测法
- 1.2 二次探测法
- 2. 链地址法
- 7.4.4 散列表的查找
- 散列表的查找效率分析
- 总结
7.4.1 散列表的基本概念
- 基本思想:根据要存储的关键字的值,来计算该存在哪里。
- 对应关系 —— hash 函数,通过这个函数将关键字的值对应到它的存储位置。
- Loc(i) = H(keyi)。
举个例子
【例1】:
- 这些同学们的信息,既不是按照输入顺序存的,也没有排好序按照递增或递减的方式存储。
- 而是根据学号最后两位数,直接对应到存储位置。
- 如果想找某位同学的信息,直接根据学号后两位去对应位置找就行了。
【例2】:
- 根据元素序列(21,23,39,9,25,11),若规定每个元素 k 的存储地址 H(k) = k,请画出存储结构图。
- H(k) = k:关键字的值是多少,对应的位置就是多少。
- 按照这样的方式来存储的话,要找某一个元素就会非常方便了,直接根据给定的值去固定的位置找就行了。
如何查找
- 根据散列函数:H(key) = k。
- 查找 key = 9,则访问 H(9) = 9 号地址,若内容为 9 则成功找到;
- 若查不到,则返回一个特殊值,如空指针或空记录。
散列表的特点
- 优点:查找效率高,时间效率可以达到 O(1)。
- 缺点:空间效率低。
散列表的术语
- 散列方法(杂凑法):
- 选取某个函数,依该函数按关键字计算元素的存储位置,并按此存放;
- 查找时,由同一个函数对给定值 k 计算地址。将 k 与地址单元中元素关键码进行比较,来确定查找是否成功。
- 散列函数(杂凑函数):散列方法中使用的转换函数。
- 散列表(杂凑表):
- 一个有限连续的存储空间,用来存储按照散列函数计算得到相应散列地址的数据记录。
- 通常散列表的存储空间是一个一维数组,散列地址是数组下标。
- 冲突:不同的关键码映射到同一个散列地址。
- key1 ≠ key2,但是 H(key1) = H(key2)。值不相同,但是都住进了一间房。
- 例:有 6 个元素的关键码分别为:(25,21,39,9,23,11)。
- 选取关键码与元素位置间的函数为 H(k) = k % 7.
- 地址编号从 0 - 6.。
- 在散列查找方法中,冲突是不可避免的,只能尽可能减少。
- 同义词:具有相同函数值的多个关键字。
- 如:上图互相冲突的值,虽然值不相同,但是函数值一样,导致了它们会呆在同一块空间内。
- 如:上图互相冲突的值,虽然值不相同,但是函数值一样,导致了它们会呆在同一块空间内。
7.4.2 散列函数的构造
使用散列表要解决两个问题:
- 构造好的散列函数
- 所选函数尽可能简单,以便提高转换速度;
- 所选函数对关键码计算出的地址,应尽可能使散列地址集中致均匀分布,以减少空间浪费。
- 制定一个好的解决冲突的方案
- 查找时,如果从散列函数计算出的地址中查不到关键码,则应当依据结局冲突的规则,有规律的查询其它相关单元。
构造散列函数需要考虑的因素
- 执行速度:即计算散列函数所需要的时间;
- 关键字的长度;
- 散列表的大小:散列表越大,产生冲突的可能性越小,但是浪费空间;
- 关键字的分布情况:根据关键字的特点,怎样才能使他们分布的更均匀;
- 查找频率:让需要经常查找的元素更容易被找到。
根据元素集合的特性构造
- 要求一:n 个数据源仅占用 n 个地址,虽然散列查找是以空间换时间,但是仍希望散列的地址空间尽量小。
- 要求二:无论用什么方式存储,目的都是尽量均匀的存放元素,以避免冲突。
散列函数的构造方法
- 直接定址法
- 数字分析法
- 平方取中法
- 折叠法
- 除留余数法
- 随机数法
直接定址法
- Hash(key) = a * key + b(a、b为常数)
- 优点:以关键字 key 的某个线性函数值为散列地址,不会产生冲突。
- 缺点:要占用连续地址空间,空间效率低。
例:{100,300,500,700,800,900},
散列函数:Hash(key) = key/100(a = 1/100,b = 0)
除留余数法
- Hash(key) = key % p(p 是一个常数),将余数作为数据元素的存储位置。
- 关键:如何选取合适的 p ?
- 技巧:假设表长为 m,取 p <= m 且为质数。
例:{15, 23, 27, 38, 53, 61, 70},散列函数 Hash(key) = key % 7,7 <= 表长7,且为质数。
如果想反过来找某个数也是同样的方法,如:找61,61 % 7 = 5,那么就去 5 号位置找它。
7.4.3 处理冲突的方法
- 开放定址法(开地址法)
- 链地址法(拉链法)
- 再散列法(双散列函数法)
- 建立一个公共溢出区
1. 开地址法
基本思想
- 有冲突时就去寻找下一个空的散列地址;
- 只要散列表足够大,空的散列地址总能找到,并将数据元素存入。
- 例如:除留余数法 Hi = (Hash(key) + di) % m,di 为增量序列。
- 开地址法的三种方法 % 的都是表长 m ,不要和除留余数法构造散列表的 % 最大质数 p 搞混了。
常用方法
- 线性探测法:增量序列 di 为 1,2,…m-1 的这样一个线性序列。
- 二次探测法:增量序列 di 为 12,-12,22,-22,…,q2 的二次序列。
- 伪随机探测法:增量序列 di 为伪随机数序列,加上一个伪随机数,将 key 随机的存储到后面的某一个位置,这个位置空着的话就存上,非空的话就继续产生一个随机数找下一块空间。
1.1 线性探测法
Hi = (Hash(key) + di) % m(1 <= i < m)
- 其中:m 为散列表长度,di 为增量序列 1,2,…m-1,且 di = i
- 一旦冲突,就找下一个地址,直到找到空地址存入
举个例子
关键码集合为 {47,7,29,11,16,92,22,8,3},散列表长度为 m = 11;散列函数为 Hash(key) = key % 11;拟用线性探测法来来处理冲突。
- 47 % 11 = 3,将 47 放在 3 号位置。7 % 11 = 7,将 7 放在 7 号位置。
- Hash(29) = 29 % 11 = 7,此时出现冲突,需要存到下一位置。
- di = 1,2,3,…,m-1,由 H₁ = (Hash(29 + 1) % 11 = 8,并且散列地址 8 还空着,因此将 29 存入。
- 让 29 存进去的时候已经做了两次运算了。
- Hash(11) = 11 % 11 = 0,且 0 还空着,将 11 存入 0号位置。Hash(16) = 16 % 11= 5,且 5 为空,将 16 存入 5号位置。Hash(92) = 92 % 11 = 4,4 空,存之。
- Hash(22) = 0,此时 22 与 0 号位置的 11 产生冲突。
- 往后移一位 (Hash(22) + 1) % 11 = 1,存储到 1 号位置去。
- Hash(8) = 8,与 29 产生冲突,寻找下一块空置空间 9 存入。
- Hash(3) = 3,与 47 冲突,依次往后寻找闲置空间。
- (Hash(3) + 3) % 11 = 6,di 从 1 一直加到 3 才找到一块空着的空间 6,此时将 3 存入。
同样,如果想要找出 3 的话,Hash(3) = 3 % 11 = 3,先从 3 号位置开始找,发现不是则依次往后找,直到比较了 4 次之后才终于找到。
平均查找长度
- 将所有元素的比较次数相加 / 元素个数
- 比较次数:11 要比较一次,22 比较两次,3 比较3次…
1.2 二次探测法
关键码集合为 {47,7,29,11,16,92,22,8,3},
- 设:散列函数为 Hash(key) = key % 11,Hi = (Hash(key) + di) % m。
- 其中:m 为散列表长度, m 要求是某个 4k + 3 的质数;di 为增量序列 12,-12,22,-22,…,q2。
- 如果当前要插入的值与当前位置产生冲突,首先要探测当前位置的下一位置,如果下一位置还是冲突,则探测当前位置的前一位置。
举个例子
-
其中:
- 黑色数字为第一次就找到空闲位置成功存进去的值。
- 蓝色为第一次比较时与其他位置有冲突的值,它们可以直接找到下一位置作为栖息地。
-
剩下的元素 3 就不能只通过往后移一位找到地方待了。
- 3 号位置已经有元素了,先让 3 在当前 3 号位置上加 1 看看 4 是否有空位,4 号位置非空,此时应该移动 -1 位到 2 号位置判断时候有空位,2 号位置为空,将关键码 3 插入在此。
2. 链地址法
基本思想:
- 将相同散列地址的记录链成一条单链表,m 个散列地址就设置 m 个单链表,然后用一个数组将 m 个单链表的表头指针存储起来,形成一个动态的结构。
举个例子
- 现有一组关键字为: {19,14,23,1,68,20,84,27,55,11,10,79}
- 散列函数为:Hash(key) = key % 13。
- 这个时候我们就发现了, 有几个元素是同义词(% 13 的余数相等),如14,1,27,79,也就是说这几个元素会产生冲突。
- 将同义词(冲突的元素)链接在同一条单链表上。
- 将链表存储在函数值算出来的位置上,如 14,1,27,79,他们的余数都是 1,将由他们链接的链表首地址放在数组 1 号位置,其余元素同理。
链地址法建立散列表步骤
- Step1:取数据元素的关键字 key,计算其散列函数值(地址)。
- 若该地址对应的链表为空,则将该元素插入此链表;否则执行 Step2 解决冲突。
- Step2:根据选择的冲突处理方法,计算关键字 key 的下一个存储地址。
- 若该地址对应的链表不为空,则利用链表的前插法或后插法将该元素插入此链表。
链地址法的优点
- 非同义词不会冲突,无聚集现象。
- 链表上结点空间动态申请,更适合于表长不确定的情况。
7.4.4 散列表的查找
算法步骤
- 给定待查找的关键字 key,根据造表时设定的散列函数计算 Ho = H(key)。
- 若单元 Ho 为空,则所查找元素不存在。
- 若单元 Ho 中元素的关键字为 key,则查找成功。
- 否则重复以下解决冲突的过程:
- 按处理冲突的方法,计算下一个散列地址 Hi;
- 若单元 Hi 为空,则所查找元素不存在;
- 若单元 Hi 中元素的关键字为 key,则查找成功。
举个例子
已知一组关键字(19,14,23,1,68,20,84,27,55,11,10,79)散列函数为:H(key) = key % 13,散列表长为 m = 16,假设每个记录的查找概率相等。
一、线性探测然后再散列处理冲突,即 Hi = (H(key) + di) % m。
- 第一次构造散列表是 % 小于表长的最大质数,所以是13。
- 处理冲突的时候是要 mod 表长,所以是 % 16.
如果想要找 79 的话就需要比较 9 次才能找到。
平均查找长度:所有元素的比较次数之和 / 元素个数。
- ASL = (1 X 6 + 2 + 3 X 3 + 4 + 9)/ 12 = 2.5
二、用链地址法处理冲突
散列表的查找效率分析
使用平均查找长度 ASL 来衡量查找算法,ASL 取决于:
- 散列函数
- 处理冲突的方法
- 散列表的装填因子 α。
- α 越大,表中记录数越多,说明表装的越慢,发生冲突的可能性就越大,查找时比较次数就越多。
ASL 与装填因子 α 有关!既不是严格的 O(1),也不是 O(n)。
总结
- 散列表技术机油很好的平均性能,优于一些传统的技术。
- 链地址法优于开地址法。
- 除留余数法作散列函数优于其他类型函数。