数据结构与算法之美——散列表
1.1 散列思想
将数据以散列函数的方式(键值对)存储
1.2 散列函数
形如hash(key)的键值对函数叫散列函数,hash(key)是值,key是键。
1.3 散列函数设计的基本要求
- 散列函数计算值应该是非负整数
- 如果散列函数的键相等,则函数一定相等
- 如果键不相等,则值也一定不相等
但是在真实的情况下第三个条件很难满足,这种不满足的情况叫它散列冲突。
1.4 散列冲突的解决办法
散列冲突有两类解决办法:开放寻址法、链表法。
- 开放寻址法
开放寻址法的思想是如果出现了散列冲突,就向后探测空闲位置,将其插入。列举下比较简单的方法,比如:线性探测法。
删除操作不能直接将其值赋为null,否则会使寻址法失效,如果一定用的话,可以用个表示来标记要删除的元素,寻址时发现此标识就向后寻找。
其实,线性探测法有很多的问题,比如:当空闲的位置越来越少的时候,寻找时间越来越久,性能会下降很多,最坏的情况下会达到O(n)。除了线性探测,还有二次探测、双重散列等方法可以解决。
不过,不管哪一种方法,空闲位置不多时,性能都会下降很多。一般情况,都会使空闲位置与散列表的长度处于一个健康的比值,这个比值叫装载因子。
- 链表法
链表法是相比寻址法较好的解决散列冲突的解决办法。他会根据键值的不同来划分不同的‘桶’,每一个桶都会对应一条链表,如果存入的键值对已经存在,则将新值插入到链表的空闲位置。
1.5 小结
开放寻址法适合装载因子小于1的情况下,性能比较好;链表法适合(链表的数据要均匀随机分布)大于1的情况。但是,由于链表法是由链表存数据,而链表的指针需要额外消耗内存,如果数据比指针的内存大小小的话,会对比较吃内存。如果是大数据的话可以将指针内存大小忽略不计。数组是对CPU缓存比友好的,对链表不友好。其实也可以将链表法的链表改造为动态的跳表或者红黑树也是可以的,即使在极端情况下,散列表退化为链表,时间复杂度也就是O(log n)。
1.6 散列表的实例的举例
开放寻址法来解决哈希冲突的例子,例如jdkLocalThreadMap,它就是使用开放寻址法;hashMap工业级的散列表,它是链表法来解决哈希冲突的,这样性能不还是会丢失的?不不,这里分析一下工业级散列表的hashMap:
- 初始大小
hashMap默认的初始化大小为16;这个默认值是可以设置的,可以预测数据量的大小,这样可以减少动态扩容的次数,提高性能。
- 装载因子和动态扩容
hashMap的装载因子为0.75,当插入元素个数超过75%,就会自动扩容为原来的2倍。
- 散列冲突的解决办法
hashMap采用链表法解决散列冲突,即使负载因子和散列函数设计的再完美,也无法避免因链子太长而影响性能。所以在jdk1.8的hashMap引入了红黑树,红黑树可以快速的执行增删改查,在链表长度少于8个,使用链表;如果链表长度多于8个时就是用红黑树。因为数据量小时,红黑树为了维护平衡,所以性能优势不太明显。