目录
1、尾插法实现哈希表
JDK1.8之后源码中的HashMap都变成通过尾插法添加元素的,我们如何使用尾插法实现一个哈希表呢?
2、概念
要实现哈希表我们就要知道哈希表的几个个概念
1、哈希表的存储和取出
哈希表是通过哈希函数计算key值得出的哈希值,存入相应的地址中。
2、哈希冲突
当我们通过哈希函数去计算哈希值时往往会出现,不同的key出现相同的哈希值的情况。例如,哈希函数为hash=key%capacity(哈希表的长度)。那么当我们的哈希表长度为10,key为2,12,22时得到的hash都是2。这就是哈希冲突,也叫哈希碰撞。
3、如何避免哈希冲突
1、设计合理的哈希函数
哈希函数需要满足使所有的key得到的哈希值尽可能地线性分布。当然,哈希函数还需要尽可能的简单。
2、降低负载因子。
负载因子 Load Factor – 负载因子是决定何时增加Map容量的度量。 默认负载系数为容量的 75%。 临界点 threshold – 临界点是当前容量和负载因子的乘积。
也就是说当我们的负载因子达到0.75时我们就应该去扩大哈希表的长度。因为当我们的负载因子增大到一定程度时冲突率就会指数状上升。
4、 解决冲突
哈希冲突是客观存在的,是不可避免的。我们需要想办法解决哈希冲突。一般解决哈希冲突的方法有两种:
1、闭散列
也被叫做开放地址法,也就是在发生哈希冲突时把该元素放在其他为空的位置。选取这个空位置我们一般有两种方法:
1、线性探测法
也即是放在该地址(冲突发生的地址)的下一位空位置。
例如:将2 12 22 32 存入一张使用线性探测法的闭散列。结果就是这样
这样很容易导致大量元素堆积在一个位置。为了优化我们还有下面的方式
2、二次探测法
找下一个空位置的方法为:新的偏移量 = (冲突偏移量 +i^2 )% m, 或者 新的偏移量 = (冲突偏移量 -i^2 )% m。其中:i = 1,2,3…,
2、开散列
又叫做哈希桶,通过把哈希表上的每一个节点都制成一个链表,实现在发生哈希冲突时把哈希地址相同的元素添加到链表上。
我们的集合类HashMap的底层就是实现的哈希桶。
5、hashcode()和equals()方法
我们在实际使用哈希表的时候是通过泛型来规定key和val的类型的,那么我们就不能只依靠这么一个,简单的哈希函数(如下),来计算我们的哈希地址,因为如果我们的key是String或者是其他自定义类型的时候我们是不能通过key%capacity来计算出我们的哈希地址。
hash=key%capacity(哈希表的长度)
这时我们就要借助hashcode()方法计算。也就是hash=key.hashcode()%capacity。所以我们在自定义类型中要重写hashcode()方法。当我们使用int来实现哈希表时添加元素的方法一般这样写
//不用泛型的写法演示
public void put(int key,int val){
//1、找到key的位置
int index=key%this.elem.length;
//2、判断index位置是否已经有key。
Node cur=this.elem[index];
while(cur!=null){
if (cur.Key==key){
cur.val=val;
return;
}
cur=cur.next;
}
//3、尾插法
Node node=new Node(key, val);
if (this.elem[index]==null){
this.elem[index]=node;
}else{
cur=this.elem[index];
while (cur.next!=null){
cur=cur.next;
}
cur.next=node;
}
this.useSize++;
//4、插入元素成功之后,检查当前散列表的负载因子
//如果负载因子>=0.75时我们要进行扩容
if(loadFactor()>=DEFAULT_LOAD_FACTOR){
resize();
}
}
我们在添加元素时要判断是否已经有key元素在哈希表中,如果有我们需要将新的val值去覆盖掉原来的val,但是在使用自定义类型时我们无法在使用==来判断key是否重复。这时我们就应该去重写equals()方法来进行判断。
所以,当我们在使用HashMap存放自定义类型时,我们在自定义类型中就要重写hashcode()和equals()方法。当然当我们使用包装类时,包装类中已经为我们重写好了这两个方法。
3、实现
全部代码如下:
package HashBuck;
import java.util.Arrays;
/**
* Created with IntelliJ IDEA.
* Description:
* User: 东莞呵呵
* Date:2022-07-03
* Time:17:21
*/
public class HashBuck <K,V>{
class Node<K,V>{
public K key;
public V val;
public Node<K,V> next;
public Node(K key, V val) {
this.key = key;
this.val = val;
}
@Override
public String toString() {
return "Node{" +
"key=" + key +
", val=" + val +
'}';
}
}
public Node<K,V>[] elem;
public int useSize;
public static final double DEFAULT_LOAD_FACTOR=0.75;
public HashBuck() {
this.elem = new Node[10];
this.useSize = 0;
}
public void put(K key,V val){
//1、找到key的位置
int index=key.hashCode()%this.elem.length;
//2、判断index位置是否已经有元素。
Node<K, V> cur=this.elem[index];
while(cur!=null){
if (key.equals(cur.key)){
cur.val=val;
return;
}
cur=cur.next;
}
//3、尾插法
Node<K,V> node=new Node<K,V>(key, val);
if (this.elem[index]==null){
this.elem[index]=node;
}else{
cur=this.elem[index];
while (cur.next!=null){
cur=cur.next;
}
cur.next=node;
}
this.useSize++;
//4、插入元素成功之后,检查当前散列表的负载因子
if(loadFactor()>=DEFAULT_LOAD_FACTOR){
resize();
}
}
//扩容之后每个元素都要重新哈希
private void resize(){
Node<K,V>[] newElem=new Node[this.elem.length*2];
for (int i = 0; i < this.elem.length; i++) {
Node<K,V> cur=this.elem[i];
while(cur!=null){
int index= cur.key.hashCode()%this.elem.length;
Node<K,V> newCur=newElem[index];
if(newCur==null){
newElem[index]=cur;
cur=cur.next;
this.elem[i]=cur;
newElem[index].next=null;
}else {
while(newCur.next!=null){
newCur=newCur.next;
}
newCur.next=cur;
cur=cur.next;
this.elem[i]=cur;
newCur.next.next=null;
}
}
}
this.elem=newElem;
}
private double loadFactor(){
return 1.0*this.useSize/this.elem.length;
}
public V get(K key){
int hash = key.hashCode();
int index = hash % elem.length;
Node<K,V> cur = elem[index];
while (cur != null) {
if(cur.key.equals(key)) {
//更新val值
return cur.val;
}
cur = cur.next;
}
return null;
}
@Override
public String toString() {
return "HashBuck{" +
"elem=" + Arrays.toString(elem) +
", useSize=" + useSize +
'}';
}
}
我们有几点需要注意:
1、扩容
当我们的添加的元素过多,也就是负载因子达到0.75时我们就需要对哈希表进行扩容。扩容时我们的数组长度发生了改变,
int index=key.hashCode()%this.elem.length;
也就是每个元素的index都有可能发生了改变,我们需要对整张表进行重新哈希。
//扩容之后每个元素都要重新哈希
private void resize(){
Node<K,V>[] newElem=new Node[this.elem.length*2];
for (int i = 0; i < this.elem.length; i++) {
Node<K,V> cur=this.elem[i];
while(cur!=null){
int index= cur.key.hashCode()%this.elem.length;
Node<K,V> newCur=newElem[index];
if(newCur==null){
newElem[index]=cur;
cur=cur.next;
this.elem[i]=cur;
newElem[index].next=null;
}else {
while(newCur.next!=null){
newCur=newCur.next;
}
newCur.next=cur;
cur=cur.next;
this.elem[i]=cur;
newCur.next.next=null;
}
}
}
this.elem=newElem;
}
2、尾插法
这个相信大家也没有什么问题,在数据结构阶段一般链表的尾插法是最初讲的内容。需要注意的是我们要先判断当前链表是否为空,如果为空就直接添加新节点,如果不为空则遍历链表到最后,再添加新节点。
//3、尾插法
Node<K,V> node=new Node<K,V>(key, val);
if (this.elem[index]==null){
this.elem[index]=node;
}else{
cur=this.elem[index];
while (cur.next!=null){
cur=cur.next;
}
cur.next=node;
}
this.useSize++;