1、哈希表
散列表(哈希表),是根据关键码值而直接进行访问的数据结构。
某种函数使元素的存储位置与它的关键码之间能够建立一一映射的关系,以加快查找的速度。这个映射函数叫做散列函数(哈希函数),存放记录的数组叫做散列表。
2、几种常见的哈希函数
1)直接定址法(常用)
取关键字的某个线性函数为散列地址:Hash(Key)= A*Key + B
优点:简单、均匀
缺点:需要事先知道关键字的分布情况
使用场景:适合查找比较小且连续的情况
2)除留取余法(常用)
设散列表中允许的地址数为m,按照哈希函数:Hash(key) = key% p,将关键码转换成哈希地址
3)平方取中法
假设关键字为1234,对它平方就是1522756,抽取中间的3位227作为哈希地址;
适合场景:不知道关键字的分布,而位数又不是很大的情况
4)随机数法
取关键字的随机函数值为它的哈希地址,即H(key) = random(key),其中random为随机数函数。
使用场景:通常应用于关键字长度不等时。
3、哈希冲突
不同关键字通过相同哈希哈数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞。
解决方式:闭散列和开散列
3.1 闭散列
闭散列(开放定址法),当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把key存放到冲突位置中的“下一个” 空位置中去。
如何寻找下一个空位置?
1)线形探测
当哈希表还没有满时,从发生哈希冲突的位置,开始一次遍历,直到找到下一个空位置为止(如果到哈希表结尾还是没找到的话,那么就从哈希表头继续找)
hash(key) = key % capacity;
问题:在哈希表中不能随便删除元素。假如删除5,当我们找55的时候,哈希函数计算出5的位置,发现数字不存在,那么就找不到55。
优点:实现简单
缺点:一旦发生哈希冲突,所有的冲突连在一起,容易产生数据“堆积”。不同关键码占据了可利用的空位置,使得寻找某关键码的位置需要许多次比较,导致搜索效率降低。
2)二次探测
当发生哈希冲突的时候,计算下一个 “空位置” 的方式不同:Hi: = (H0 + )% m,其中 i = 0,1,2,3…,当计算出来的位置有元素时,那么就继续计算下一个位置,直到计算出来的位置没有元素时(前提是:元素的个数小于哈希表的容量)
哈希表的空间利用率比较低:当表的长度为质数且表装载因子a不超过0.5时,新的表项一定能够插入,而且任何一个位置都不会被探查两次。因此只要表中有一半的空位置,就不会存在表满的问题(也就是说,在容量用到一半的时候,就会进行扩容,对空间的利用率非常低)。
3.2 闭散列删除问题
解决闭散列删除元素的方法:可以设置三种状态,empty(当前位置没有元素)exist(当前位置有元素)delete(当前位置元素被删除)
我们在删除元素的时候,将该位置设置为delete,当我们查找元素的时候,如果发现标记是delete,那么就继续往后找。
3.3 开散列
开散列法(链地址法、开链法),首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中。
3.4 开散列扩容问题
如果插入元素发生哈希冲突的概率很高时,导致出现哈希桶中会出现一个单链很长的情况,此时就需要扩容。开链法最好的情况就是每个桶中只挂一个节点,如果在继续插入元素的话,那么就会发生哈希冲突,那么就在此时给哈希表扩容。
3.5 闭散列和开散列
开散列处理哈希冲突时,需要增设指针,似乎增加了存储开销。
闭散列保留大量的空闲空间来保持效率。由此,开散列比闭散列更节省空间。