概念:
理想的搜索方法:可以不经过任何比较,一次直接从表中得到要搜索的元素。 如果构造一种存储结构,通过某种函 数(hashFunc)使元素的存储位置与它的关键码之间能够建立一一映射的关系,那么在查找时通过该函数可以很快找到该元素。
当向该结构中:
*插入元素 :根据待插入元素的关键码,以此函数计算出该元素的存储位置并按此位置进行存放。
*搜索元素 :
对元素的关键码进行同样的计算把求得的函数值当做元素的存储位置,在结构中按此位置取元素比较,若 关键码相等,则搜索成功
该方式即为哈希(散列)方法哈希方法中使用的转换函数称为哈希(散列)函数,构造出来的结构称为哈希表(HashTable)(或者称散列表),
它的增删查改的时间复杂度都是O(1)
哈希冲突/碰撞:
概念:
对于两个数据元素的关键字Ki 和Kj (i != j),有Ki != Kj,但有:Hash( Ki) == Hash( Kj),即:不同的关键字,通过相同的哈希函数计算,得到了相同的哈希地址,这种现象称为哈希碰撞或哈希冲突。
避免:
首先,我们需要明确一点,由于我们哈希表底层数组的容量往往是小于实际要存储的关键字的数量的,这就导致一 个问题,
冲突的发生是必然的
,但我们能做的应该是尽量的
降低冲突率:
1.设计合理的哈希函数
常见的哈希函数
(1)
直接定制法:
取关键字的某个线性函数为散列地址:Hash(Key)= A*Key + B 优点:简单、均匀 缺点:需要事先知道关 键字的分布情况。使用场景:适合查找比较小且连续的情况
(2)
除留余数法:
设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,按照哈希函数: Hash(key) = key% p(p<=m),将关键码转换成哈希地址
2.负载因子调节
负载因子:α=填入表的元素个数/散列表的长度(冲突率和负载因子成正比,要想降低冲突率,就要减小负载因子,就要增加散列表的长度->扩容)
一般java的负载因子不超过0.75
解决:
1.闭散列
闭散列:也叫开放地址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以 把key
存放到冲突位置中的
“
下一个
”
空位置中去。那如何确定下一个位置呢?
(1)线性探测:
从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止。缺点是:产生冲突的数据堆积在一块
采用闭散列处理哈希冲突时,不能随便物理删除哈希表中已有的元素,若直接删除元素会影响其他 元素的搜索。 因此线性探测采用标 记的伪删除法来删除一个元素。
(2)二次探测:
找下一个空位置的方法为:Hi=(H0+i^2)%m,或者Hi=(H0-i^2)%m,其中i=1,2,3...,H0
是通过散列函数Hash(x)对元素的关键码 key 进行计算得到的位置, m是表的大小
最大的缺陷是:空间的利用率较低
2.开散列/哈希桶(java的HashMap中解决哈希冲突的方法)
开散列法又叫链地址法
(
开链法
)
,首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码归于同一子 集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中。
数组+链表+红黑树,这种处理哈希冲突的结果,就是java当中HashMap的结构
哈希表扩容需要注意什么?重新哈希。数组容量变大(len变了),如果再通过key%len来计算,那么老的节点,就有可能找不到了重新哈希的逻辑:需要遍历原来哈希表的每一个桶的链表,每个节点的key都需要重新计算
package a23;
//哈希桶
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;
}
}
private Node[] array=new Node[10];//哈希桶有Node类型的数组
private int usedSize;
private static final double DEFAULT_LOAD_FACTOR=0.75;//规定负载因子为0.75
public void push(int key,int val){
Node node=new Node(key,val);
//第一步:先找到要放的位置
int index=key%this.array.length;
//用cur遍历array[index]这个链表有没有相同的key
Node cur=array[index];
while(cur!=null){
if(cur.key==key){
cur.val=val;//找到相同key时,修改当前key的val
return;
}
cur=cur.next;
}
//cur=null,这个链表中没有找到相同的key,采用头插或尾插
node.next=array[index];
array[index]=node;
this.usedSize++;
//每放完一个元素都要计算负载因子,若大于规定的负载因子,则扩容
if(loadFactor()>=DEFAULT_LOAD_FACTOR){
resize();
}
}
//计算负载因子
private double loadFactor(){
return this.usedSize*1.0/this.array.length;
}
//扩容 重哈希
private void resize(){
Node[] newArray=new Node[2*array.length];
for(int i=0;i<array.length;i++){
Node cur=array[i];
//cur不为空时,遍历cur下的链表
while (cur!=null){
int index=cur.key%newArray.length;//将旧的节点放入新数组中
Node curNext=cur.next;
cur.next=newArray[index];//头插
newArray[index]=cur;
cur=curNext;
}
}
this.array=newArray;
}
public int get(int key){
int index=key%this.array.length;
Node cur=array[index];
while(cur!=null){
if(cur.key==key){
return cur.val;
}
cur=cur.next;
}
return -1;
}
}
若key不是整数时,可以用hashcode,用hashCode()时必须重写equals和hashode方法
class Person{
public String id;
public Person(String id) {
this.id = id;
}
@Override
public String toString() {
return "Person{" +
"id='" + id + '\'' +
'}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
return Objects.equals(id, person.id);
}
@Override
public int hashCode() {
return Objects.hash(id);
}
}
public class TestDemo {
public static void main(String[] args) {
Person person1=new Person("1");
Person person2=new Person("1");
//用hashCode()时必须重写equals和hashode方法
System.out.println(person1.hashCode());//转为整数
System.out.println(person2.hashCode());
}
结果:
关于哈希表的一些问题:
1.hashcode相同,equals一定相同吗?
不一定,hashcode相同代表找的位置相同,但是一个位置不只有一个节点
2.equals相同,hashcode一定相同吗?
一定相同,equals是精准查找,指两个节点相同,那么它们肯定是在同一个位置的
3.如果new HashMap(19),bucket数组多大?
32(2^4<19<2^5),向上取最近的2的n次幂。
4.HashMap什么时候开辟bucket数组占用内存?
当第一次put时,数组变为了16,但是开始调用无参的构造方法时,数组大小为0。
3. hashMap何时扩容?
超过负载因子时,2倍扩容
4. 当两个对象的hashcode相同会发生什么?
hashcode相同表明在同一个位置,
会发生哈希冲突
5. 如果两个键的hashcode相同,你如何获取值对象? 遍历与hashCode值相等时相连的链表,直到相等或者 null
遍历hashcode位置链表,用equals判断key是否相同,若相同,更新val值;若不同,采用头插或尾插把节点插入链表。
6. 你了解重新调整HashMap大小存在什么问题吗?
需要2倍扩容,还需要重哈希,改变旧节点的位置到新的数组中
链表长度超过8, 数组不为空且数组长度超过64才会变成红黑树