哈希(Hash)是一种常用的数据结构和算法,用于将数据快速映射到一个固定大小的索引值,从而实现高效的数据查找、插入和删除操作。哈希算法能够通过计算数据的哈希值,将其均匀地映射到哈希表(Hash Table)中。
理想的搜索方法:可以不经过任何比较,一次直接从表中得到要搜索的元素。
如果构造一种存储结构,通过某种函数使元素存储位置与它的关键码之间能够建立一一映射的关系,那么在查找时通过该元素可以很快找到该元素。
当向该结构中插入元素时:
根据待插入元素的关键码,以此函数计算出该元素的存储位置并按此位置进行存放。
搜索元素时:
对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,在节后中按此位置取元素比较,若关键码相等,则搜索成功。
该方式即为哈希(散列)方法,哈希方法中使用的转换函数称为哈希(散列)函数,构造出来的结构称为哈希表(或称散列表)。
这里要解决的问题就是比如对于数据集合(25,66,79,98),应该采用什么样的映射方式,映射到哈希表中呢?
映射方法多种多样,下面我们讲解两种:直接定址法和除留余数法。
直接定址法(Direct Addressing):使用一个固定的偏移量或简单的数学运算将输入直接映射到哈希表位置。映射值跟关键字直接或者间接相关。
在使用直接定址法时,每一个可能的输入值都对应着哈希表中的一个位置。具体来说,当使用直接定址法时,我们可以通过一个固定的偏移量或者一个简单的数学运算来计算输入值对应的哈希表位置。这个偏移量或数学运算通常与输入值的某些特征相关。
假设我们有一个存储学生信息的哈希表,其中每个学生的学号是唯一的。我们将学生的学号作为输入值,并将其对应的哈希表位置存储该学生的信息。
首先,我们需要定义一个合适大小的哈希表,该表的大小应该能够容纳所有可能的输入值。
然后,我们定义一个直接定址函数,该函数接受学生的学号作为输入,并通过简单的运算得到对应的哈希表位置。
当需要插入或查找学生信息时,我们只需通过学号运算得到对应的哈希表位置,并在该位置存储或检索学生信息。
直接定址法的优点是操作简单,时间复杂度为O(1),即常数级别。然而,它要求输入的取值范围必须是已知的且能够预先计算出对应的哈希表位置。如果输入的范围很大或者不连续,直接定址法可能会导致哈希表中的某些位置被频繁使用,而其他位置却未被使用,从而影响了哈希表的效率。
直接定址法是没有哈希冲突的,因为每个值都映射了一个唯一位置。
除留余数法(Division Method):将输入值除以一个不大于哈希表大小的固定数,并保留余数作为哈希值。
所谓除,就是除以哈希表的大小,留就是留其余数。
首先,确定哈希表的大小,通常选择一个质数或者接近质数的数作为哈希表的大小。
将输入值除以哈希表的大小,并取得其余数。这个余数就作为该输入值的哈希值。
将计算得到的哈希值作为索引,在哈希表中存储或查找对应的数据。
示例:假设哈希表的大小为10,我们要将输入值23、45和67映射到哈希表中。
将25除以10,得到的商为2,余数为5。因此,输入值25的哈希值为5。
将66除以10,得到的商为6,余数为6。因此,输入值66的哈希值为6。
将79除以10,得到的商为7,余数为9。因此,输入值79的哈希值为9。
将98除以10,得到的商为9,余数为8。因此,输入值98的哈希值为8。
在实际应用中,除留余数法可以很容易地通过取余运算来实现,因为大多数编程语言都提供了取余操作符。
除留余数法的优点是简单、高效,并且适用于大多数情况。然而,如果输入值的分布不均匀或者哈希表大小选择不当,可能会导致哈希冲突(Hash Collision)的发生,即不同的输入值映射到了同一个哈希值。为了解决哈希冲突,需要采用适当的冲突处理方法,如链接法(重点)(Chaining)或开放定址法(Open Addressing)等。
闭散列(Closed Hashing),也被称为开放定址法(Open Addressing),是一种哈希冲突解决方法,用于解决哈希表中发生冲突的情况。与链接法(Chaining)不同,闭散列将所有元素存储在哈希表中,而不是通过链表连接。
在闭散列中,当发生哈希冲突时,会尝试寻找哈希表中的下一个可用位置,直到找到一个空槽或者遍历整个哈希表。这里有几种常见的闭散列的方法:
线性探测(Linear Probing):当发生冲突时,将新的元素插入到冲突位置的下一个可用位置,即逐个向后查找,并在找到空槽或遍历整个哈希表后停止。
二次探测(Quadratic Probing):当发生冲突时,使用二次增量来查找哈希表中的下一个位置,而不是线性地向后查找。
二次探测让冲突的一片数据相对更分散了,不会聚集到一起,形成连片冲突。但是闭散列的开放定址法不是一种好的解决方式,因为它是一种抢占式的思路(发现自己的位置被占了,就回去占别人的位置),也就是说它的哈希冲突会互相影响。
无论是哪种探测方法,在哈希表逐渐接近满的过程中,插入数据的效率会越来越低,因此闭散列哈希表不能满了再增容。
那么如何解决这个问题呢?
要在快接近满的时候就增容,这里我们提出一个概念——负载因子,负载因子 = 表中的数据个数 / 表的大小。比如,闭散列的哈希表中,负载因子到0.7就可以开始增容。一般情况下,负载因子越小,冲突概率越低,效率越高。相反,负载因子越大,冲突概率越高,效率越低。
其实控制负载因子是一种以空间换时间的思路,因此负载因子也不能控制的太小,不然会导致大量的空间浪费,所以负载因子一般取一个折中值。
我们可以这样设计增容:
开一个2倍的空间出来
将旧表中的数据重新映射到新表
释放旧表的空间
拉链法(Chaining)是一种解决哈希冲突的方法,也被称为链接法。它通过在哈希表中的每个槽中存储一个链表(或其他可扩展结构,如红黑树)来处理冲突。当多个键值对映射到同一个位置时,它们会以链表的形式连接在一起。
根据拉链发设计出来的数据结构也被称为哈希桶(Hash Bucket)。
假设有一个大小为8的哈希表,其中包含以下键值对:
(key1, value1)
(key2, value2)
(key3, value3)
使用哈希函数计算每个键的哈希值,并将键值对插入到对应的槽中:
key1 的哈希值为 2,将 (key1, value1) 插入到槽 2 的链表中
key2 的哈希值为 5,将 (key2, value2) 插入到槽 5 的链表中
key3 的哈希值为 2,将 (key3, value3) 追加到槽 2 的链表末尾
哈希表的内部结构如下:
槽 0:
槽 1:
槽 2:(key1, value1) -> (key3, value3)
槽 3:
槽 4:
槽 5:(key2, value2)
槽 6:
槽 7:
在使用拉链法时,通过哈希函数计算键的哈希值,并访问对应的槽。如果槽中的链表为空,表示没有冲突发生;如果链表不为空,则需要在链表中搜索具有相同哈希值的键并找到所需的键值对。
拉链法的优点是简单且易于实现,适用于哈希表大小固定、预期的负载因子较低的情况。同时,它可以有效地处理大量的冲突,因为每个槽中可以存储多个键值对。
然而,拉链法也存在一些缺点。当负载因子较高时,即槽中的链表较长时,查找操作的时间复杂度可能增加,因为需要在链表中进行线性搜索。此外,拉链法在内存使用方面可能存在一些额外的开销,因为需要维护链表的指针。
总之,拉链法是一种解决哈希冲突的方法,通过在哈希表的每个槽中存储一个链表来保存具有相同哈希值的键值对。它是一种常见且实用的解决冲突的方法,能够处理大量的冲突情况。