什么是哈希表?
哈希表
哈希冲突
哈希函数设计
负载因子调节
解决哈希冲突
实现
性能分析
哈希表的实现
HashMap
HashMap和HashSet的区别
HashSet检查重复
HashMap和HashTable的区别
哈希表(Hash table,也叫散列表),是根据关键码值(Key value)而直接进行访问的数据结构。
也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。
顺序结构以及平衡树中,元素关键码与其存储位置之间没有对应的关系,因此在查找一个元素时,必须要经过关键码的多次比较。顺序查找时间复杂度为O(N),平衡树中为树的高度,即O( log N),搜索的效率取决于搜索过程中元素的比较次数。
理想的搜索方法:可以不经过任何比较,一次直接从表中得到要搜索的元素。
如果构造一种存储结构,通过某种函数(hashFunc)使元素的存储位置与它的关键码之间能够建立一一映射的关系
那么在查找时通过该函数可以很快找到该元素。
当向该结构中:
- 插入元素
根据待插入元素的关键码,以此函数计算出该元素的存储位置并按此位置进行存放。
- 搜索元素
对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置取元素比较,
若关键码相等,则搜索成功。
该方式即为哈希(散列)方法,哈希方法中使用的转换函数称为哈希(散列)函数,构造出来的结构称为哈希表(HashTable)(或者称散列表)。
两个不同的元素,通过哈希函数计算出来的哈希地址是相同的,这种现象称为哈希冲突或哈希碰撞。
由于我们哈希表底层数组的容量往往是小于实际要存储的关键字的数量的,这就导致一个问题,冲突的发生是必然的,但我们能做的应该是尽量的降低冲突率。
哈希函数设计的原则:
- 哈希函数的定义域必须包含需要存储的全部关键码,如果散列表允许有m个地址时,其值域必须是0 到m-1之间
- 哈希函数计算出来的地址能均匀分布在整个空间中
- 哈希函数要比较简单
常见的哈希函数
哈希函数设计的越精妙,产生哈希冲突的可能性就会越低,但是无法避免哈希冲突。
冲突率 ~ 元素个数/数组长度
- 对冲突率有一个上限的阈值,所以对于负载因子有一个上限的阈值
- 要降低冲突,需要降低负载因子:元素个数不能懂,所以只能增加数组的长度(扩容)
- Java中一般负载因子是0.75
闭散列(开放定址法)
当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把key存放到冲突位置中的“下一个” 空位置中去。
线性探测法
从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止。
插入
开散列(拉链法)——用另一种数据结构来解决冲突的元素
Java中HashMap使用的就是拉链法来解决哈希冲突的。
首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码归于同一子集合,每一个子集合称为一个桶,
各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中。
每一个桶中放的都是发生哈希冲突的元素。
// 使用拉链法解决冲突
public class MyHashTable {
// 1. 数组
private Node[] array = new Node[11];
// 2. 维护哈希表中的有的元素个数
private int size;
// true: key 之前不在哈希表中
// false: key 之前已经在哈希表中
public boolean insert(Integer key) {
// 1. 把对象转成 int 类型
// hashCode() 方法的调用是核心
int hashValue = key.hashCode();
// 2. 把 hashValue 转成合法的下标
int index = hashValue % array.length;
// 3. 遍历 index 位置处的链表,确定 key 在不在元素中
Node current = array[index];
while (current != null) {
// equals() 方法的调用是核心
if (key.equals(current.key)) {
return false;
}
current = current.next;
}
// 4. 把 key 装入节点中,并插入到对应的链表中
// 头插尾插都可以,头插相对简单
Node node = new Node(key);
node.next = array[index];
array[index] = node;
// 5. 维护 元素个数
size++;
// 6. 通过维护负载因子,进而维护较低的冲突率
if (size / array.length * 100 >= 75) {
扩容();
}
return true;
}
public boolean remove(Integer key) {
// hashCode()
int hashValue = key.hashCode();
// 得到合法下标
int index = hashValue % array.length;
Node preivous = null;
Node current = array[index];
while (current != null) {
if (key.equals(current.key)) {
// 删除
if (preivous != null) {
preivous.next = current.next;
} else {
// current 是这条链表的头节点
array[index] = current.next;
}
size--;
return true;
}
preivous = current;
current = current.next;
}
return false;
}
public boolean contains(Integer key) {
int hashValue = key.hashCode();
int index = hashValue % array.length;
Node current = array[index];
while (current != null) {
if (key.equals(current.key)) {
return true;
}
current = current.next;
}
return false;
}
// O(n)
private void 扩容() {
Node[] newArray = new Node[array.length * 2];
// 搬原来的元素过来
// 不能直接按链表搬运,因为元素保存的下标和数组长度有关
// 数组长度变了,下标也会变
// 所以,需要把每个元素重新计算一次
// 遍历整个数组
for (int i = 0; i < array.length; i++) {
// 遍历每条链表
Node current = array[i];
while (current != null) {
// 高效的办法是搬节点,写起来麻烦
// 我们采用复制节点,简单一点
Integer key = current.key;
int hashValue = key.hashCode();
int index = hashValue % newArray.length;
// 头插尾插都可以,头插简单
Node node = new Node(key);
node.next = newArray[index];
newArray[index] = node;
current = current.next;
}
}
array = newArray;
}
}
在实际的使用过程中,哈希表的冲突率是不高的,冲突的个数也是可控的,每个桶中的链表的长度是一个常数,所以,通常认为哈希表的插入/删除/查找的时间复杂度是O(1).
纯Key模型:HashSet——实现了Set接口
Key-Value模型:HashMap——实现了Map接口
HashMap源码剖析
JDK1.8 之前 HashMap 底层是 数组和链表 结合在⼀起使⽤也就是 链表散列。
HashMap 通过 key 的hashCode 经过哈希函数处理过后得到 hash 值,然后通过 (n - 1) & hash 判断当前元素存放的位置(这⾥的 n 指的是数组的⻓度),如果当前位置存在元素的话,就判断该元素与要存⼊的元素的 hash值以及 key 是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。
使⽤ hash ⽅法是为了防⽌⼀些实现⽐较差的 hashCode() ⽅法【使⽤哈希函数之后可以减少碰撞】
JDK1.8 之后
相⽐于之前的版本, JDK1.8 之后在解决哈希冲突时有了较⼤的变化,
当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间.
——将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树。
HashSet 底层就是基于 HashMap 实现的。(HashSet 的源码⾮常⾮常少,因为除了 clone() 、 writeObject() 、 readObject() 是 HashSet ⾃⼰不得不实现之外,其他⽅法都是直接调⽤ HashMap 中的⽅法。
当把对象加入HashSet时,HashSet 会先计算对象的 hashcode 值来判断对象加⼊的位置,
同时也会与其他加⼊的对象的 hashcode 值作⽐较,如果没有相符的 hashcode, HashSet 会假设对象没有重复出现。
但是如果发现有相同 hashcode 值的对象,这时会调⽤ equals() ⽅法来检查 hashcode 相等的对象是否真的相同。
如果两者相同, HashSet 就不会让加⼊操作成功。
hashcode() 和equals() 方法: