作者:opLW
参考:王争老师的 《数据结构与算法之美》
学习 《数据结构与算法之美》 的一些简要的笔记。记录一个大体的思路,可能不是很详细。?
目录
1.散列表的定义
2.散列表的存放的方式
3.决定散列表性能的关键点
- 1. 散列表的定义 散列表来源于数组。它借助散列函数对数组这种数据结构进行扩展,利用的是数组支持按照下标随机访问元素的特性。
- 2. 散列表的存放的方式 散列表以键 – 值对形式存在。
- 3. 决定散列表性能的关键点
- 3.1 散列函数的设计
- 1) 基本要求
1.散列函数计算得到的散列值是一个非负整数。因为要根据结果存放在数组中,而数组下标是一个非负数。
2.若key1=key2,则hash(key1)=hash(key2)
3.若key≠key2,则hash(key1)≠hash(key2)。 - 2) 存在的问题 理想情况下是要求不同的key对应不同的hash值,但是由于数组的大小有限,所以无可避免的散列函数会产生相同的值,就是哈希冲突。
- 3) 总体要求
- 散列值要均匀 尽可能让散列后的值随机且均匀分布,这样会尽可能减少散列冲突,即便冲突之后,分配到每个槽内的数据也比较均匀。
- 散列值计算简单耗时短 如果为了均匀而使用太过复杂的散列函数,那么会浪费不必要的计算时间,从而得不偿失。
- 4) 常见方法 直接寻址法、平方取中法、折叠法、随机数法等。
- 5) 例子 java中HashMap的散列函数
int hash(Object key) { int h = key.hashCode(); return (h ^ (h >>> 16)) & (capitity -1); //capicity表示散列表的大小 }
亮点1 获取对象的hashcode以后,先进行移位运算,然后再和自己做异或运算,即:hashcode ^ (hashcode >>> 16),这一步甚是巧妙,是将高16位移到低16位,这样计算出来的整型值将“具有”高位和低位的性质。
亮点2 & (capitity -1) java的HashMap中会使capitity的值始终为2的倍数,这会使得与的结果更加均匀。
- 1) 基本要求
- 3.2 哈希冲突的解决
-
1) 开放地址法 如果出现散列冲突,就重新探测一个空闲位置,将其插入。
- 线性探测法
- 插入数据 当我们往散列表中插入数据时,如果某个数据经过散列函数之后,存储的位置已经被占用了,我们就从当前位置开始,依次往后查找,看是否有空闲位置,直到找到为止。
- 查找数据 我们通过散列函数求出要查找元素的键值对应的散列值,然后比较数组中下标为散列值的元素和要查找的元素是否相等,若相等,则说明就是我们要查找的元素;否则,就顺序往后依次查找。如果遍历到数组的空闲位置还未找到,就说明要查找的元素并没有在散列表中。
- 删除数据 为了不让查找算法失效,可以将删除的元素特殊标记为deleted,当线性探测查找的时候,遇到标记为deleted的空间,并不是停下来,而是继续往下探测。
- 结论 最坏时间复杂度为O(n)
- 二次探测 线性探测每次探测的步长为1,即在数组中一个一个探测,而二次探测的步长变为原来的平方。
- 双重散列 使用一组散列函数,直到找到空闲位置为止。
- 总结:适用场景 如果存放的数据量不是很大,那么可以采取开放地址法。如ThreadLocalMap。
- 线性探测法
-
2) 链地址法 发生冲突时,在当前下标对应的位置上追加链表结构。
-
插入数据 当插入的时候,我们需要通过散列函数计算出对应的散列槽位,将其插入到对应的链表中即可,所以插入的时间复杂度为O(1)。
-
查找或删除数据 当查找、删除一个元素时,通过散列函数计算对应的槽,然后遍历链表查找或删除。对于散列比较均匀的散列函数,链表的节点个数k=n/m,其中n表示散列表中数据的个数,m表示散列表中槽的个数,所以是时间复杂度为O(k)。
-
优点 链表法比起开放寻址法,对大装载因子的容忍度更高。
-
缺点 使用链表需要额外的指针空间;当链表长度太大时,查询的时间复杂度会下降为O(n)。
-
优化思路 当链表长度太大时,我们将链表法中的链表改造为其他高效的动态数据结构,比如跳表、红黑树。这样,即便出现散列冲突,极端情况下,所有的数据都散列到同一个桶内,那最终退化成的散列表的查找时间也只不过是O(logn)。
-
-
- 3.3 装载因子 / 阀值
- 1) 装载因子 用装载因子(load factor)来表示目前散列表已有的数据占散列表长度的比例。装载因子越大,说明空闲位置越少,冲突越多,散列表的性能会下降。
- 2) 阀值 装载因子的上限。
- 3) 出现的目的 不管采用哪种探测方法,当散列表中空闲位置不多的时候,散列冲突的概率就会大大提高。为了尽可能保证散列表的操作效率,一般情况下,我们会尽可能保证散列表中有一定比例的空闲槽位。当达到装载因子达到阀值时,需要扩容,来保持一定比例的空闲槽。
- 4) 计算公式 散列表的装载因子=填入表中的元素个数/散列表的长度
- 5) 装载因子的大小
- 当使用开放地址发解决冲突时,装载因子 要小于1。
- 当使用链地址法解决冲突时,因为可以在对应下标处追加链表,所以装载因子可以大于1。
- 6) 阈值设置 需要权衡时间复杂度和空间复杂度。
- 如果内存空间不紧张,对执行效率要求很高,可以降低装载因子的阈值;
- 如果内存空间紧张,对执行效率要求又不高,可以增加装载因子的阈值。
- 7) 避免低效扩容
- 3.1 散列函数的设计
- 4. 工业级散列表的设计要点
- 5. 使用散列表的具体例子
- Word文档中单词拼写检查功能是如何实现的?
- 思路 字符串占用内存大小为8字节,20万单词占用内存大小不超过20MB,所以用散列表存储20万英文词典单词,然后对每个编辑进文档的单词进行查找,若未找到,则提示拼写错误。
- 假设我们有10万条URL访问日志,如何按照访问次数给URL排序?
- 思路 字符串占用内存大小为8字节,10万条URL访问日志占用内存不超过10MB,通过散列表统计url访问次数,然后用TreeMap(可以排序的散列表)存储散列表的元素值(作为key)和数组下标值(作为value)
- Word文档中单词拼写检查功能是如何实现的?
万水千山总是情,麻烦手下别留情。
如若讲得有不妥,文末留言告知我,
如若觉得还可以,收藏点赞要一起。