散列表(Word文档中的单词拼写检查功能)
优势
- 模拟映射关系
- 防止重复
- 缓存、记住数据,以免服务器再通过处理生成。
- 查找、插入、删除都非常快。
- 可以结合散列函数和数组来创建散列表,一般编程语言都提供了实现。
散列表执行各种操作的时间都为O(1),常量时间,无论散列表多大,所需时间都相同。
平均情况下,查找与数组一样快,插入和删除速度与链表一样快。
填装因子: = 散列表占用位置 / 位置总数
填装因子越大,说明空闲位置越少,冲突越多,性能会下降。
一个小的经验规则:一旦填装因子大于0.7,就调整散列表的长度。
一个好的散列函数:SHA。
思想
数组支持按照下标随机访问数据的特性,散列表是数组的一种扩展,由数组演化而来。
散列冲突
- 开放寻址法
出现了冲突就重新探测一个空闲位置插入。
比如线性检测:
查找的过程中,遍历到空闲位置结束。
删除的时候一般不是置空,而是标记为delete,因为置空会影响线性探测。
二次探测:二次探测和线性探测相比,探测的步长为2。
双重散列:不仅仅使用一个散列函数,如果第一个散列函数冲突,就使用第二个散列函数,依次类推。 - 链表法
每个元素位置再接一个链表。那个元素位置叫做桶或槽。多增加了链表的遍历。
时间复杂度和链表的长度成正比。散列比较均匀的话,k = n/m,n = 数据个数,m是槽的个数。
词典检查就是把所有单词存储在散列表中进行查找,验证是否能找到。空间不大,放在内存里就行。
工业级散列表设计
动态扩容,均摊扩容(插入到新的散列表中,同时把老的散列表中一个数据拿到新的里面。)
当数据量比较小、装载因子小的时候,适合采用开放寻址法。这也是Java 中的 ThreadLocalMap 使用开放寻址法的原因。
虽然链表法比较耗费空间,但是如果存储的是大对象,那么链表中的结点占用空间很少,所以可以忽略不计。
比较适合存储大对象,大数据量的散列表,可以用红黑树代替链表。
HashMap 分析
默认是初始大小是16,可以调节。
最大装载因子是0.75,启动扩容会扩大到两倍大小。
底层使用链表法解决冲突,JDK1.8版本中,链表长度超过8时,链表转化为红黑树。当红黑树结点少于8个的时候,红黑树转化为链表。
以下为其散列函数。
int hash(Object key) {
int h = key.hashCode();//返回Java对象的hash code
return (h ^ (h >>> 16)) & (capitity -1); //capicity 表示散列表的大小
}
散列表和链表的结合
将添加、删除、查找操作时间复杂度降到O(1)。
散列表中嵌入双向链表,双向链表增加特殊字段hnext。
结点除了可以使用前驱后继指针访问之外,还可以通过hnext索引访问(拉链)。
疑惑点:为何查找到一个结点之后将其放在链表尾部。
Redis有序集合
Redis和跳表关系很紧密
Java LinkedHashMap
LinkedHashMap可以支持按照插入顺序遍历数据,还支持按照访问顺序来遍历数据。
Linked指的是双向链表。
访问完数据之后也会放到链表结尾,估计是LRU策略(最近最少使用)
小结
散列表无法按照某种顺序快速遍历,一个方法是拷贝到数组中,排序遍历。
散列表是动态数据结构,不停有插入、删除情况。如果按照顺序遍历散列表,那么常常会将散列表和链表(或者跳表)一块使用。