哈希表是一个数组
哈希函数将任意类型一个键转换为数组中的索引,一般来说键和索引应为一一对应的关系,但是现实过程中很难达到这种关系所以就会产生哈希冲突,哈希冲突是指不同的键通过相同的哈希函数最终转换成了相同的索引,为了解决哈希冲突这一问题我们只能将产生的索引尽可能地分配均匀。
一般的小范围的整数,例如0-99由于其长度不是特别的长所以我们可以直接分配一个长度为100的数组,每一个数字就相当于数组中的索引我们可以直接使用而不做任何的操纵。
int[] ints = new int[100];*//* *直接创建一个长度为**100**的数组*
如果为小范围的负整数,例如-50~49同上面一样由于数字的范围并不是特别的大我们也可以直接分配一个长度为100的数组,计算索引时我们只需要将数字整体向右移即可,就比如说-50所对应的索引应为0,-49所对应的索引应为1。
int[] ints = new int[100];// 直接创建一个长度为100的数组
int num = -50, num1 = -49;
int hashCode = num + 50;
hashCode = num1 + 50;
如果为大范围的整数我们就不能直接分配相同长度的数组直接使用了,我们需要对大范围的整数进行取余操作这样得到的余数相对直接分配来说能小很多。例如0255我们可以通过对97进行取余的操做进而将计算出来的哈希值控制在096之间,并且得到的哈希值也算是较为均匀的分布在0~96这个范围中而不会存在有太多的数字经过哈希函数的计算得到相同的结果。
但是对那个数字进行取余操作也是有讲究的,例如10、20、30、40、50这几个数字由于其不是连续的索引我们没有必要创建一个长度为41的数组作为哈希表,我们就可以对这几个数字进行取余操作,例如我们选择4来取余那么经过计算我们会发现这五个数字进行取余操作后算出来的索引分配是极不均匀的,索引只会在0和2之间徘徊。
10 % 4 = 2
20 % 4 = 0
30 % 4 = 2
40 % 4 = 0
50 % 4 = 2
为了解决这一问题我们一般都是采用对一个素数进行取余操作,这种方法得到的结果是比较均匀的。
10 % 7 = 3
20 % 7 = 6
30 % 7 = 2
40 % 7 = 4
50 % 7 = 1
还有就是对字符串进行哈希值的计算,例如"code"我们可以类比一下对数字进行的操作,拿1234来说
1234 = 1 * 10 ^ 3 + 2 * 10 ^ 2 + 3 * 10 ^ 1 + 4 * 10 ^ 0
这里的10叫做权重我们可以用权重计算出数字的大小并对数字进行取余操作得到较为适合的哈希值,同样的字符串也可以使用这种方法。假如字符串"code"中只存在小写字母,那么我们就可以将其的权重看作是26,通过和上面相同的方法得到一个整数。
“code” = c * 26 ^ 3 + o * 26 ^ 2 + d * 26 ^ 1 + e * 26 ^ 0
经过对这种方法计算出来的整数进行取余操作就可以计算出字符串的哈希值了。
当然这里的权重也是根据每个人的喜好进行选择的,所以我们可以把权重看成一个变量B,把取余的数字看成M于是就有了下面这个式子。
int B, M;// B任意 M一般为素数
int hashCode = (c * B ^ 3 + o * B ^ 2 + d * B ^ 1 + e * B ^ 0) % M;
再将B提取出来得到下面的式子。
int hashCode = (((c * B + o) * B + d) * B + e) % M;
由于((2 * 5) + 4) % 7 = ((2 % 7 * 5) + 4) % 7 = 0
所以再对上面的式子进行转换。
int hashCode = (((c % M * B + o) % M * B + d) % M * B + e) % M;
这样便得到了一个对字符串求哈希值操作的公式。
int B, M;// B任意 M一般为素数
String s = "code";
int hashCode = 0;
for (int i = 0; i < s.length(); i++) {
hashCode = (hashCode * B + s.charAt(i)) % M;
}
对混合类型进行哈希值的计算例如日期类型。
Date date = new Date();
int year = date.getYear();
int month = date.getMonth();
int day = date.getDate();
由于通过Date获取到的年月日都为整数类型,于是我们可以将year、month、day的值相加然后再进行取余操作就可以直接计算出其哈希值了。
int hashCode = ((year % M * B + month) % M * B + day) % M;
对对象进行哈希值的计算。
public class Student {
String name;
int age;
String gender;
public Student(String name, int age, String gender) {
this.name = name;
this.age = age;
this.gender = gender;
}
public int getHashCode() {
int B;
int hashCode = 0;
hashCode = hashCode * B + name.hashCode();
hashCode = hashCode * B + age;
hashCode = hashCode * B + gender.hashCode();
return hashCode;
}
public static void main(String[] args) {
Student student = new Student("张三", 19, "男");
System.out.println(student.getHashCode() % 37);
}
}
我们可以将对象中的每一个属性转换为整数类型再相加最后进行取余操作得到对象的哈希值。这里为了方便字符串类型转换为整数类型我们是直接通过对象中的hashCode方法获取的,但是对象中的hashCode方法有时也是不能直接使用的,例如负数整数类型的hashCode方法获取到的哈希值是负数,这样当我们进行相加操作的时候有可能算出来的哈希值是为负数的,这将不利于我们在数组中进行存储,因为数组中的下标都是从0开始的不存在负数的下标。
获取对象的哈希值也可以使用对象中的hashCode方法。
Student student = new Student("张三", 19, "男");
System.out.println(student.hashCode());
还有一种解决哈希冲突的办法:链地址法
在计算哈希值时一定会产生哈希冲突这是无法避免的,但是当产生了哈希冲突时我们不再使用数组来存放元素,而是将数组转换为链表的组合。原来数组的一个下标只能对应一个元素,现在将一个元素变为一个结点当出现哈希值相同的情况时,只需要将元素连接到对应索引所在的结点后面即可,这样也可以避免哈希冲突问题。