哈希表
哈希表:
底层是一个数组,通过关键字key,找到合法的下标,读取数据。
将数据中的key,通过一定的关系——哈希函数,可以确定该数据在数组中的坐标。
哈希冲突:不同关键字通过相同的哈希函数计算出相同的哈希地址。
如上例中,1, 11, 21与10取模得到的哈希索引坐标都是1,这就造成了哈希冲突。
常见的哈希函数:
1)直接定址法:取某个线性函数为散列地址。
2)除留余数法:设哈希表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,按照哈希函数:Hash(key) = key% p(p<=m),将关键码转换成哈希地址.
负载因子:r = 填入表中的数据个数/散列表(哈希表)的长度
当负载因子过大时,就要想办法来降低冲突率,而哈希表中的数据个数是不能变的,能够改变的只有哈希表的数组大小,我们往往通过对哈希表的数组进行扩容来降低负载因子。
解决哈希冲突两种常见方法是:闭散列(开放定址法)和开散列(链地址法)
1 闭散列(开放定址法)
开放定址法,当发生哈希冲突时,如果哈希表未被装满,那么就可以将key存放到冲突位置的“下一个”空位置中去。
(1)线性探测
比如1,11,21在存放数据时,都发生了哈希冲突,那么就可以用线性探测法,将其移至下一个空位置中。
注意:
- 使用闭散列处理哈希冲突时,不能直接删除某个元素,因为用闭散列处理的数据都是互相关联的,可以采用标记的伪删除法来删除一个元素。
(2)二次探测
在线性探测中,数据容易堆积到一起,找位置都是按顺序往后查找空位。因此二次探测提供了一个找空位置的方法:
Hi = (H0 +i2) % compacity
其中,H0为哈希函数求得的索引,i为冲突的次数。
研究表明:当表的长度为质数且装载因子a不超过0.5时,新的数据一定能够插入,而且任何一个位置都不会被探查两次。因此只要表中有一半的空位置,就不会存在表满的问题。在搜索时可以不考虑表装满的情况,但在插入时必须确保表的装载因子a不超过0.5,如果超出必须考虑增容。因此散列表最大的缺陷就是空间利用率比较低,同时这也是哈希的缺陷。
2 开散列(链地址法)(哈希桶)
首先通过哈希函数计算散列地址,具有相同地址的元素归于同一个子集合中,集合中的元素通过一个单链表链接起来,将各链表的头节点存储于哈希表中。
开散列可以认为是把一个大集合中的搜索问题转化为小集合中的搜索问题。但是如果冲突严重时,小集合的搜索性能就会变差,这时需要将小集合进行转换,可以将每个小集合替换为另一个哈希表,也可以替换为一颗搜索树。
注意:
- 在应用中,我们一般都会限制,每个桶的链表长度,如果冲突链表长度,就考虑对哈希数组进行扩容。
- 哈希表的插入,删除,查找的时间复杂度都是O(1)
- HashMap 和HashSet 即java 中利用哈希表实现的Map 和Set
- java 中使用的是哈希桶方式解决冲突的
- java 会在冲突链表长度大于一定阈值后,将链表转变为搜索树(红黑树)
- java 中计算哈希值实际上是调用的类的hashCode 方法,进行key 的相等性比较是调用key 的equals 方法。所以如果要用自定义类作为HashMap 的key 或者HashSet 的值,必须覆写hashCode 和equals 方法,而且要做到equals 相等的对象,hashCode 一定是一致的。
基本数据类型哈希表的简单实现:
public class HashBucket {
static class Node {
private int key;
private int value;
private Node next;
public Node(int key, int value) {
this.key = key;
this.value = value;
}
}
private Node[] array = new Node[8];//存放单链表的头结点
private int size;//当前数据的个数
public int getValue(int key) {
int index = key % array.length;
//遍历array[index]下标的链表,找到值为key的数据,并且返回
Node head = array[index];
Node cur = head;
while(cur!=null){
if(cur.key == key) {
return cur.value;
}
cur=cur.next;
}
return -1;
}
//插入数据
public void put(int key,int value) {
int index = key % array.length;
//遍历链表,如果有相同的key,则将后插入的key-value代替前面的
Node cur = array[index];
while(cur!=null){
if(cur.key == key) {
cur.value = value;
return;
}
cur= cur.next;
}
//头插法
Node node = new Node(key,value);
node.next = array[index];
array[index] = node;
this.size++;
//计算负载因子,如果大于0.75,就对哈希数组进行扩容
if(loadFactor() >= 0.75) {
resize();
}
}
//计算负载因子
private double loadFactor() {
return this.size*1.0 / array.length;
}
//哈希数组扩容
public void resize() {
Node[] newArray = new Node[2*array.length];
//重新哈希
//1、遍历原来的数组,将里面的元素重新进行哈希到新的数组里面
for(int i = 0;i < array.length;i++) {
Node curNext = null;
//拿到原来链表的头节点
Node cur = array[i];
while(cur!=null){
curNext = cur.next;
//重新计算哈希数组坐标
int index = cur.key % newArray.length;
//头插法插入到新的链表中去
cur.next = newArray[index];
newArray[index] = cur;
cur=curNext;
}
}
array = newArray;
}
}