目录
哈希表
哈希(散列)函数:
通过这个函数,可以使得存放的值与其存储位置产生一一对应的关系,那这个函数就时哈希(散列)函数。比如
哈希冲突 :
可以看到,存放数据的时候会出现有的数据改存的位置被存放了,这样的情况就叫哈希冲突。
负载因子:哈希表中的数据个数 / 表长
负载因子越大,冲突概率越大。
减少冲突:
1.优化哈希函数
哈希函数的定义域必须包括需要存储的全部关键码,而如果哈希表允许有 m 个地址时,其值域必须在 0 到 m-1 之间哈希函数计算出来的地址能均匀分布在整个空间中哈希函数应该比较简单
常见的哈希函数有
1. 直接定制法 --( 常用 )取关键字的某个线性函数为散列地址: Hash ( Key ) = A*Key + B 优点:简单、均匀 缺点:需要事先知道关 键字的分布情况 使用场景:适合查找比较小且连续的情况2. 除留余数法 --( 常用 )设散列表中允许的 地址数为 m ,取一个不大于 m ,但最接近或者等于 m 的质数 p 作为除数,按照哈希函数:Hash(key) = key% p(p<=m), 将关键码转换成哈希地址
2.闭散列
1. 线性探测 ,如上图,从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止。
2. 二次探测 ,和线下探测基本一样,只不过下一个空位置的方法为:newIndex = (oldIndex+ i² )% m, 或者:newIndex = (oldIndex - i²)% m。其中:i = 1,2,3…
这两种方法都不是Java所使用的方法,Java使用的是开散列。
3.开散列(哈希桶)
开散列法又叫链地址法 ( 开链法 ) ,首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中。
简单实现哈希桶(无树化)
import java.util.Arrays;
import java.util.HashMap;
// key-value 模型
public class HashBucket {
private static class Node {
private int key;
private int value;
private Node next;
public Node(int key, int value) {
this.key = key;
this.value = value;
}
}
private Node[] array;
private int size; // 当前的数据个数
private static final double LOAD_FACTOR = 0.75;//负载因子
private static final int DEFAULT_SIZE = 8;//默认桶的大小
public HashBucket() {
array = new Node[8];
size = 0;
}
public void put(int key, int value) {
Node node = new Node(key, value);
int tmp = key % array.length;
//不在数组里
if (array[tmp] != null) {
//使用尾插法放到哈希桶后面
Node cur = array[tmp];
while (cur.next != null) {
cur = cur.next;
}
cur.next = node;
} else {
array[tmp] = node;
}
size++;
double LoadFactor = loadFactor();
if (LoadFactor >= LOAD_FACTOR) {
//扩容,重放
resize();
}
}
//扩容
private void resize() {
int oldLength = array.length;
Node[] newArray = new Node[oldLength * 2];
int newLength = newArray.length;
for (int i = 0; i < oldLength; i++) {
//遍历原数组当中的数据
Node outCur = array[i];
//数据不为空进行复制
if (outCur != null) {
//数组内部用新引用指向
Node inCur = outCur;
//内部的链表可能是一串,所以用while循环
while (inCur != null) {
int tmp = inCur.key % newLength;
//如果新数组有数据,则使用尾插法
if (newArray[tmp] != null) {
//cur找到尾
Node cur = newArray[tmp];
while (cur.next != null) {
cur = cur.next;
}
//此时cur指向了链表的最后一个结点
//创建一个新结点是为了不改变inCur,因为插入后,next必须置空,这样就找不到后面了
Node node = new Node(inCur.key, inCur.value);
cur.next = node;
}
//如果新数组没有数据,直接插入,创建结点理由同上
else {
Node node = new Node(inCur.key, inCur.value);
newArray[tmp] = node;
}
inCur = inCur.next;
}
}
}
array = newArray;
}
// 头插法
// private void resize() {
// Node[] newArray = new Node[2*array.length];
// for (int i = 0; i < array.length; i++) {
// Node cur = array[i];
// while (cur != null) {
// Node curNext = cur.next;
// int newIndex = cur.key % newArray.length;
// cur.next = newArray[newIndex];
// newArray[newIndex] = cur;
// cur = curNext;
// }
// }
// array = newArray;
// }
private double loadFactor() {
return size * 1.0 / array.length;
}
public int get(int key) {
int tmp = key % array.length;
if (array[tmp] == null) {
return -1;
} else {
Node cur = array[tmp];
while (cur.next != null) {
if (cur.key == key) {
return cur.value;
} else {
cur = cur.next;
}
}
return -1;
}
}
}
HashMap&HashSet
HashMap是实现了Map接口的一个类,它的底层是哈希桶。
HashMap底层结构 | 哈希桶 |
插入 / 删除 / 查找时间 复杂度 | O(1) |
是否有序 | 否 |
线程安全 | 不安全 |
应用场景 |
Key
是否有序不关心,需要更高的
时间性能
|
HashSet是实现了Set接口的一个类,底层是HashMap。
HashMap注意事项:
重写equals和hashCode:
对于自定义类的方法,相较于TreeMap实现Comparable或者Comparator,HashMap则需要重写equals和hashCode方法。
import java.util.HashMap;
import java.util.Map;
class Student {
public String id;
public String name;
public Student(String id, String name) {
this.id = id;
this.name = name;
}
@Override
public String toString() {
return "Student{" + id + " "+ name + "}";
}
}
public class TestHash {
public static void main(String[] args) {
HashMap<Student, String> map = new HashMap<>();
Student student1 = new Student("2022101", "张三");
Student student2 = new Student("2022106", "李四");
Student student3 = new Student("2022101", "张三");
map.put(student1, "A207宿舍");
map.put(student2, "A208宿舍");
map.put(student3, "A208宿舍");
System.out.println(student1.hashCode());
System.out.println(student3.hashCode());
System.out.println(student1.equals(student3));
for (Map.Entry<Student, String> entry: map.entrySet()) {
System.out.println(entry.getKey() + "分配在" + entry.getValue());
}
}
}
上面代码没有重写equals方法和hashCod方法,张三这个人被认为不是同一个人。只要重写了这两个方法,就会按照我们所想的方法进行。
equals()方法:
进入源码我们发现,它所比较的只是引用是否相等,这显然不符合我们Student类的情况。
hashCode()方法:
进入源码,它是一个native方法,也就是C/C++写的方法,我们无法继续看。
但通过其注释的一部分
类Object的equals方法在对象上实现了最有区别的可能等价关系;也就是说,对于任何非空引用值x和y,当且仅当x和y引用同一对象时(x==y的值为true),此方法返回true。
请注意,通常需要在该方法被重写时重写hashCode方法,以便维护hashCod方法的一般约定,该约定规定相等的对象必须具有相等的哈希代码。
我们可以得知,它与equals方法是密不可分的。
那么我们重写后的这两个方法是在哪里发挥作用了呢?
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
class Student {
public String id;
public String name;
public Student(String id, String name) {
this.id = id;
this.name = name;
}
@Override
public String toString() {
return "Student{" + id + " "+ name + "}";
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Student student = (Student) o;
return Objects.equals(id, student.id) && Objects.equals(name, student.name);
}
@Override
public int hashCode() {
return Objects.hash(id, name);
}
}
public class TestHash {
public static void main(String[] args) {
HashMap<Student, String> map = new HashMap<>();
Student student1 = new Student("2022101", "张三");
Student student2 = new Student("2022106", "李四");
Student student3 = new Student("2022101", "张三");
map.put(student1, "A207宿舍");
map.put(student2, "A208宿舍");
map.put(student3, "A208宿舍");
System.out.println(student1.hashCode());
System.out.println(student3.hashCode());
System.out.println(student1.equals(student3));
for (Map.Entry<Student, String> entry: map.entrySet()) {
System.out.println(entry.getKey() + "分配在" + entry.getValue());
}
}
}
此时代码达到了我们预期效果。
调用不带参数的构造方法,第一次put()时才开辟内存:
equals和hashCode在部分源码中:
总结一下,在哈希桶中,hashCode()把数据定位到数组的哪个地方,而equals()在数组存放链表的头节点开始依次从hashCode()定位的地方往后找,比较数据是否相同,相同就替换value值,不进行插入,如都不相同,就插入新结点。
二者可以类比成查字典。按照拼音查字典,先找第一个目标拼音(类似于hashCode()一下),在具体找字(类似于equals()),相同读音的汉字虽然多,但是有了equals(),就可以区别出来。
初始化空间大小:
初始化空间大小可能并不是表面上看起来那么简单。
总结:当传入目标大小后,开辟的是2^n的值,同时要取到大于接近的值
什么时候树化:
总结:
数组长度 >= 64 && 链表长度 >= 8才可以树化
有什么错误评论区指出,希望可以帮到你。