什么是 Hash
Hash(哈希),又称“散列”。
散列(hash)英文原意是“混杂”、“拼凑”、“重新表述”的意思。
在某种程度上,散列是与排序相反的一种操作,排序是将集合中的元素按照某种方式比如字典顺序排列在一起,而散列通过计算哈希值,打破元素之间原有的关系,使集合中的元素按照散列函数的分类进行排列。
在介绍一些集合时,我们总强调需要重写某个类的 equlas() 方法和 hashCode() 方法,确保唯一性。这里的 hashCode() 表示的是对当前对象的唯一标示。计算 hashCode 的过程就称作 哈希。
为什么要有 Hash
我们通常使用数组或者链表来存储元素,一旦存储的内容数量特别多,需要占用很大的空间,而且在查找某个元素是否存在的过程中,数组和链表都需要挨个循环比较,而通过 哈希 计算,可以大大减少比较次数。
举个栗子:
现在有 4 个数 {2,5,9,13},需要查找 13 是否存在。
1.使用数组存储,需要新建个数组 new int[]{2,5,9,13},然后需要写个循环遍历查找:
int[] numbers = new int[]{2,5,9,13};
for (int i = 0; i < numbers.length; i++) {
if (numbers[i] == 13){
System.out.println("find it!");
return;
}
}
这样需要遍历 4 次才能找到,时间复杂度为 O(n)。
2.而假如存储时先使用哈希函数进行计算,这里我随便用个函数:
H[key] = key % 3;
四个数 {2,5,9,13} 对应的哈希值为:
H[2] = 2 % 3 = 2;
H[5] = 5 % 3 = 2;
H[9] = 9 % 3 = 0;
H[13] = 13 % 3 = 1;
然后把它们存储到对应的位置。
当要查找 13 时,只要先使用哈希函数计算它的位置,然后去那个位置查看是否存在就好了,本例中只需查找一次,时间复杂度为 O(1)。
因此可以发现,哈希 其实是随机存储的一种优化,先进行分类,然后查找时按照这个对象的分类去找。
哈希通过一次计算大幅度缩小查找范围,自然比从全部数据里查找速度要快。
比如你和我一样是个剁手族买书狂,家里书一大堆,如果书存放时不分类直接摆到书架上(数组存储),找某本书时可能需要脑袋从左往右从上往下转好几圈才能发现;如果存放时按照类别分开放,技术书、小说、文学等等分开(按照某种哈希函数计算),找书时只要从它对应的分类里找,自然省事多了。
哈希函数
哈希的过程中需要使用哈希函数进行计算。
哈希函数是一种映射关系,根据数据的关键词 key ,通过一定的函数关系,计算出该元素存储位置的函数。
表示为:
address = H [key]
几种常见的哈希函数(散列函数)构造方法
直接定址法
取关键字或关键字的某个线性函数值为散列地址。
即 H(key) = key 或 H(key) = a*key + b,其中a和b为常数。
比如这里写图片描述
除留余数法
取关键字被某个不大于散列表长度 m 的数 p 求余,得到的作为散列地址。
即 H(key) = key % p, p < m。
比如这里写图片描述
构造哈希函数的方法很多,实际工作中要根据不同的情况选择合适的方法,总的原则是尽可能少的产生冲突。
通常考虑的因素有关键字的长度和分布情况、哈希值的范围等。
如:当关键字是整数类型时就可以用除留余数法;如果关键字是小数类型,选择随机数法会比较好。
哈希冲突的解决
选用哈希函数计算哈希值时,可能不同的 key 会得到相同的结果,一个地址怎么存放多个数据呢?这就是冲突。
常用的主要有两种方法解决冲突:
1.链接法(拉链法)
拉链法解决冲突的做法是:
将所有关键字为同1词的结点链接在同一个单链表中。
若选定的散列表长度为 m,则可将散列表定义为一个由 m 个头指针组成的指针数组 T[0…m-1] 。
凡是散列地址为 i 的结点,均插入到以 T[i] 为头指针的单链表中。
T 中各分量的初值均应为空指针。
在拉链法中,装填因子 α 可以大于 1,但一般均取 α ≤ 1。
2.开放定址法
用开放定址法解决冲突的做法是:
用开放定址法解决冲突的做法是:当冲突发生时,使用某种探测技术在散列表中形成一个探测序列。沿此序列逐个单元地查找,直到找到给定的关键字,或者碰到一个开放的地址(即该地址单元为空)为止(若要插入,在探查到开放的地址,则可将待插入的新结点存人该地址单元)。查找时探测到开放的地址则表明表中无待查的关键字,即查找失败。