哈希(散列)原理及使用
哈希(散列)原理及避免哈希冲突
- 哈希(散列)原理及使用
- 哈希冲突
- 常见哈希函数
- 解决哈希冲突方案
- 实现一个开散列哈希表
当我们在一堆数据中查找一个元素时,使用普通方法得到的时间复杂度是O(N)
,使用搜索树的时间复杂度为O(logn)
,而现在告诉你有一种数据结构可以只使用O(1)
的时间复杂度就可以查找,对程序的效率提升是一种质的飞跃。而这就是哈希(散列)。
哈希函数:hash(key) = key % capacity;capacity为存储元素底层空间总的大小。
哈希冲突
//数组
arr = {1,3,6,5,8,2};
这就是哈希冲突!哈希冲突是永远存在的,只能尽可能的避免哈希冲突!
引起哈希冲突的因素:
- 哈希函数设计不合理。
哈希函数设计规则:
- 哈希函数的定义域必需包含需要存储的所有关键字,如果哈希表中允许有
m
个地址,那么这个值域必须在0~m-1
之间- 哈希函数计算得到的地址应该均匀分布在整个哈希表中
- 哈希函数应该简单易懂
常见哈希函数
- 直接定制法
针对比较小的空间且连续空间进行计算,需要先知道关键字的分布情况
- 除留余数法
设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,按照哈希函数:
Hash(key) = key% p(p<=m),将关键码转换成哈希地址
- 负载因子法
设置一个负载因子
α
,α = 填入表中元素/哈希表的长度
,通过这个公式可以知道当填入数据越多,α
就会越大,也表明了产生冲突的可能性越大,所以要避免冲突就尽量将负载因子减小!
解决哈希冲突方案
-
闭散列
也称为“开放地址法”,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以
把key存放到冲突位置中的“下一个” 空位置中去
线性探测:
插入元素,合理利用剩余空间,不过当删除3
这个元素时,13
就会丢失头部,而下标为3的元素此刻为空,意味着失去指示头,对13
进行查找时可能会受到影响。
二次探测法
针对线性探测的把产生冲突的数据放在一起,与找下一个位置有关联,而找到下一个位置就填入,未免有点不太合理。二次探测为了避免这个问题,设立了一个寻找下一个位置的方法:H_i= (H_0 + i^2)%M
H_i : 代表的是当前元素的下标
H_0 : 是本来元素的下标
i : 是指当前发生冲突的次树,i从1开始计算
M : 是当前数组总长度
这里要清楚当计算出来的下标已经存在数字时,i+1,直到找到一个空,将数据填入。
不过这样会浪费很多空间,也是哈希的缺陷所在。
- 开散列
链地址法,开链法,首先对关键码集合用哈希函数计算地址,具有相同的码又归为一类集合,每一个集合称为一个桶!桶中的元数使用链表进行串联,头节点存储在哈希表中。
实现一个开散列哈希表
首先要明确需要一个链表和一个链表数组。
- 链表数组保存哈希值
- 链表保存后续值
public class HashBucket {
//哈希链表
private static class Node {
private int key;
private int value;
Node next;
//构造方法
public Node(int key, int value) {
this.key = key;
this.value = value;
}
}
private Node[] array;//哈希数组
private int size; // 当前的数据个数
private static final double LOAD_FACTOR = 0.75;
private static final int DEFAULT_SIZE = 8;//默认桶的大小
//设置一个哈希数组长度为DEFAULT_SIZE
public HashBucket() {
array = new Node[DEFAULT_SIZE];
}
//添加元素操作
public int put(int key, int value) {
//先查找是否存在这个值
int index = key % array.length;//找到这个哈希值对应的数组下标
Node cur = array[index];//找到链表头节点
while (cur != null){
if(cur.key == key){
//查找
cur.value = value;
return value;
}
cur = cur.next;
}
//使用头插法
Node node = new Node(key,value);
node.next = array[index];
array[index] = node;
size++;
//判断是否需要扩容
if(loadFactor() == 1){
resize();
}
return -1;
}
private void resize() {
//重新哈希
Node[] newArray = new Node[2*array.length];//创建一个两倍大小的新哈希表
for (int i = 0; i <array.length ; i++) {
Node cur = array[i];
while (cur!=null){
//找到了在新数组当中的位置
int index = cur.key%newArray.length;//重新计算每个哈希值并填入新的哈希链表中
Node nextNode = cur.next;
cur.next = newArray[index];
newArray[index] = cur;
cur = nextNode;
}
}
array = newArray;
}
//判断是否满了
private double loadFactor() {
return size * 1.0 / array.length;
}
//获取key,找到这个key值下的所有key,遍历找到后,返回value
public int get(int key) {
// write code here
int index = key % array.length;
Node cur = array[index];
while (cur != null){
if(cur.key == key){
return cur.value;
}
cur = cur.next;
}
return -1;
}
}
测试
public static void main(String[] args) {
HashBucket hashBucket = new HashBucket();
hashBucket.put(1,12);
hashBucket.put(2,11);
hashBucket.put(3,13);
hashBucket.put(4,15);
hashBucket.put(6,16);
hashBucket.put(7,17);
System.out.println(hashBucket.get(1));
System.out.println(hashBucket.get(7));
}