文章目录
- 一、 什么是哈希表
- 二、 哈希冲突
- 2.1 为什么会出现冲突
- 2.2 如何避免出现冲突
- 2.3 出现冲突如何解决
- 三、模拟实现哈希桶/开散列(整型数据)
-
- 四、模拟实现哈希桶/开散列(泛型)
-
- 五、区别
- 5.1 TreeMap 和 HashMap 的区别
- 5.2 TreeSet 和 HashSet 的区别
- 六、HashMap 源码分析
- 6.1 成员变量+结点定义
- 6.2 构造方法
- 6.3 put()
一、 什么是哈希表
- 是个存储结构:可以让我们一次从表中直接拿到想要的元素,时间复杂度为O(1)
- 为什么能实现O(1):通过哈希(散列)方法,使元素的存储位置和它的关键码之间建立一一映射的关系
- 如果想要存取元素,都是利用哈希(散列)方法 + 关键码,从而计算出index位置,然后进行操作(怎么放的就怎么给它取出来)
- 哈希函数示例:hash(key) = key % capacity
二、 哈希冲突
2.1 为什么会出现冲突
- 原因:两个不一样的关键字通过相同的哈希函数映射到了相同的位置
- 两个不一样的假如哈希函数是【hash(key) = key % capacity】,如果有两个key,分别为4和14,capacity为10,此时4和14生成的位置都是一样的
2.2 如何避免出现冲突
- 哈希冲突无法规避:由于哈希表底层数组的容量往往是小于实际要存储的关键字的数量的,所以冲突的发生是必然的,我们能做的只是尽量降低冲突率
- 方式:
- 方式一:将哈希函数设置地更为合理。不过一般Java库已经帮我们写好了哈希方法,不需要程序员去设计
- 哈希函数设计原则:【如果有m个元素,哈希出来的地址一定在0 ~ m-1】 + 【元素能够均匀地分布在整个空间里】 + 【简单】
- 直接定制法:取关键字的某个线性函数为散列地址:Hash(Key)= A*Key + B
- 除留余数法
- Hash(key) = key% p(p<=capacity),p是个最接近或者等于capacity的p
- 平方取中法
- 折叠法
- 随机数法
- 数学分析法
- 方式二:调节负载因子
2.3 出现冲突如何解决
- 解决方法一:闭散列(将key存到哈希冲突位置的其他空位置去)
- 寻找空位置方法:
- 线性探测:找到下一个空的位置,然后把冲突的key放进去。但这样会把冲突的元素都挤在一起
- 二次探测:
- 闭散列缺陷:
- 数组利用率/空间利用率不高:利用率高的情况是把同样下标的放在一起,不占用其他格子
- 不方便删除:假如4和14都在同一个下标,14放在了其他位置,但我们定义出来是在4下标,此时不好删除
- 解决方法二:开散列/哈希桶
- 关于O(1)时间的复杂度:
- 虽然哈希表一直在强调哈希冲突,但其实实际中我们认为哈希表的冲突率是不高的,即每个桶中的链表长度是一个常熟。所以我们通常认为哈希表的插入/删除/查找的时间复杂度为O(1)
三、模拟实现哈希桶/开散列(整型数据)
3.1 结构
public class HashBucket {
static class Node {
private int key;
private int value;
private Node next;
public Node(int key, int value) {
this.key = key;
this.value = value;
}
}
public Node[] array;
public int usedSize;
private static final float DEFAULT_LOAD_FACTOR = 0.75f;
public HashBucket() {
this.array = new Node[10];
}
}
3.2 插入元素
- 思路:
- 首先根据key和哈希函数计算出对应的index,由于该index下包含了冲突的元素,所以我们需要遍历该链表
- 重复的值需要更新:注意,因为HashMap是继承了Map接口,而Map的一大特点就是【如果有相同的key,会更新Value值】,所以如果有相同的我们需要更新
- 如果遍历完发现没有重复的,就进行插入,可以头插也可以尾插,此处我们用的是尾插
- 插入完毕后,需要计算负载因子,如果负载因子大于定义的值,就需要扩容
- 扩容需要注意的问题:需要把桶中的数据一个个拿出来重新哈希到新的数组中。因为扩容后,原本的key哈希后得到的index很可能不是原来的index了,所以需要重新哈希。
public void put(int key,int val) {
Node node = new Node(key,val);
int index = key % array.length;
Node cur = array[index];
while (cur != null) {
if(cur.key == key) {
cur.value = val;
return;
}
cur = cur.next;
}
node.next = array[index];
array[index] = node;
usedSize++;
if(loadFactor() >= DEFAULT_LOAD_FACTOR) {
resize();
}
}
private void resize() {
Node[] tmpArray = new Node[array.length * 2];
for (int i = 0; i < array.length; i++) {
Node cur = array[i];
while (cur != null) {
Node curNext = cur.next;
int index = cur.key % tmpArray.length;
cur.next = tmpArray[index];
tmpArray[index] = cur;
cur = curNext;
}
}
array = tmpArray;
}
private float loadFactor() {
return usedSize*1.0f / array.length;
}
3.3 获取元素
public int get(int key) {
int index = key % array.length;
Node cur = array[index];
while (cur != null) {
if(cur.key == key) {
return cur.value;
}
cur = cur.next;
}
return -1;
}
四、模拟实现哈希桶/开散列(泛型)
4.1 结构
public class HashBucket<K,V> {
static class Node<K,V> {
private K key;
private V value;
private Node<K,V> next;
public Node<K,V>(K key, V value) {
this.key = key;
this.value = value;
}
}
public Node<K,V>[] array;
public int usedSize;
private static final float DEFAULT_LOAD_FACTOR = 0.75f;
public HashBucket() {
this.array = (Node<K,V>[])new Node[10];
}
}
4.2 插入元素
public void put(K key,V val) {
Node<K,V> node = new Node<>(key,val);
int hash = key.hashCode();
int index = hash % array.length;
Node<K,V> cur = array[index];
while (cur != null) {
if(cur.key.equals(key)) {
cur.val = val;
return;
}
cur = cur.next;
}
node.next = array[index];
array[index] = node;
usedSize++;
}
4.3 获取元素
- 代码解析:
- 自定义类型需要重写 equals 和 hashCode方法,hashCode用来找index位置,equals用来判断元素是否相同
class Person {
public String id;
public Person(String id) {
this.id = id;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
return Objects.equals(id, person.id);
}
public int hashCode() {
return Objects.hash(id);
}
}
public V get(K key) {
int hash = key.hashCode();
int index = hash % array.length;
Node<K,V> cur = array[index];
while (cur != null) {
if(cur.key.equals(key)) {
return cur.val;
}
cur = cur.next;
}
return null;
}
- 测试:因为此时person1和person2的hashCode结果是一样的,所以最后能打印出的name是【zhangsan】,即可以用person2去找到person1
public static void main(String[] args) {
Person person1 = new Person("1234");
Person person2 = new Person("1234");
HashBuck<Person,String> hashBucket = new HashBucket<>();
hashBuck.put(person1,"zhangsan");
String name = hashBuck.get(person1);
System.out.println(name);
}
五、区别
5.1 TreeMap 和 HashMap 的区别
Map底层结构 | TreeMap | HashMap |
底层结构 | 红黑树 | 哈希桶 |
插入/删除/查找时间复杂度 | O(logN) | O(1) |
是否有序 | 关于Key有序 | 无序 |
线程安全 | 不安全 | 不安全 |
插入/删除/查找区别 | 需要进行元素比较 | 通过哈希函数计算哈希地址 |
比较与覆写 | key必须能够比较,否则会抛出 ClassCastException异常 | 自定义类型需要覆写equals和 hashCode方法 |
应用场景 | 需要Key有序场景下 | Key是否有序不关心,需要更高的时间性能 |
5.2 TreeSet 和 HashSet 的区别
Map底层结构 | TreeMap | HashMap |
底层结构 | 红黑树 | 哈希桶 |
插入/删除/查找时间复杂度 | O(logN) | O(1) |
是否有序 | 关于Key有序 | 无序 |
线程安全 | 不安全 | 不安全 |
插入/删除/查找区别 | 按照红黑树的特性来进行插入和删除 | 先计算key哈希地址,然后进行插入和删除 |
比较与覆写 | key必须能够比较,否则会抛出 ClassCastException异常 | 自定义类型需要覆写equals和 hashCode方法 |
应用场景 | 需要Key有序场景下 | Key是否有序不关心,需要更高的时间性能 |
六、HashMap 源码分析
6.1 成员变量+结点定义
6.2 构造方法
- Map<String,Intger> map1 = new HashMap<>(1000):此时写着容量是1000,但实际上是2次幂数,容量为1024
6.3 put()