哈希表(Hash Table)是一种基于哈希函数实现的数据结构,用于存储键值对(Key-Value Pair)。它通过哈希函数将键(Key)映射到一个特定的存储位置,从而实现快速的数据查找、插入和删除操作。
HashMap的操作时间复杂度O(1),相比于TreeMap(操作时间复杂度O(logN))我们会更加偏向使用HashMap。
哈希函数设置为:hash(key) = key % capacity; capacity为存储元素底层空间总的大小。
但是如果再向集合中存储元素44,那么此时应该怎么存储?
对于两个数据元素的关键字 和 (i != j),有 != ,但有:Hash( ) == Hash( ),即:不同关键字通过相同哈希哈数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞。根据上述,我们发现再向集合中存储元素大小为44时,此时数组小标为4 的位置已经有元素,我们将这个就叫做冲突。
首先,我们明确,哈希存储的底层数组大小往往小于实际存储数据的大小,这就导致一个问题,冲突的发生是必然的,但我们能做的应该是尽量的避免冲突。
引起哈希冲突的一个原因可能是:哈希函数设计不够合理。 哈希函数设计原则:
概念:负载因子(Load Factor)是哈希表(如HashMap、Hashtable等)的一个重要参数,用于控制哈希表的扩容时机,从而在性能和内存使用之间取得平衡。
定义:负载因子是哈希表中元素数量与哈希表容量(桶的数量)之间的比例关系,计算公式为:
负载因子=当前元素个数 / 哈希表容量
负载因子和冲突率的关系粗略演示:
这里发现负载因子越大冲突率就越大,所以这里我们需要降低负载因子的大小,从而使冲突率降低。
负载因子是由当前元素个数和哈希表容量控制的,但我们不能控制当前元素个数,所以我们一般控制哈希表容量。在java系统库中,我们规定负载因子大小为0.75,当负载因子超过0.75,我们就会扩大哈希表容量。
解决哈希冲突的两种方法:开散列和闭散列
闭散列:也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把key存放到冲突位置中的“下一个” 空位置中去。
- 标记状态:
- EMPTY:表示该位置为空,可以插入新元素。
- EXIST:表示该位置有元素。
- DELETE:表示该位置的元素已被逻辑删除。
- 插入操作:
- 计算哈希值,找到初始位置。
- 如果该位置为空(EMPTY)或已被删除(DELETE),则可以插入新元素。
- 如果该位置已被占用(EXIST),则通过线性探测找到下一个空位置
- 查找操作:
- 计算哈希值,从初始位置开始查找。
- 如果遇到标记为EXIST的元素,比较键值是否匹配。
- 如果遇到标记为DELETE的位置,继续向后查找,直到遇到EMPTY为止。
- 删除操作:
- 计算哈希值,找到目标元素。
- 将目标元素的状态标记为DELETE,而不是直接物理删除。
开散列法:又叫链地址法(开链法),首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中。
static class Node{
int key;
int value;
Node next;
public Node(int key, int value){
this.key = key;
this.value = value;
}
}
public Node[] array=new Node[10];
public int useSized;
private static final double LOAD_FACTOR=0.75;
public void put(int key, int value){
//1.先获取插入元素的下标
int index=key% array.length;
Node cur=array[index];
//2.判断集合中是否已经包含key
while(cur!=null){
if(cur.key==key){
cur.value=value;
}
cur=cur.next;
}
//3.插入key
Node newNode=new Node(key,value);
newNode.next=array[index];
array[index]=newNode;
useSized++;
//4.判断是否需要扩容
if(Do_LOAD_FACTOR()>=LOAD_FACTOR){
resize();
}
}
private void resize() {
//5.因为数组容量扩大,所以需要重新遍历一遍原来的数组
Node[] newArray=new Node[array.length*2];
for(int i=0;i<array.length;i++){
Node cur=array[i];
while(cur!=null){
Node curN=cur.next;
int index=cur.key% newArray.length;
cur.next=newArray[index];
newArray[index]=cur;
cur=curN;
}
}
array=newArray;
}
private double Do_LOAD_FACTOR(){
return useSized*1.0 /array.length;
}
public int getValue(int key){
int index=key% array.length;
Node cur=array[index];
while(cur!=null){
if(cur.key==key){
return cur.value;
}
cur=cur.next;
}
return -1;
}
class Person{
String name;
public Person(String name){
this.name = name;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person preson = (Person) o;
return Objects.equals(name, preson.name);
}
@Override
public int hashCode() {
return Objects.hashCode(name);
}
}
public class RTHashbucket<K,V> {
static class Node<K,V>{
K key;
V value;
Node<K,V> next;
public Node(K key, V value){
this.key = key;
this.value = value;
}
}
public Node<K,V>[] array=(Node<K, V>[])new Node[10];
public int useSized;
private static final double LOAD_FACTOR=0.75;
public void put(K key, V value){
int hash = key.hashCode();
int index = hash % array.length;
Node<K,V> cur=array[index];
while(cur!=null){
if(cur.key.equals(key)){
cur.value=value;
}
cur=cur.next;
}
Node<K,V> newNode=new Node<>(key, value);
newNode.next=array[index];
array[index]=newNode;
useSized++;
if(Do_LOAD_FACTOR()>=LOAD_FACTOR){
resize();
}
}
private void resize() {
Node<K,V>[] newArray=(Node<K, V>[]) new Node[2* array.length];
for(int i=0;i<array.length;i++){
Node<K,V> cur=array[i];
while(cur!=null){
Node<K,V> curN=cur.next;
int hash = cur.key.hashCode();
int index = hash % newArray.length;
cur.next=newArray[index];
newArray[index]=cur;
cur=curN;
}
}
array=newArray;
}
private double Do_LOAD_FACTOR() {
return useSized*1.0/array.length;
}
public V getValue(K key){
int hash = key.hashCode();
int index = hash % array.length;
Node<K,V> cur=array[index];
while(cur!=null){
if(cur.key==key){
return cur.value;
}
cur=cur.next;
}
return null;
}
}
在引用类型的哈希桶代码实现,有个非常重要的方法:hashCode()方法。
作用:
- hashCode()方法是将对象的内存地址或其他特征转换为一个整数值,从而让我们得到数组下标。
- 对象的比较:如果两个对象的哈希码不同,那么它们一定不相等;如果哈希码相同,还需要进一步调用 equals() 方法来确认对象是否相等。
注意:如果要用自定义类作为 HashMap 的 key 或者 HashSet 的值,必须覆写 hashCode 和 equals 方法,而且要做到 equals 相等的对象,hashCode 一定是一致的。
此篇博客的全部代码!!!