哈希表1--初识哈希表、哈希冲突的解决方案、哈希函数、各种类型的数据如何生成哈希值

一,前言

前说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的元素串联起来。

四,jdk1.8 解决哈希冲突的方法

  • 默认使用单向链表将元素串联起来。
  • 在添加元素时,可能会由单向链表转为红黑树来存储(当哈希表容量 >= 64 且单向链表的节点数大于8 时)。
  • 当红黑树的节点数量少到一定程度时,又会变成链表。
  • JDK1.8 中的哈希表是使用 链表 + 红黑树来解决哈希冲突的
    在这里插入图片描述
    思考:为什么用单向链表,而不用双向链表?
    双向链表的效率不是比单向的要高吗?为啥不用双向链表呢?
    1.单向链表每次插入都要遍历一遍链表。每次插入前的遍历其实并不多余。因为我们插入一个value都要找一下value在链表中是否存在。遍历正好可以起到查找的作用。
    2.单向链表比双向链表少一个指针的,可以节省空间。

五,哈希函数

  • 哈希表中哈希函数的实现步骤大概如下:
    1). 先生成Key的哈希值(整数)
    2). 再让Key的哈希值和数组的长度大小进行相关运算,生成一个索引。这个步骤的主要目的就是,让哈希值不要超过数组的长度。
    在这里插入图片描述但是取模运算符的效率低下。为了提高效率我们可以使用&来代替%(有前提条件:将数组长度设计为2的幂)
    在这里插入图片描述
    为什么要求数组长度为2的幂,而且还和数组长度减去1 来 & 运算呢?
    &运算一定不陌生吧,2^n - 1的二进制一定都是1。
    比 2的n次方 小的和 2的n次方减1 &运算就是他本身。
    在这里插入图片描述
    比 2的n次方 大的和 2的n次方减1 & 运算就只取小的部分。
    在这里插入图片描述
    用 & 代替 %,也可以控制索引小于数组长度,而且效率还比%要高。

  • 一个良好的哈希函数
    让哈希值更加均匀分布—>减少哈希冲突的次数—>提升哈希表的性能

六,如何生成Key的哈希值

  • Key的常见种类:整数,浮点数,字符串对象,自定义类型
  • 不同种类的Key,哈希值的生成方式不一样,但是目标是一致的。
    1).尽量让每个Key的哈希值唯一。
    2).尽量让Key的所有信息参与运算。

1.Key为Int

整数的哈希值就是他本身。
在这里插入图片描述

2.Key为Float

浮点数在内存中是以二进制存储的
将浮点数存储的二进制格式转化成整数
在这里插入图片描述

3.Key为Long

long类型就是整数啊?那他的哈希值不就是他本身了吗?
并不是的,java定义哈希值是int类型,int类型是4字节32位,而long是8字节64位的。那我们怎么办呢?有一种方法,我们只取long的前32位或者后32位,显然这种方案是不妥当的,因为Key只有一半的信息参与运算。
java官方是采用如下的方案:
在这里插入图片描述
这样运算的目的是让高32位和低32位混合运算出32位的哈希值。我们最后强转为int,那么只取最后的32位,也就是图中的橙色部分。
在这里插入图片描述
为什么用异或 ^运算而不用其他的(比如&和 |)
用&运算如果高位都是1,那就相当于没算。
用|运算,大部分情况结果都是1。算出来也没意义,冲突太多了。

4.Key为Double

也没什么好说的了,就是结合了一下浮点数和Long的处理步骤。代码如下
在这里插入图片描述

5.Key为String

  • 字符串“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 <length;i ++) {
			char c = string.charAt(i);
			hashCode = hashCode * 31 + c;
		}
		System.out.println(hashCode);
	}

以后用到就不需要自己写了,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());

结果如下:
在这里插入图片描述

6.Key为自定义类型

首先提出几个问题
1.自定义类型如何计算出哈希值
2.两个自定义类型数据算出来的索引值相同怎么处理?

(1)自定义类型如何计算出哈希值

准备工作创建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;
	}

}
  • 自定义类型如果没有重写hashcode方法,那么就将地址作为哈希值
    在这里插入图片描述

打印结果如下:为地址值

在这里插入图片描述

在工作中却不常使用地址值做哈希值。还记得我们之前计算哈希值的要求吗?尽量让key的所有信息参与运算,我们把Person作为key,就要尽量让Key的所有信息参与运算。于是乎我们采用下面的方法:还记得字符串怎么算哈希值的吗?
  • 重写hashcode方法
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的哈希值相同了。

(2)两个自定义类型数据算出来的索引值相同怎么处理?

我们再上面讲链表的时候就说过,当几个同样类型的值算出来索引相同是如何处理的。比如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);
	}
  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值