散列表介绍
概念
- 原始数据叫作 键(键值) 或 关键字(key) ;
- 将原始数据转化为数组下标的映射方法称为 散列函数(或“Hash 函数”“哈希函数”,hash function) ;
- 散列函数计算得到的值就叫作 散列值(或“Hash 值”“哈希值”,table)
散列表
散列表(Hash table) 也叫哈希表:根据键Key
直接访问在内存存储位置的数据结构。
通过计算一个关于键值的函数,将所需查询的数据映射到表中一个位置来访问记录,加快查找速度。这个映射函数称作 散列函数 ,存放记录的数组称为 散列表 。
散列函数
它就是一个函数。
把它定义为hash(key)
,其中key
表示元素的键值,hash(key)
的值表示经过散列函数计算的到的散列值。
- 确定性: 如果两个散列值(同一个散列函数)不相同,那么两个散列值的原始输入也是不相同。
- 散列碰撞: 散列函数的输入和输出不是唯一对应关系,两个散列值相同,两个输入的值很可能是相同,也可能不同。
- 不可逆: 一个哈希值对应无数明文,但是你并不知道是哪个。
- 混淆特性: 输入一些数据计算出散列值,然后部分改变输入值,一个具有强混淆特性的散列函数会产生一个完全不同的散列值。
散列函数
MD5
MD5: 信息摘要算法5,用于确保信息传输完整一致。将数据运算为另一固定长度值,是杂凑算法的基础原理。
MD5: 输入不定长度信息,输出固定长度128-bits
的算法,经过程序流程,生成4个32位数据,最后成为128-bits
散列。
过程: 求余、取余、调整长度、与链接变量进行循环运算得出结果。
应用: 计算广泛用于检查错误,软件通过计算MD5
来检验下载到的碎片完整性。
SHA-1
SHA-1: 安全散列算法1 是一种密码散列函数,SHA-1
可以生成一个称为消息摘要的160
位散列值,散列值通常呈现形式为40
个十六进制数。
散列冲突
理想中的散列函数希望达到 如果 key1 ≠ key2,那 hash(key1) ≠ hash(key2)
事实,再好的散列函数也不能避免 散列冲突 ,比如10个苹果放在9个抽屉里,总有一个抽屉有两个苹果
对于散列表来说,无论存储区域(n)多大,当需要存储数据大于n
时,必然会存在哈希值相同的情况。这就是所谓的 散列冲突
散列冲突解决方法
常用散列冲突方法有两类:
- 开放寻址法
- 链表法
开放寻址法
开放寻址法是一种解决碰撞的方法,对于开放寻址冲突解决方法,比较经典的有:
- 线性探测方法(Linear Probing)
- 二次探测(Quadratic probing)
- 双重散列(Double hashing)
1.线性探测方法
我们往散列表中插入数据时,如果某个数据经过散列函数散列之后,存储位置已经被占用了,我们就从当前位置开始,依次往后查找,看是否有空闲位置,直到找到为止。
弊端: 当散列表插入数据越来越多,散列冲突发生的可能性就会越来越大,空闲位置越来越少,线性探测的时间就会越久(依次往后查找)。极端情况下,需要从头到尾探测整个散列表,所以最坏情况下的时间复杂度为 O(n)。
2.二次探测方法
二次探测是二次方探测法的简称。顾名思义,使用二次探测进行探测的步长变成了原来的“二次方”,也就是说,它探测的下标序列为 hash(key)+0
,hash(key)+1^2
或[hash(key)-1^2]
,hash(key)+2^2
或[hash(key)-2^2]
。
3.双重散列法
就是不仅使用一个散列函数,而是使用一组散列函数hash1(key),hash2(key),hash3(key)。。。。。
,先用第一个散列函数,如果得到的存储位置被占用,再用第二个散列函数,依次类推,直到找到空闲存储位置。
总结
不管采用哪种探测方法,只要当散列表中空闲位置不多的时候,散列冲突的概率就会大大提高。为了尽可能保证散列表的操作效率,一般情况下,需要尽可能保证散列表中有一定比例的空闲槽位。
加载因子: 表示 Hsah
表中元素的填满的程度,若加载因子越大,则填满的元素越多,这样的好处是:空间利用率高了,但冲突的机会加大了。反之,加载因子越小,填满的元素越少,好处是冲突的机会减小了,但空间浪费多了。
链表法
更加常用的散列冲突解决办法,相比开放寻址法,它要简单很多。
在散列表中,每个位置对应一条链表,所有散列值相同的元素都放到相同位置对应的链表中。
- 插入:通过散列函数计算出对应的散列槽位,将其插入到对应链表中即可时间复杂度:O(1)。
- 查找、删除:需要遍历,时间复杂度跟链表的长度 k 成正比,也就是 O(k)
k = (数据个数)/槽的个数
。
选择冲突的方法
1、开放寻址法
- 优点
- 易序列化
- 有效利用
CPU
缓存加速查询速度(数据存储在数组中)
- 缺点
- 删除操作需要特殊标记已删除的数据
- 冲突代价高于链表法
- 场景
- 数据量比较小、装载因子小的时候
2、链表法
- 优点
- 内存的利用率比开放寻址法要高(链表结点可在需要时再创建)
- 对大的装载因子的容忍度更高(只要散列函数的值随机均匀,即使装载因子很大,虽查找效率会下降,但比顺序查找要快)
- 缺点
- 对于比较小的数据的存储,比较消耗内存(存储指针)
- 链表的结点不连续,对CPU缓存不友好,执行效率也有一定的影响
- 改进链表法
- 链表 ==》其他高效的动态数据结构(eg:跳表、红黑树)。
- 适用
- 适合存储大对象、大数据量的散列表,而且,比起开放寻址法,它更加灵活,支持更多的优化策略,比如用红黑树代替链表。
工业级散列表
分析对象:Java 中 HashMap
1、初始大小
- HashMap 默认初值为 16,可修改==》较少动态扩容次数,可提升HashMap的性能。
2、装载因子和动态扩容
- 最大装载因子默认为 0.75
- 当 HashMap 中元素个数超过 0.75 * capacity(capacity表示散列表的容量)时,就会启动扩容,每次扩容都会是原来的两倍大小。
3、散列冲突解决方法
- 采用链表法解决冲突。
- 即使负载因子和散列函数设计得再合理,也免不了会出现拉链过长的情况,一旦出现拉链过长,则会严重影响 HashMap 的性能。
- 优化(JDK1.8):
- 当链表长度≥8,则链表转换为红黑树 进行快速增删改查 提升HashMap性能
- 当链表长度≤8,则红黑树转换为链表(数据量小时,红黑树要维护平衡,性能优势不明显)