前说TreeMap添加,删除,搜索的时间复杂度都是 O(logn),效率算是比较高的了。但是TreeMap有约束条件
1). Key必须具备可比较性
2).元素分布是有序的
但是在实际开发中我们的Key不具有可比较性,Map中的元素分布也不需要顺序。不考虑顺序,不考虑Key的可比较性,Map有更好的实现方案,平均复杂度可以达到O(1)级别。就是使用哈希表来实现Map。
哈希表处理数据的流程如下
我们拥有如下数据
哈希表添加、搜索、删除的步骤都是类似的
1).利用哈希函数生成Key对应的index。
2).根据index操作定位数组元素。
如下图, Jack 通过哈希函数生成的 index 为 14,再通过数组的索引 14 去操作(增,删,改)Jack 对应的 value。
哈希表是典型的空间换时间,下图中的table的空间使用率还是挺低的,存三个映射关系,却需要预留很多的数组空间。
哈希表内部的数组元素,很多地方也叫Bucket(桶),整个数组叫Buckets 或者叫 Bucket Array。
哈希冲突
前面我们讲到了 Map 中的 Key 通过哈希函数生成了数组的索引 index 。如果两个 Key 通过哈希函数算出来的结果 index 相同,就会发生哈希冲突。如下图:Rose 和 Kate 的 index 值重复了。
解决方案
1). 开放地址法:按照一定的规则向其他的地址探测,直到遇到空桶。
①线性探测法:Rose先占据了索引 3。Kate 再算出来索引也为 3,发现 3 已经被占了,然后就往下找到 4,如果 4 也被占了,就再往下找,直到找到没有被占的索引,就将其作为自己的索引。
①平方探测法:就不是一个一个往下找了,比如先找索引3下面 1² 的位置(也就是4),看有没有被占据,如果被占据就找3下面 2 ²(也就是7)…
2).再哈希法:设计多个哈希函数,这个哈希函数算出来冲突,再用其他的哈希函数来算。
3).链地址法:通过链表将同一index的元素串联起来。
哈希表中哈希函数的实现步骤大概如下:
1). 先生成Key的哈希值(整数)
2). 再让Key的哈希值和数组的长度大小进行相关运算,生成一个索引。这个步骤的主要目的就是,让哈希值不要超过数组的长度。
但是取模运算符的效率低下。为了提高效率我们可以使用&来代替%(有前提条件:将数组长度设计为2的幂)
为什么要求数组长度为2的幂,而且还和数组长度减去1 来 & 运算呢?
&运算一定不陌生吧,2^n - 1的二进制一定都是1。
比 2的n次方 小的和 2的n次方减1 &运算就是他本身。
比 2的n次方 大的和 2的n次方减1 & 运算就只取小的部分。
用 & 代替 %,也可以控制索引小于数组长度,而且效率还比%要高。
一个良好的哈希函数
让哈希值更加均匀分布—>减少哈希冲突的次数—>提升哈希表的性能
浮点数在内存中是以二进制存储的
将浮点数存储的二进制格式转化成整数
long类型就是整数啊?那他的哈希值不就是他本身了吗?
并不是的,java定义哈希值是int类型,int类型是4字节32位,而long是8字节64位的。那我们怎么办呢?有一种方法,我们只取long的前32位或者后32位,显然这种方案是不妥当的,因为Key只有一半的信息参与运算。
java官方是采用如下的方案:
这样运算的目的是让高32位和低32位混合运算出32位的哈希值。我们最后强转为int,那么只取最后的32位,也就是图中的橙色部分。
为什么用异或 ^运算而不用其他的(比如&和 |)
用&运算如果高位都是1,那就相当于没算。
用|运算,大部分情况结果都是1。算出来也没意义,冲突太多了。
也没什么好说的了,就是结合了一下浮点数和Long的处理步骤。代码如下
字符串“5480”是如何计算出来的呢?
5 * 10^3 + 4 * 10^2 +8 * 10^1 +9 * 10^0
若字符串是由若干个字符组成的呢?
1).比如“jack”,由j,a,c,k四个字符组成
2).因此“jack”的哈希值可以表示为: j * n^3 + a * n^2 + c * n^1 + k * n^0,我们发现算这个算式 n的次方 要算四次,我们可以做一个优化 [ (j * n + a) * n + c] * n + k,提升效率。
3).在JDK中,乘数n为31,为什么使用31呢?
因为31是一个奇素数,JVM会将31 * i 自动优化成 (i << 5) - i
代码逻辑
public static void main(String[] args) {
String string = "jack";
int length = string.length();
int hashCode = 0;
for(int i = 0;i
以后用到就不需要自己写了,JDK为我们提供了方法string.hashCode(); 调用一下就行了。
上面我们说了5种基本类型的Key生成哈希值的算法。JDK已经为我们封装好了(在基本类型的封装类中),以后再使用的时候直接调用方法即可。
Integer a = 110;
Float b = 10.1f;
Long c = 1516l;
Double d = 10.9;
String e = "rose";
System.out.println(a.hashCode());
System.out.println(b.hashCode());
System.out.println(c.hashCode());
System.out.println(d.hashCode());
System.out.println(e.hashCode());
首先提出几个问题
1.自定义类型如何计算出哈希值
2.两个自定义类型数据算出来的索引值相同怎么处理?
准备工作创建Person类
public class Person {
private int age ;
private float height;
private String name;
public Person(int age, float height, String name) {
super();
this.age = age;
this.height = height;
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public float getHeight() {
return height;
}
public void setHeight(float height) {
this.height = height;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
打印结果如下:为地址值
在工作中却不常使用地址值做哈希值。还记得我们之前计算哈希值的要求吗?尽量让key的所有信息参与运算,我们把Person作为key,就要尽量让Key的所有信息参与运算。于是乎我们采用下面的方法:还记得字符串怎么算哈希值的吗?public int hashCode() {
int hashCode = Integer.hashCode(age);
hashCode = hashCode * 31 + Float.hashCode(height);
hashCode = hashCode * 31 + (name != null ? name.hashCode():0);
return hashCode;
}
同样的31,同样的叠乘。和字符串算哈希值有异曲同工之妙吧。
然后我们再次打印p1和p2的哈希值:
p1和p2的哈希值相同了。
我们再上面讲链表的时候就说过,当几个同样类型的值算出来索引相同是如何处理的。比如p1,p2,p3(引用类型做Key)算出来的索引相同。我们用链表连接起来,每次在链表后面添加。但是添加的时候要查找Key在链表中是否存在。先加p1,比较p2和p1是否相等,不相等就在链表尾部加p2,比较p3和p1是否相等,不相等再比较p3和p2是否相等,不相等就在链表尾部添加p3。
问题来了,我们如何比较p1,p2,p3是否相等呢?
用compare?我们之前说过因为不需要数据可比较性我们才用hashmap的。所以不能用。
用“==”?这是比较地址,每个数据的地址肯定都不一样,这样比较没意义。
最终还是用equal方法。
在Person中我们重写equal方法完成内容的比较,逻辑都写在注释上了
public boolean equals(Object obj) {
//内存地址相同肯定相等
if (this == obj) {
return true;
}
//类型不一样肯定不相等
if (obj == null || obj.getClass() != getClass()) {
return false;
}
//下面开始比较成员变量
Person person = (Person) obj;
return person.age == age
&& person.height == height
&& person.name == null ? name == null:person.name.equals(name);
}