一、哈希表简介
哈希表(Hash table,也叫散列表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。
记录的存储位置=f(关键字)
这里的对应关系f称为散列函数,又称为哈希(Hash函数),采用散列技术将记录存储在一块连续的存储空间中,这块连续存储空间称为散列表或哈希表(Hash table)。
哈希表hashtable(key,value) 就是把Key通过一个固定的算法函数既所谓的哈希函数转换成一个整型数字,然后就将该数字对数组长度进行取余,取余结果就当作数组的下标,将value存储在以该数字为下标的数组空间里。(或者:把任意长度的输入(又叫做预映射, pre-image),通过散列算法,变换成固定长度的输出,该输出就是散列值。这种转换是一种压缩映射,也就是,散列值的空间通常远小于输入的空间,不同的输入可能会散列成相同的输出,而不可能从散列值来唯一的确定输入值。简单的说就是一种将任意长度的消息压缩到某一固定长度的消息摘要的函数。)
而当使用哈希表进行查询的时候,就是再次使用哈希函数将key转换为对应的数组下标,并定位到该空间获取value,如此一来,就可以充分利用到数组的定位性能进行数据定位。
数组的特点是:寻址容易,插入和删除困难;
而链表的特点是:寻址困难,插入和删除容易。
那么我们能不能综合两者的特性,做出一种寻址容易,插入删除也容易的数据结构?答案是肯定的,这就是我们要提起的哈希表,哈希表有多种不同的实现方法,我接下来解释的是最常用的一种方法——拉链法,我们可以理解为“链表的数组”,如图:
左边很明显是个数组,数组的每个成员包括一个指针,指向一个链表的头,当然这个链表可能为空,也可能元素很多。我们根据元素的一些特征把元素分配到不同的链表中去,也是根据这些特征,找到正确的链表,再从链表中找出这个元素。
二、解决Hash冲突的方式
1.线性探测法:
如果hash函数计算得出的存储位置一杯占用,那么将继续向后寻找空间的位置进行存放
缺点:hash一旦冲突,将会增加查询的时间复杂度,由原来的O(1)转化成O(n),hash冲出越多,效率越低
随着hash表越来越满,哈希冲的概率就越大,所以的哈希表都有一个衡量因子,即加载因子(loadfactor = 0.75)
公式: 以占有的hash位置 / 总的hash位置 > loadfactor O(1)
2.链地址法(拉链法):
把哈希冲突冲突的元素都放在一个链表中,但是链表的长度不能太长,越长效率越慢
时刻关注哈希表的loadfactor加载因子,如果查过及时进行扩容操作,扩容后原来表的hash数据,需要重新hash
如图:
三、代码实现
public class HashMap<K,V> {
/**
* hash表定义
*/
private ArrayList<LinkedList<Entry<K,V>>> mapList;
/**
* 加载因子 以占有的个数 / 总的个数
*/
private double loadfactor = 0.75;
/**
* 记录已使用的个数
*/
private int usedBuckets = 0;
/**
* 素数表
*/
private static int [] primeTable = {3,7,23,47,97,251,443,911};
/**
* 素数表的索引
*/
private int primeTableIndex = 0;
/**
* 哈希表初始化
*/
public HashMap() {
this(0.75);
}
/**
* 自定义加载因子
* @param loadfactor
*/
public HashMap(double loadfactor) {
this.mapList = new ArrayList<>(primeTable[primeTableIndex]);
for (int i = 0; i < primeTable[primeTableIndex]; i++) {
this.mapList.add(new LinkedList<Entry<K,V>>());
}
this.loadfactor = loadfactor;
}
/**
* 打包键值对
* @param <K> key
* @param <V> value
*/
static class Entry<K,V>{
K key;
V value;
public Entry(K key, V value) {
this.key = key;
this.value = value;
}
}
/**
* 增加一个键值对
* 不允许添加key : null的元素
* 不允许key重复
* key存在则覆盖,不存在则新增
* @param key
* @param value
*/
public void put(K key ,V value){
//1.扩充处理
//计算现在的长度
double lf = this.usedBuckets * 1.0 / this.mapList.size();
System.out.println("加载因子:"+ lf +"----"+"哈希表的长度:"+this.mapList.size());
//需要扩容
if(lf > this.loadfactor){
resize();
}
//2. 计算key的散列码
int index = key.hashCode() % mapList.size();
LinkedList<Entry<K, V>> list = mapList.get(index);
if(list.isEmpty()){
//index 位置为空,未被占用
list.add(new Entry<K,V>(key,value));
//位置被占用
this.usedBuckets ++;
}else {
for (Entry<K,V> entry:list) {
//key 存在,直接替换value
if(entry.key.equals(key)){
entry.value = value;
return;
}
}
//key 不存在,新增 key value
list.add(new Entry<K,V>(key,value));
}
}
/**
* hash 删除
* @param key
* @return
*/
public V remove(K key){
//1. 计算key的散列码
int index = key.hashCode() % mapList.size();
LinkedList<Entry<K, V>> list = mapList.get(index);
//2. 不存在
if(list.isEmpty()){
return null;
}
//3. 存在 删除
Iterator<Entry<K, V>> it = list.iterator();
while (it.hasNext()){
Entry<K, V> entry = it.next();
if(entry.key.equals(key)){
V value = entry.value;
it.remove();
return value;
}
}
return null;
}
/**
* hash 查询
* @param key
* @return
*/
public V get(K key){
//1. 计算key的散列码
int index = key.hashCode() % mapList.size();
LinkedList<Entry<K, V>> list = mapList.get(index);
//2. 不存在
if(list.isEmpty()){
return null;
}
//3. 存在 删除
Iterator<Entry<K, V>> it = list.iterator();
while (it.hasNext()){
Entry<K, V> entry = it.next();
if(entry.key.equals(key)){
V value = entry.value;
return value;
}
}
return null;
}
/**
* 哈希表的扩容操作 ---HashMap transfer转移节点的函数
*/
private void resize(){
//oldBucketsList 指向原来的 hash表
ArrayList<LinkedList<Entry<K,V>>> oldBucketsList = mapList;
//将个数归 0
this.usedBuckets = 0;
//已达到最大扩容数量
if(primeTableIndex == this.primeTable.length){
return;
}
//重新开辟容量
this.mapList = new ArrayList<>(primeTable[++primeTableIndex]);
for (int i = 0; i < primeTable[primeTableIndex]; i++) {
this.mapList.add(new LinkedList<Entry<K,V>>());
}
//遍历原来的数据,存储到新的哈希表中
for (LinkedList<Entry<K,V>> oldList:oldBucketsList ) {
if(oldList.size() >0 ){
//把key value 添加到新的hash表中
for(Entry<K,V> entry : oldList){
//2. 计算key的散列码
int index = entry.key.hashCode() % mapList.size();
LinkedList<Entry<K, V>> list = mapList.get(index);
if(list.isEmpty()){
//位置被占用
this.usedBuckets ++;
}
list.add(new Entry<K,V>(entry.key,entry.value));
}
}
}
}
四、效果演示
这里的HashMap是上面自定义的,请不要引用错
public static void main(String[] args) {
HashMap<Integer,String> map = new HashMap<>();
map.put(1,"aaaa");
map.put(2,"bbbb");
map.put(3,"cccc");
map.put(4,"dddd");
map.put(5,"eeee");
map.put(6,"eefee");
// System.out.println(map.get(1));
// System.out.println(map.get(2));
// System.out.println(map.get(3));
//
// map.put(1,"cccc");
// System.out.println(map.get(1));
//
// map.remove(2);
// System.out.println(map.get(2));
}
更多学习资料请看这里
SpringCloud学习代码: https://github.com/Dylan-haiji/javayh-cloud
Redis、Mongo、Rabbitmq、Kafka学习代码: https://github.com/Dylan-haiji/javayh-middleware
AlibabaCloud学习代码:https://github.com/Dylan-haiji/javayh-cloud-nacos