目录
1、哈希表的引入
在我们已经学过的数据结构顺序表、数组中,存储的元素与其存储位置之间没有相应的关系,因此查找一个元素时,必须要经过多次的比较,时间复杂度达到了O(N)。
理想的搜索方式:不经过比较,可以直接得到要查找的元素。如果构造一种存储结构,通过某种函数(hashFunc)使元素的存储位置与它的关键码之间能够建立一一映射的关系,那么在查找时通过该函数可以很快找到该元素。这种方法称为哈希(散列)方法,哈希方法中使用函数成为哈希(散列)函数,构造出来的这种结构称为哈希表。
- 插入元素:插入时通过这个函数计算出插入的位置。
- 搜索元素:将此元素通过该函数计算,找到相应位置,如果此位置的元素与搜索元素相等,则查找成功。
哈希冲突:不同的关键字通过相同的哈希函数计算得到相同的哈希地址。由于哈希表的底层数组往往要小于实际要存储的元素的个数,所有冲突无法避免,我们要做的是降低冲突的发生率。
2、哈希冲突的避免--函数设计
引起冲突的一个原因可能是:哈希函数的设计不合理。
哈希函数的设计规则:
- 哈希函数的定义域一定要包含需要存储的所有元素,哈希表允许有m个位置时,至于必须要在0到m-1之间。
- 焊锡函数计算出来的地址能够均匀地分布在整个空间中。
- 哈希函数设计要简单。
常见的哈希函数:
- 直接定制法:去关键字的某个线性函数为哈希函数(需要事先知道关键字的分布情况)。适合查找比较小且连续的情况。
- 除留余数法:设哈希表中允许的地址数为m,取一个不大于m,最接近m的质数作为除数,对关键码进行取模作为哈希地址。
3、冲突避免--负载因子调节
对于开放地址法,负载因子应严格控制在0.8以下。
当冲突率比较高时,我们需要通过降低负载因子来降低冲突率。哈希表中关键字个数不可变,只能对哈希表中数组进行扩容来降低冲突率。
4、哈希冲突--解决
解决哈希冲突有两种常见的方法:闭散列和开散列
(1)闭散列
闭散列:也叫开放地址法,当发生哈希冲突时,如果哈希表未装满,哈希表中必然还存在空位置,可以将key存放在冲突位置的“下一个”空位置上。寻找下一个空位置:
a、线性探测:从发生冲突的位置依次向后探测,直到寻找到下一个空位置。
插入时,如果通过哈希函数获取的位置没有元素,直接插入新元素;如果该位置有元素,则发生哈希冲突,通过线性探测找到下一个空位置,进行插入。
b、二次探测:线性探测的缺陷是产生冲突的元素会堆积在一起,为了避免该问题,使用以下方法找下一个位置:H=(N+i^2)%m或H=(N-i^2)%m。(其中i=1,2,3……,N为通过哈希函数第一次找到的位置,m为哈希表的大小)
使用闭散列处理哈希冲突时,不能随便删除哈希表中已有的元素,若直接删除会影响其他元素的查找。因此删除时通过标记的伪删除法来删除元素。
当表的长度为质数且负载因子不超过0.5时,新的表项一定可以插入,而且任何一个位置都不会被探查两次。因此只要表中有一半的空位置,就不会存在表满的问题。如果负载因子超过0.5,则需要考虑扩容。因此,闭散列最大的缺点就是空间利用率比较低,这也是哈希的缺陷。
(2)开散列/哈希桶
开散列法又叫链地址法。首先对关键码集合用哈希函数计算哈希地址,具有相同地址的关键码归于同一个子集和,每一个子集和称为一个哈希桶,各个桶中的元素通过单链表链接起来,各链表的头节点存储在哈希表中。
开散列可以认为是把一个大集合的搜索问题转化为小集合的搜索问题。
5、实现哈希表(开散列)(重点)
开散列实现哈希表,数组的每个元素相当于一个链表,查找元素的时候遍历当前下标的链表即可。插入元素时要特别注意,如果插入后哈希表的负载因子超过了默认的负载因子,需要对数组进行扩容,同时对所有元素进行重新哈希,切记!!!
class Node{
public int key;
public int val;
public Node next;
public Node(int key,int val){
this.key=key;
this.val=val;
}
}
public class HashTable {
public Node[] array=new Node[8];
public int usedSize;
//默认的负载因子
public static final double loadFactor=0.75;
//插入元素
public void put(int key,int val){
Node node=new Node(key,val);
//1、查找位置
int index=key%array.length;
//2、遍历当前链表
Node cur=array[index];
while(cur!=null){
//存在相同key值时,更新val值
if(cur.key==node.key){
cur.val=node.val;
return;
}
cur=cur.next;
}
node.next=array[index];
array[index]=node;
this.usedSize++;
//如果负载因子大于默认的负载因子,需要扩容,
//这里计算负载因子时时整数,需要对usedSize进行强转
if((double)usedSize/array.length>=loadFactor){
reHash();
}
}
//扩容,重新哈希
private void reHash(){
//扩容,申请2倍的空间
Node[] tmp=new Node[2*array.length];
//对哈希表中所有的元素进行重新哈希
for(int i=0;i<array.length;++i){
Node cur=array[i];
while(cur!=null){
Node curNext=cur.next;
int newIndex=cur.key%tmp.length;
cur.next=tmp[newIndex];
tmp[newIndex]=cur;
cur=curNext;
}
}
this.array=tmp;
}
//查找
public boolean find(int key){
//1、找到位置
int index=key%this.array.length;
//2、遍历当前链表
Node cur=array[index];
while(cur!=null){
if(cur.key==key){
cur=cur.next;
this.usedSize--;
return true;
}
}
return false;
}
}