一、Hash表
1、定义:Hash表是一种特殊的数组
2、Hash函数
(1)设计原则
(2)作用
(3)应用
(4)Hash冲突:
二、Hash函数的设计
1、解决Hash索引分布不均匀的方法-模一个素数
2、操作对象是整数
3、寻找最优的素数
4、浮点数在内存中的存储
5、浮点数在hash数组中的hash索引
6、字符串在Hash数组中的Hash索引
7、日期对象在Hash数组中的Hash索引(将日期对象转为整型进行处理)
8、复杂对象在Hash数组中的Hash索引(将复杂对象转为整型进行处理)
三、Java中的hashCode
4、实现原理
四、Java中的equals和hashCode
1、Java中的equals方法和hashCode方法
2、总结
五、Hash冲突的处理方法-链地址法
2、Hash冲突
3、链地址法
4、哈希表中的缩容与扩容
5、使用AVL树作为数组元素实现哈希表
六、哈希表的复杂度分析
七、总结
一、Hash表
1、定义:Hash表是一种特殊的数组
2、Hash函数
(1)设计原则
- 必须满足Hash索引大的一致性,即如果 a==b,则 Hash(a) = Hash(b)
(2)作用
将操作对象转换为数组中对应的索引
如:index = ch - 'a';
将字符串中的字符与Hash表中的索引进行一一对应
(3)应用
可将字符串、日期对象、浮点数、复杂对象转换为索引
(4)Hash冲突:
不同的操作对象通过Hash函数进行计算有可能得到相同的索引(Hash索引)
二、Hash函数的设计
“键”通过哈希函数得到的“索引”分布越均匀越好
1、解决Hash索引分布不均匀的方法-模一个素数
(1)选取4作为素数-分布不均匀
(2)选取7作为素数-分布均匀
2、操作对象是整数
(1)小范围正整数直接使用(正整数--->索引)
举例:统计0到120每个数字出现的次数,创建一个大小为121的int[]类型的数组,直接使用每个数字作为数组中的索引
(2)小范围负整数偏移后使用(将小范围负整数转换为小范围正整数)(偏移后数字-->索引)
举例:统计-100到100每个数字出现的次数,创建一个大小为201的int[]类型的数组,将每个数字加100后作为数组的索引
(3)大整数取模,模一个素数(大于1的自然数中,只有1和他本身两个因数的数)
举例:为身份证号123456546126688888创建索引
- 直接将身份证号作为索引需要在内存中创建一个很大的数组
- 直接将身份证号后四位作为索引,可能会出现索引重复
- 身份证号中的信息没有被完全利用
3、寻找最优的素数
根据下表进行选择,可得到最优素数,比如在2^5和2^6之间的数的最优素数是53
good hash table primes
4、浮点数在内存中的存储
(一)根据国际标准IEEE754,任意一个二进制浮点数V可以表示成如下形式
(1)(-1)^S*M*2^E
(2)(-1)S表示符号位,当S=0时,V是整数,当S=1时,V是负数
(3)M表示有效数字,且1<=M<2
(4)举例:
- 十进制的5.0对应的2进制是101.0,等价于1.01*2^2,与V的格式相对应,可得到S=0,M=1.01,E=2,标准写法为(-1)^0*1.01*2^2
- 十进制的-5.0对应的2进制是-101.0,等价于-1.01*2^2,与V的格式相对应,可得到S=1,M=1.01,E=2,标准写法为(-1)^0*1.01*2^2
(二)IEEE754规定
(1)对于32位的浮点数,最高大的一位是符号位S,接着的8位是指数位E,剩下的23位是有效数字M
(2)在计算机内部保存M时,默认这个数的第一位总是1,因此可以被舍去,只保存后面的部分,比如在保存1.01的时候只保存01,等到读取的时候,第一位的1加上去 ,这样做的目的是节省1位有效数字,以32位浮点数为例,留给M只有32位,将第一位的1舍去以后,等于可以保存24位有效数字
5、浮点数在hash数组中的hash索引
将浮点数在内存中的二进制存储表示为十进制整数,再将得到的十进制整数对素数取余得到数组的hash索引
举例:求3.2的hash索引
(1)32位的浮点数
(2)64位的浮点数
6、字符串在Hash数组中的Hash索引
以字符串“persist”为例
code = p*26^6 + e*26^5 + r*26^4 + s*26^3 + i*26^2 + s*26^1 + t
= p*B^6 + e*B^5 + r*B^4 + s*B^3 + i*B^2 + s*B^1 + t
B代表随机的整数,一般字符串都是小写字母时取26,字符串都是大写字母时取52
code是一个大整数,要对code进行取模
(1)hash(code) = (p*B^6 + e*B^5 + r*B^4 + s*B^3 + i*B^2 + s*B^1 + t)%M(M是素数,B^K,K越大,计算机越慢)
(2)hash(code) = ((p*B^5 + e*B^4 + r*B^3 + s*B^2 + i*B^1 + s) * B + t)%M
= (((p*B^4 + e*B^3 + r*B^2 + s*B^1 + i) * B + s) * B + t)%M
= ((((p*B^3 + e*B^2 + r*B^1 + s) * B + i)) * B + s) * B + t)%M
= (((((p*B^2 + e*B^1 + r) * B + s) * B + i)) * B + s) * B + t)%M
= ((((((p*B + e) * B + r) * B + s) * B + i)) * B + s) * B + t)%M(加快了计算机的速度,但如果字符串较长同时B较大时,计算机计算出来的整数计算机无法表示,可能会出现溢出)
(3)hash(code) = ((((((p%M*B + e) %M* B + r)%M * B + s)%M * B + i)) %M* B + s) %M* B + t)%M (对于整数a,b,M,存在公式 (a + b) % M = ( a%M + b%M)% M)
(4)代码实现:求字符串“persist”的hash索引
/**
* 获取字符串对象的Hash索引
* @param s
* @return
*/
public int stringHashIndex(String s){
int b = 26;// 取位权
int m = 97;// 取素数
int hashIndex = 0;// 字符串s的hash索引
for(int i = 0;i < s.length(); i++){
hashIndex = (hashIndex * b + s.charAt(i))%m;
}
return hashIndex;
}
public static void main(String[] args) {
System.out.println(new HashIndex().stringHashIndex("persist"));
}
7、日期对象在Hash数组中的Hash索引(将日期对象转为整型进行处理)
/**
* 获取程序运行时系统时间的Hash索引
* @return
*/
public int DateHashIndex(){
Calendar calendar = Calendar.getInstance();
int year = calendar.get(Calendar.YEAR) ;
int month = calendar.get(Calendar.MONTH) + 1;
int day = calendar.get(Calendar.DAY_OF_MONTH);
int hashIndex = (year + month + day) % 97;
return hashIndex;
}
public static void main(String[] args) {
System.out.println(new HashIndex().DateHashIndex());
}
8、复杂对象在Hash数组中的Hash索引(将复杂对象转为整型进行处理)
以学生类为例
public class Student {
private String name;
private char gender;
private int age;
private float weight;
public Student(String name, char gender, int age, float weight) {
this.name = name;
this.gender = gender;
this.age = age;
this.weight = weight;
}
/**
* 获取学生对象的Hash索引
* @return
*/
public int hashIndex() {
int hashIndex = 0;// Hash索引
int b = 26;// 位权
int m = 97;// 素数
// 姓名
for(int i = 0;i < this.name.length(); i++){
hashIndex = (hashIndex * b + this.name.charAt(i))%m;
}
// 性别
hashIndex = (hashIndex % m + this.gender % m)%m;
// 年龄
hashIndex = (hashIndex % m + this.age % m)%m;
// 体重
hashIndex = (hashIndex % m + Float.hashCode(this.weight) % m)%m;
return hashIndex;
}
public static void main(String[] args) {
Student student = new Student("小李", '男',27, 23.1f);
System.out.println(student.hashIndex());
}
}
三、Java中的hashCode
1、返回值为int类型,hashCode有正有负
2、hashCode是Java中对象的Hash索引,如果还是比较大,可以再模一个素数变为较小的Hash索引
3、举例:
public static void main(String[] args) {
// int类型的hashCode
int a = 10;
System.out.println(Integer.hashCode(a));
a = -10;
System.out.println(Integer.hashCode(a));
// double类型的hashCode
double gdp = 6598462581.23;
System.out.println(Double.hashCode(gdp));
// String类型的hashCode
String s = "comma";
System.out.println(s.hashCode());
}
4、实现原理
通过将对象的地址转换为一个整数,然后对该整数通过Hash函数算法就得到了该对象在Hash表中对应的Hash索引
四、Java中的equals和hashCode
1、Java中的equals方法和hashCode方法
(1)调用Object类中的equals方法是通过两个对象的地址进行比较来判断这两个对象是否是相等的
Student student1 = new Student("小李", '男',27, 23.1f);
Student student2 = new Student("小李", '男',27, 23.1f);
System.out.println(student1.equals(student2));
(2)重写Object类中的equals方法,使得equals方法是通过两个对象中的内容来判断这两个对象是否是相等的
注意:浮点数在内存中存储时会损失精度且不可直接用==比较是否相等
@Override
public boolean equals(Object obj) {
if(obj == null){// obj是空对象
return false;
}
if(this == obj){// 两个对象地址相同
return true;
}
if(obj instanceof Student){// 比较对象内容
if(this.name.equals(((Student) obj).name) && this.gender == ((Student) obj).gender
&& Math.abs(this.weight - ((Student) obj).weight)<=1.0E-10){
return true;
}else{
return false;
}
}else{
return false;// 不是同一类型的对象
}
}
Student student1 = new Student("小李", '男',27, 23.1f);
Student student2 = new Student("小李", '男',27, 23.1f);
System.out.println(student1.equals(student2));
(3)调用Object类中的hashCode方法,会使得相等对象的hashCode是不同的,违背了Hash函数的设计原则,即相等对象的Hash索引必须是相等的
Student student1 = new Student("小李", '男',27, 23.1f);
Student student2 = new Student("小李", '男',27, 23.1f);
System.out.println(student1.equals(student2));
System.out.println(student1.hashCode() == student2.hashCode());
(4)重写Object类中的hashCode方法,使得两个相等对象的hashCode是相等的
@Override
public int hashCode() {
int hashIndex = 0;// Hash索引
int b = 26;// 位权
int m = 97;// 素数
// 姓名
for(int i = 0;i < this.name.length(); i++){
hashIndex = (hashIndex * b + this.name.charAt(i))%m;
}
// 性别
hashIndex = (hashIndex % m + this.gender % m)%m;
// 年龄
hashIndex = (hashIndex % m + this.age % m)%m;
// 体重
hashIndex = (hashIndex % m + Float.hashCode(this.weight) % m)%m;
return hashIndex;
}
2、总结
(1)如果a.equals(b)返回true,那么a和b的hashCode()必须要相等
(2)如果a.equals(b)返回false,那么a和b的hashCode()有可能相等,也有可能不相等
(3)两个对象相等的必要条件是两个对象的hashCode()相等
(4)使用hashCode和equals来共同判断两个对象是否相等
五、Hash冲突的处理方法-链地址法
1、hashCode(k1)可能为负数,需要通过多按位与运算对hashCode(k1)进行处理,即hashCode(k1)&0x7fffffff,可将hashCode(k1)变为正数
举例:-10
public static void main(String[] args) {
int a = -10;
System.out.println(Integer.hashCode(a));// 求-10的hashCode 为-10
// 将-10的hashCode的-10变为10
System.out.println(Integer.hashCode(a) & 0x7fffffff);
/*
-10 : 原码:10000000 00000000 00000000 00001010
反码:11111111 11111111 11111111 11110101
补码:11111111 11111111 11111111 11110110
& 01111111 11111111 11111111 11111111
11111111 11111111 11111111 11110110
*/
}
2、Hash冲突
3、链地址法
在数组的每一个位置都存放一个链表,本质上就是一个查找表,查找表的实现也可以使用平衡树的结构
4、哈希表中的缩容与扩容
对于Java中哈希表的实现,在Java8之前,哈希表中的每个位置对应一个链表,从Java8开始,当哈希冲突达到8时,每个位置自动从链表转为红黑树。红黑树的链化阈值是6,即哈希表在进行扩容时,单个Node节点下的红黑树节点个数小于6时,会将红黑树转为链表(用实际元素数目除以Hash表的容量的结果与6进行比较)
HashMap原理详解(含面试题):https://zhuanlan.zhihu.com/p/127147909
5、使用AVL树作为数组元素实现哈希表
/**
* 哈希表
*/
public class MyHashTable <K extends Comparable<K>,V>{
private AVLTree<K,V>[] hash;// hash数组
private int size;// 实际元素数量
private int M;// 哈希表的容量
private final int[] capacity = {53,97,193,389,769,1543,3079,6151,12289,24593,49157,98317,196613,393241,786433,1572869};
private int capacityIndex;
private static final int upperTol = 8;// 缩容的阈值
private static final int lowerTol = 6;// 扩容的阈值
public MyHashTable(int capacityIndex){
this.capacityIndex = capacityIndex;
this.M = this.capacity[this.capacityIndex];// 模的素数
this.size = 0;
this.hash = new AVLTree[this.M];
// 初始化hash数组
for(int i = 0;i < this.M;i ++){
hash[i]= new AVLTree<>();
}
}
public MyHashTable(){
this(0);
}
/**
* 获取实际存放元素的数目
* @return
*/
public int getSize(){
return this.size;
}
/**
* 删除元素
* @param pair
*/
public void remove(Pair<K,V> pair){
// 1、获取pair对应的hash索引
int hashIndex = (pair.key.hashCode() & 0x7fffffff) % this.M;
// 2、从AVL树中删除pair
if(hash[hashIndex].find(hash[hashIndex].root,pair.key) == null){
System.out.println("找不到");
}
hash[hashIndex].root = hash[hashIndex].remove(hash[hashIndex].root, pair.key);
this.size--;
// 在删除元素后判断hash数组是否需要缩容
if(this.size/this.M <= this.lowerTol && this.capacityIndex >= 1){
System.out.println("执行缩容");
reSize(--this.capacityIndex);
}
}
/**
* 对hash数组扩容
* @param newCapacityIndex
*/
public void reSize(int newCapacityIndex){
System.out.println("新容积: "+ this.capacity[newCapacityIndex]);
// 保存旧的M
int oldM = this.M;
// 更新M
this.M = this.capacity[newCapacityIndex];
// 创建新的hash数组
AVLTree<K,V>[] newHash = new AVLTree[this.M];
// 初始化newHash
for(int i = 0;i < this.M;i ++){
newHash[i]= new AVLTree<>();
}
// 将原来hash数组中的数据全部存入新的hash数组中
for(int i =0;i < oldM;i++){
// 拿到AVL树
AVLTree<K,V> tmp = this.hash[i];
while (tmp.getSize()!=0){
// 获取节点值
Pair<K,V> pair = tmp.removeMinNode(this.hash[i].root);
// 计算节点的新hash索引
int newHashIndex = (pair.key.hashCode() & 0x7fffffff) % this.M;
// 将节点加到新的hash数组中
newHash[newHashIndex].root = newHash[newHashIndex].insert(newHash[newHashIndex].root,pair);
}
}
this.hash = newHash;
}
/**
* 添加元素
* @param pair
*/
public void add(Pair<K,V> pair){
// 在添加元素之前判断是否要对hash数组进行扩容
if(this.size/this.M >= this.upperTol){
System.out.println("执行扩容");
reSize(++this.capacityIndex);
}
// 1、获取pair对应的hash索引
int hashIndex = (pair.key.hashCode() & 0x7fffffff) % this.M;
// 2、将pair添加到hash表中对应的位置
hash[hashIndex].root = hash[hashIndex].insert(hash[hashIndex].root,pair);
// 3、更新size
this.size++;
}
public static void main(String[] args) {
MyHashTable<Integer,Integer> hashTable = new MyHashTable<>();
int[] nums = new int[10000];
for(int i = 0;i < 10000;i++){
nums[i] = i;
}
for(int i = 0;i < nums.length;i++){
hashTable.add(new Pair<>(nums[i],nums[i] + 1));
}
for(int i = 0;i < nums.length;i++){
hashTable.remove(new Pair<>(nums[i],nums[i] + 1));
}
}
}
六、哈希表的复杂度分析
假设哈希表有M个地址,当数据量达到upperTol*M是要对哈希表进行扩容操作,哈希表的时间复杂度为,则哈希表的均摊时间复杂度为