产生原因
在顺序结构或树形结构的数据集合中,我们想要查询一个元素时,必须进行遍历,所以顺序结构的查询时间复杂度为O(N),树形结构查询的时间复杂度为O(log2^N),但是我们想要一种不用遍历就知道其位置的方法,我们在查询时就会很方便。
哈希
哈希也叫作映射,我们可以通过其元素的值(关键码),查找到该元素的具体位置。我们将这种一一对应的称为映射。==哈希是一种存储结构。==通过这种映射方式构造出来的结构称为散列表。
举一个简单栗子:一次函数。
我们知道x值,通过函数表达式,我们就可以计算出y的值。这种一一对应的关系就是映射,这个函数表达式就是哈希函数。
- 插入元素:根据插入的元素(关键码),通过哈希函数计算出存储位置,将元素插入到指定位置。
- 查询元素:将该元素通过哈希函数计算出相应的位置,我们在将该位置的元素取出,判断值是否相等,如果相等则表示存在。
上述的方法确实能够实现查询的时间复杂度为O(1),但是也存在一个问题,如果我们插入的元素是2,22,32,222呢?我们通过哈希函数计算的位置相同,这就是哈希冲突。
哈希冲突
不同的元素通过哈希函数计算出来的位置相同,我们称其为哈希冲突或者哈希碰撞。
产生哈希冲突的原因之一:哈希函数设计缺陷:
- 哈希函数设计的方法:
- 直接定值法:就类似于线性函数 y = kx+b的形式。
- 除留余数法:类似于上述栗子中的方法。
- 平方取中法:取一个数的平方中间几位数。
- 数学分析法:找一些数的规律,利用规律设计哈希函数。
哈希函数的设计是有效的预防哈希冲突,但是不能避免产生哈希冲突。如何避免哈希冲突:闭散列、开散列。
闭散列
开放定值法,当发生哈希冲突时,如果哈希表没有装满,说明在哈希表中必然还有空位置,那么我们可以将元素放到冲突的下一个位置。如何寻找下一个位置。
- 线性探测:从发生冲突的位置开始,依次向后探测,直到找到下一个空位置为止。
- 线性探测插入方法:通过哈希函数判断插入的位置。如果该位置没有元素则直接插入新元素,如果该位置发生哈希冲突,则使用线性探测找到写一个位置。
- 线性探测删除方法:在线性探测不能随便删除一个元素,因为直接删除会影响到其他元素的搜索,所以删除采用的是伪删除。
- 扩容时机
负载因子=当有效元素的个数/闭散列的长度。当负载因子大于0.7时,需要进行扩容。 - 缺点:
容易产生线性堆积,导致查询效率降低。
为了解决线性探测所产生的线性堆积问题,我们避免产生数据堆积在一起,我们提出二次探测。
- 二次探测:探测方式:Hi = (Ho + i^2)%capacity,i取值为1,2,3, ……
- 扩容机制:
当负载因子大于0.5,就需要考虑扩容。
- 扩容机制:
闭散列就是用空间换时间的思想,提高查询效率
开散列
开散列称为链地址法,将通过哈希函数计算出具有相同地址空间的元素放在同一个子集中,这个子集我们称为哈希桶,桶中的元素通过单链表的链接起来。但是单链表插入分为头插与尾插,这里采用头插法进行讲解。
我们可以看出,每一个哈希桶中都存放的是产生哈希冲突的元素。
- 扩容时机:当所有桶中元素的个数等于容量时,哈希表扩容。
开散列中增加链表指针,看似比闭散列中多了一个内存开销,但是由于闭散列中存在二次探测,二次探测的负载因子在达到0.5就需要进行扩容,总和来看,开散列更加节省空间