1. 概念
在顺序结构和平衡树中,元素与其存储位置之间没有对应的关系,因此在查找一个元素时必须经过元素之间的多次比较,顺序查找时间复杂度为O(n),平衡树中的时间复杂度为O(log2 n),搜索效率与比较的次数有关。
理想的搜索方法:可以不经过比较就可以得到要搜索的元素。如果构造一种存储结构,通过某种函数(hashfunc)来使元素与存储位置之间能够建立一一映射的关系,那么在查找时通过该函数就可以很快的找到该元素。
- 元素插入
在元素插入时通过此函数计算出该元素的存储位置将其插入。 - 元素搜索
因为元素是按公式插入的,那么我们同样可以通过该函数计算出这个元素的存储位置,那么就不需要进行元素之间的比较直接就可以的到该元素。
例如:数据集合{1,7,4,5,9}
将哈希函数设置为:hash(key) = key % length;
(length为元素底层空间的总大小)
但是按照上述操作,如果插入44会如何?
为了解决这个问题所以引出下面的冲突。
2. 冲突
不同的元素通过相同的哈希函数计算出了相同的地址,这种现象称之为哈希冲突或者哈希碰撞。(例如上述提到的4 和44这两个元素)
3. 冲突避免
那么怎么避免冲突呢?
首先需要明确的是发生冲突是必然的,我们需要做的是尽量的降低冲突率。
引起哈希冲突的可能原因:哈希函数设计不够合理。
哈希函数的设计原则:
- 哈希函数的定义域必须包括需要存储的全部元素,如果散列表有m个地址时其函数的值域必须在[0,m-1)。
- 哈希函数计算出来的地址能均匀的分布在整个空间。
- 哈希函数应该比较简单。
调节负载因子
负载因子定义:α = 表中元素的个数 / 散列表的长度。
α越大表示填入的元素越多,产生冲突的可能性就越大,反之,α越小,表中的元素越少,产生冲突的可能性就越小。
在Java系统底层中限制负载因子为0.75,超过此值将resize散列表(扩容)。
4. 冲突解决
解决办法:闭散列和开散列。
闭散列
也叫开放地址法,当发生哈希冲突时如果哈希表未被装满,说明哈希表中必然还存在着空位,那么可以把key值空位存放到冲突位置的下一个空位去。如何寻找下一个空位?
a.线性探测
例如上述所说的44先通过哈希函数计算出哈希地址为4,因为此时4号下标,但是因为此时4号小标已经存放了值为4的元素(发生了哈希冲突)
。
插入
- 通过哈希函数获取待插入元素在哈希表中的位置。
- 如果该位置中没有元素则直接插入新元素,如果该位置中有元素发生哈希冲突,使用线性探测找到
下一个空位置,插入新元素
采用闭散列处理哈希冲突时,不能随便物理删除哈希表中已有的元素,若直接删除元素会影响其他元素的搜索。比如删除元素4,如果直接删除掉,44查找起来可能会受影响。因此线性探测采用标记的伪删除法来删除一个元素。
2.二次探测
线性探测的缺陷是产生冲突的数据堆积在一块,这与其找下一个空位置有关系,因为找空位置的方式就是挨着往后逐个去找,因此二次探测为了避免该问题,找下一个空位置的方法为:Hi = ( H0+i^2 )% m, 或者Hi = ( H0- i^2 )% m。其中:i =1,2,3…, 是通过散列函数Hash(x)对元素的关键码 key 进行计算得到的位置,m是表的大小。
接着以上述4和44为例
因此:闭散列最大的缺陷就是空间利用率比较低,这也是哈希的缺陷。
开散列(重点)
也叫哈希桶又叫链地址法(开链法),首先对元素集合用哈希函数计算哈希地址,具有相同地址的元素归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中。
接着以上述数组为例:
开散列,可以认为是把一个在大集合中的搜索问题转化为在小集合中做搜索了。
5. 性能分析
虽然哈希表一直在和冲突做斗争,但在实际使用过程中,我们认为哈希表的冲突率是不高的,冲突个数是可控的,也就是每个桶中的链表的长度是一个常数,所以,通常意义下,我们认为哈希表的插入/删除/查找时间复杂度是
O(1) 。
6. 和Java类集的关系
- HashMap 和 HashSet 即 java 中利用哈希表实现的 Map 和 Set。
- java 中使用的是哈希桶方式解决冲突的。
- java 会在冲突链表长度大于一定阈值后,将链表转变为搜索树(红黑树)。
- java 中计算哈希值实际上是调用的类的 hashCode 方法,进行 key 的相等性比较是调用 key 的 equals 方法。所以如果要用自定义类作为 HashMap 的 key 或者 HashSet 的值,必须覆写 hashCode 和 equals 方法,而且要做到 equals 相等的对象,hashCode 一定是一致的。
6. 代码实现
这里只写方法,自己直接写个main方法调用即可。
此代码采用的是前插法,后插法直接看IDEA的底层即可。
class HashBack{
static class Node{
public int data;
public int value;
public Node next;
public Node(int data ,int value){
this.data = data;
this.value = value;
}
}
public Node[] array;
public int usedSize;
public HashBack(){
this.array = new Node[10];
this.usedSize = 0;
}
public void put(int key,int value){
int index = key % array.length;
for ( Node cur = array[index];cur != null;cur = cur.next){
if(cur.data == key){
cur.value = value;
return;
}
}
//该链表中没有你要插入的元素,头插法
Node node = new Node(key,value);
node.next = array[index];
array[index] = node;
this.usedSize++;
if (loadFactor() > 0.75){
resize();
}
}
//计算负载因子
public double loadFactor(){
return usedSize*1.0 / this.array.length;
}
/*
*扩容时必须重新哈希
* */
public void resize(){
Node[] newArray = new Node[array.length*2];
for (int i = 0; i < this.array.length; i++) {
Node node = null;
for (Node cur = array[i];cur != null ; cur = node){
node = cur.next;
int index = cur.data % newArray.length;
cur.next = newArray[index];
newArray[index] = cur;
}
}
this.array = newArray;
}
public int getValue(int key){
int index = key % array.length;
for(Node cur = array[index];cur != null;cur = cur.next){
if (cur.data == key) return cur.value;
}
return -1;
}
}