【概念】
哈希表是一种特殊的存储方法,通过“哈希函数”使元素的存储位置与它的关键码之间建立起一一映射的关系,在查找时可以通过该函数快速地找到该元素
//可以把所有函数比作一堆锁,目前想开一把锁,方法比作钥匙,其他方法的钥匙只能一把一把挨个试,而哈希方法的钥匙和锁之间有编号,想开几号锁只需要找到对应编号的钥匙即可开启
当向哈希表中:
插入元素
根据插入元素的关键码,以此函数计算出该元素的存储位置并按此位置进行存放
搜索元素
对元素的关键码进行同样的计算,把求得的函数值当作元素的存储位置,在结构中按此位置取元素比较,若关键码相等,则搜索成功
该方式叫“哈希方法”(散列方法),哈希方法中使用的转换函数为“哈希(散列)函数”,构建出的结构为“哈希表(散列表)”
【哈希函数】
//capacity为容量
【哈希冲突】
key1和key2关键字不同,使用了相同的哈希函数 key % 容量 ==> index,得到的index下标相同,这种现象为“哈希冲突”或者“哈希碰撞”
把具有不同关键码而具有相同哈希地址的数据元素称为“同义词”
【避免哈希冲突方法】
由于哈希表底层数组的容量小于实际要存储的关键字数量,因此冲突的发生是必然的,我们能做的是“尽量降低冲突率”
1.设计合理的哈希函数
哈希函数的设计原则是:
1、哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表允许有 m 个地址时,其值
域必须在 0 到 m-1 之间
2、哈希函数计算出来的地址能均匀分布在整个空间中
3、哈希函数应该比较简单
常见的哈希函数有直接定址法和除留余数法:
直接定址法:
取关键字的某个线性函数为散列地址: Hash ( Key ) = A*Key + B
其简单、均匀,但需要事先知道关键字的分布情况,适合查找比较小且连续的情况
除留余数法:
设散列表中允许的 地址数为 m ,取一个不大于 m ,但最接近或者等于 m 的质数 p 作为除数,
按照哈希函数: Hash(key) = key% p(p<=m), 将关键码转换成哈希地址
2.较低的载荷因子
散列表越长。载荷因子越低,越小的载荷因子代表着越低的冲突率,当载荷因子为0.75时,就需要扩容了
【解决哈希冲突方法】
1.闭散列(开放定址法)
当发生哈希冲突时,如果哈希表未被装满,那么说明在哈希表中必然还有空位置,那么可以把key存放到冲突位置中的“下一个” 空位置中去,下面将介绍寻找下一个空位置的方法
1.1:线性探测
从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止。
举一个例子,上厕所时,在还有空位的情况下,第一个选择的坑位已经有人了,这个时候肯定不会站在门口等着,而是会选择旁边的位置。
1.2:二次探测
公式:
H0:冲突的位置 i:冲突的次数(从1开始) m:容量
从发生冲突的位置开始,以一定规律的变化向后依次探测空位
在决定好寻找下一个空位置的方法后,设定好比例,当空位已经不足一个比例时(比如已经有十分之七的位置被使用),这样哈希冲突产生的概率就会大大提高,因此就需要对其进行扩容操作,随后把原本的值依照新的存储空间大小来进行新的操作
2.开散列(开链法)
首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中
从上图中可以看出,开散列中每个桶中放的都是发生哈希冲突的元素
【哈希表的实现】
package BinaryTree;
public class HashBuck
{
static class Node
{
public int key;
public int val;
public Node next;
public Node(int key, int val)
{
this.key = key;
this.val = val;
}
}
public Node[] array;//里面存放的元素
public int usedSize;//里面存放的元素个数
public double loadFactor = 0.75;//负载因子
//进行HashBuck的构造方法
public HashBuck()
{
array = new Node[10];
}
public void put(int key, int val) {
int index = key % array.length;放元素,首先需要找元素的位置,index位置 = 当前key % 整个数组的长度
Node cur = array[index];//然后要采取头插法(头插尾插都可以,这里以头插法为例)
//1.遍历链表是否存在当前值
while (cur != null) {
if (cur.key == key) {
cur.val = val;//如果key相同,就更新val
return;
}
cur = cur.next;//如果不相同,cur继续往后走
}
//2.走到这里说明没有当前值,此时进行头插
Node node = new Node(key, val);//定义一个要插入的节点node
node.next = array[index];//将node的next域指向头节点array[index]
array[index] = node;//头节点array[index]指向node,达成头插
usedSize++;//里面存放的元素个数增加
//3.判断负载因子
if (loadFactorCount() >= 0.75) // 负载因子大于0.75,说明要进行扩容了,扩容代表着遍历每个数组元素下的链表的每个元素,每个元素都需要进行重新哈希
{
resize();
}
}
public void resize() // 扩容方法
{
Node[] newArray = new Node[array.length * 2];//扩容成原来的2倍
for(int i = 0;i < array.length;i++)
{
Node cur = array[i];
//开始遍历链表
while(cur != null)
{
int newIndex = cur.key % newArray.length;//重新哈希,把数据存放在新的数组的newIndex位置
Node curN = cur.next;
cur.next = newArray[newIndex];//把cur指向的头节点地址放到newArray的newIndex中
newArray[newIndex] = cur;//newArray的newIndex指向了cur
cur = curN;
}
}
array = newArray;//array指向了新的数组
}
public double loadFactorCount()//计算负载因子
{
return usedSize * 1.0 / array.length; // 填入表中的元素个数 / 哈希表长度
}
public int get(int key)
{
int index = key % array.length;
Node cur = array[index];
while (cur != null) {
if (cur.key == key) {
return cur.val; //如果key相同,直接返回key的value
}
cur = cur.next;//如果不相同,cur继续往后走
}
return -1;//没找到,返回-1
}
}
【自定义类型的比较】
哈希桶内部自带比较方法,但这里Student是一个自定义类型,没办法比较,我们需要把一个引用对象转变为整数,这个转变整数的方法叫“hashCode”
package BinaryTree;
class Student
{
public String stuN;
public Student(String stuN)
{
this.stuN = stuN;
}
}
public class Test {
public static void main(String[] args)
{
Student student1 = new Student("240115");
Student student2 = new Student("240115");
int hashCode1 = student1.hashCode();
int hashCode2 = student2.hashCode();
System.out.println(hashCode1);
System.out.println(hashCode2);
}
}
如果认为这两个是同一个学生,那么key % array.length得到的整数index应该是一样的整数,但实际上并非如此
这意味着,虽然student1和student2学号相同,但并不被认为是同一个学生,这样一来我们就需要重写hashCode方法
通过idea编译器右键——Generate——equals() and hashCode()来快速生成
package BinaryTree;
import java.util.Objects;
class Student
{
public String stuN;
public Student(String stuN)
{
this.stuN = stuN;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Student student = (Student) o;
return Objects.equals(stuN, student.stuN);
}
@Override
public int hashCode() {
return Objects.hash(stuN);
}
}
public class Test {
public static void main(String[] args)
{
Student student1 = new Student("240115");
Student student2 = new Student("240115");
int hashCode1 = student1.hashCode();
int hashCode2 = student2.hashCode();
System.out.println(hashCode1);
System.out.println(hashCode2);
}
}
这样一来在运行后,两个输出值就一样了
因此,自定义类型比较时一定要重写equals方法和hashCode方法