本文目录
本文讲解散列表的思想和手动计算,不涉及具体代码实现。
一、简单介绍
定义:
散列表(Hash Table),也称为哈希表,是一种根据关键码值(Key value)而直接进行访问的数据结构。它通过把关键码值映射到表中一个位置来访问记录,从而加快查找速度。这个映射函数称为散列函数,存放记录的数组称为散列表。
解释:
-
在谈到查找的时候,我们应该能够想到线性表和树。以线性顺序表为例,如果一个顺序表中存储一系列数字(关键字),我们要查找其中一个,可以采用顺序查找,此时时间复杂度为O(n)。如果顺序表是有序的,可以采用二分查找,此时时间复杂度就降为了O(log n)。
-
但是如果我们已经知道了存储要查找的数(关键字)在顺序表中的位置下标,根据下标查找它,所付出的时间则是O(1),非常快了。可问题是我们一般情况下很难知道某个数的位置下标,所以为了实现这种查找方式,我们就需要把存储的数和存放它的位置下标相关联,这样,我们只需要拿出要查找的数,就可以直接算出它的位置下标,并在O(1)的时间内访问它。
-
这就是散列表(哈希表)的思想。其中,把要存储的数和存储它的位置下标联系起来的函数称为散列函数(哈希函数)。
【注】:以上所说的顺序表下标只是举例,实际上散列函数可以把关键字映射成数组下标、索引、内存地址等多种。
二、散列表的构造
1.散列函数
散列函数有很多种,这里介绍一下比较常用的一种:除留余数法。
顾名思义,这个方法的思想就是拿关键字去和某个数做除法,得到的余数就是存储位置下标。至于要和谁做除法?一般是选择一个不大于散列表表长的最大的质数,因为对质数取余可以使得结果分布得更加地均匀。如果表长15,可以选13作为该除数;如果表长9,可以选7作为除数。
以表长为9为例,输入数据15,15%7=1,则15应该放在下标为1处;5%7=5,则5应该放在下标5处。
但是当想要插入数字8时,发现8%7=1,应该放在下标1处,但是1处已经有数据了,这种情况就是发生了“冲突”。8和15发生了冲突,称二者为同义词。为了解决这个问题,就要想办法处理冲突。
2.处理冲突的方法
处理冲突主要有两种方法:开放定址法和拉链法。
1.拉链法
这个方法很简单直观,即,把所有产生冲突的关键字组成一个链表。如上面的例子中,8和15产生了冲突,则处理方式如下:
其中当新的同义词出现时,可以采用头插法,也可以采用尾插法插入对应链表。每个链表由其散列地址(这里指下标)唯一标识。
2.开放定址法
这个方法指的是,空闲的地址不仅可以给同义词用,也可以开放给非同义词用。也就是说,如果两个关键字产生了冲突,后来的关键字可以再另外找一个位置存放。
开放定址法公式可以表示为:
Hi=(H(key)+di)%m
其中m为表长(注意,表长不一定是除留余数法选定的作为除数的那个质数),i表示第几次查找地址,key是关键字,di是第i次查找地址时用的增量值。看定义可能很难理解,等会举个例子就好理解了。
这个式子中最重要的就是di的取值,关于它的取值,有很多种方法。这里介绍一下常用的两种。
线性探测法
意思是di的取值是线性变化的。di=1,2,3,……,m-1.
例如,还以上面的为例,表长为9,散列函数是H(key)=key%7。
输入15,输出1,把15放在1处。
输入8,输出1,但1处已经有数据了,因此采用线性探测法,H1=(H(8)+d1)%9=2 。冲突问题解决,把8放在下标2处。
如果再来一个数据8,8%7=1,发生了冲突,进行线性探测,H1=(H(8)+d1)%9=2 ,2处依然有数据,就再进行线性探测,H2=(H(8)+d2)%9= (1+2)%9=3,把它放在3处。
【注】这里只是举例,演示一下两次线性探测的情况。在实际题目中是否允许相同值重复插入,视情况而定。
由于对表长m取余,得到的可能的取值有0~m-1共m种,完全可以覆盖整个散列表,也就是只要散列表装得下,就一定能处理冲突数据。
平方探测法
这种方法的操作过程与线性探测法类似,只是di的取值序列不同。di=1^2, -1^2, 2^2, -22,……,k2,-k^2
注意是正负交替的序列
具体过程不再演示,和上面基本一样。(其实是因为懒)
三、散列表的平均查找长度(ASL)
这部分是考研常考的内容。下面用例子来演示一下。
查找成功的ASL
【2018年 统考真题】
现有长度为7、初始为空的散列表HT,散列函数H(k)=k%7,用线性探测再散列法解决冲突。将关键字22,43,15依次插入HT后,查找成功的平均查找长度是()。
- 首先构造散列表。22%7=1,43%7=1,15%7=1,三个数都产生了冲突,利用线性探测法依次处理。
22放到1位置。
H1=(H(43)+d1)%7=2,43放到2位置。
H1=(H(15)+d1)%9=2,不行,H1=(H(15)+d2)%9=3,15放到3位置。如下:
- 计算查找成功的ASL。
查找22,只需用散列函数计算得到1,与1位置比较一次。
查找43,需要计算散列函数得到1,与1位置比较1次,不匹配;线性探测法计算一次得到2,与2位置比较一次,查找成功,共比较了2次。
查找15,需要计算散列函数得到1,与1位置比较1次,不匹配;线性探测法计算一次得到2,与2位置比较一次,不匹配;线性探测法再计算一次得到3,与3位置比较一次,查找成功,共比较了3次。
ASL=(1+2+3)/3=2,答案为2.
查找失败的ASL
【2019年 统考真题】
现有长度为11且初始为空的散列表HT,散列函数是H(key)=key%7,采用线性探测再散列法解决冲突,将关键字序列87,40,30,6,11,22,98,20依次插入HT后,HT查找失败的平均查找长度是( )。
- 首先构造散列表。根据题意,散列函数是H(key)=key%7并采用线性探测再散列法解决冲突,得到如下散列表。(可以自己动手算一下)
- 计算查找失败的平均查找长度。怎样才算查找失败?就是当查找某个关键字,按照散列函数找到对应位置后发现该位置数据与要查找的关键字不匹配,再按照线性探测再散列法继续查找,直到查到空位置为止。因为如果在查找时某位置是空位置,那么就算按照线性探测法继续查找,该位置之后也都为空(说的有点啰嗦,能理解就好)。
- 要使得一个关键字查找失败,它肯定和它对比的任何位置上的数都不相等。当关键字的散列函数值为0,它会从0位置开始与98对比,对比失败后,根据线性探测法向后对比22,30,87,11…一直对比到8位置,此时该位置为空,查找失败,共对比了9次;当关键字散列函数值为1,它先和22对比,之后依次对比30,87,11…一直到8位置,共对比8次;以此类推,散列函数值的可能取值是0~6。
查找失败ASL=(9+8+7+6+5+4+3)/7=6. 答案是6
【注1】从上面的计算可以看出:
查找成功的ASL针对散列表中实际存在的所有关键字,即计算每个关键字被找到所需探测次数的平均值。
查找失败的ASL针对散列函数可能返回的所有位置,即计算查找一个不存在的关键字时的探测次数的平均值。
【注2】某个元素被删除后,在散列表中的位置不能置为空,而是标记一下删除。因为如果置为空的话,当查找的关键字在该位置之后时会导致查找失败。以此表为例:H(k)=k%7
如果删除了43并置为空,
当查找15时,查找位置1不匹配,线性探测法查找位置2,为空,查找失败。显然是错误的。
四、散列表的特性和与其它的比较
这部分是我问deepseek的总结,可以对散列表有个更好的认识。
散列表与线性表、树的比较
- 散列表(哈希表)
- 机制:通过哈希函数将键(Key)直接映射到存储位置,实现近似直接访问。
- 时间复杂度:平均情况 O(1)(无冲突时),最坏情况 O(n)(所有键冲突时)。
- 数据特性:数据无序存储,不支持自然顺序遍历。
- 线性表(数组/链表)
- 无序线性表:需遍历所有元素,时间复杂度 O(n)。
- 有序线性表:可通过二分查找优化至 O(log n),但需额外维护有序性。
- 数据特性:顺序存储(数组)或链式存储(链表)。
- 树表(BST、AVL、红黑树等)
- 机制:基于树形分层结构,通过比较键值逐步缩小搜索范围。
- 时间复杂度:平衡树(如 AVL、红黑树)稳定在 O(log n)。
- 数据特性:数据按顺序组织,支持中序遍历等有序操作。
数据结构 | 典型场景 | 关键优势 | 主要缺陷 |
---|---|---|---|
散列表 | 缓存、字典、数据库索引(精确查询) | O(1) 平均操作时间 | 无序、冲突处理开销 |
线性表 | 小数据集、频繁遍历、简单实现 | 实现简单、内存紧凑 | O(n) 查找、低效插入删除 |
树表 | 范围查询、有序遍历(如数据库区间查询) | O(log n) 稳定性能、有序性 | 实现复杂、略慢于哈希表 |
- 选择散列表:当需要极致单点查询速度,且无需维护数据顺序时(如缓存、键值存储)。
- 选择树表:当需要范围查询、有序遍历或稳定对数时间复杂度时(如数据库索引)。
- 选择线性表:当数据量小、实现简单或内存空间敏感时(如临时数据存储)。
散列表的核心优势
- 极快的平均查找速度
哈希表在理想情况下(低冲突)的查找、插入、删除操作均为 O(1),远快于线性表的 O(n) 和树表的 O(log n),适合高频单点查询场景(如缓存、字典)。 - 直接访问特性
通过哈希函数直接定位数据,无需逐层比较或遍历,尤其适合精确匹配(如数据库主键查询)。 - 插入与删除的高效性
在负载因子合理时,插入和删除操作的时间复杂度接近 O(1),优于树表的 O(log n) 和线性表的 O(n)(数组需移动元素)。
散列表的局限性
- 无法维护数据顺序
哈希表数据无序存储,不支持范围查询(如查找 10~20 之间的值)或顺序遍历,需额外维护索引。 - 冲突处理的开销
哈希冲突可能导致性能下降(如链地址法的链表过长),需合理设计哈希函数和动态扩容策略。 - 内存空间占用
为减少冲突需预留空槽,空间利用率低于紧凑存储的线性表或树表。