概念
哈希表就是一种以 键-值(key-value) 存储数据的结构,我们只要输入待查找的值即key,即可查找到其对应的值。插入删除都是O(1)
插入元素 :
根据待插入元素的关键码,以此函数计算出该元素的存储位置并按此位置进行存放
搜索元素 :
对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置取元素比较,若 关键码相等,则搜索成功
该方式即为哈希(散列)方法,哈希方法中使用的转换函数称为哈希(散列)函数,构造出来的结构称为哈希表(Hash Table)(或者称散列表)
哈希函数
除留余数法
设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,按照哈希函数: Hash(key) = key% p(p<=m),将key转换成哈希地址
key % 数组长度 =》下标值
哈希查找第一步就是使用哈希函数将键映射成索引。这种映射函数就是哈希函数。
在实际中,我们的键并不都是数字,有可能是字符串,还有可能是几个值的组合等,所以我们需要实现自己的哈希函数。
特点: 两个相同的key,计算得到的hash值一定相同。
如果key为整数,hash‘值计算方式主要就是通过除留余数的方式
如果key为字符串,利用md5 sha1这种常用的字符串哈希算法
md5:
1、无论输入的字符串有多长,最终得到的md5的值都是固定长度。
2、如果两个字符串大部分内容相同,只有某个字符不同,计算生成的md5的值差别也会很大,这就决定了,md5计算的hash值最终冲突概率比较小,工程上可以忽略不记。
3、通过字符串计算哈希值很高效,通过md5找出字符串,计算量非常大,理论上不可行经常用于加密场景中。
md5可以用来进行文件校验,对文件计算md5之后进行比较如果md5值相等则相同。
冲突
如果两个不同的key,通过哈希函数计算之后得到了相同的hash值即:不同关键字通过相同哈希函数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞。 把具有不同关键码而具有相同哈希地址的数据元素称为“同义词”。
处理冲突
避免冲突
尽量避免冲突(降低冲突概率)
负载因子:哈希表中保存的元素个数 / 数组的长度
已知哈希表中已有的关键字个数是不可变的,那我们能调整的就只有哈希表中的数组的大小
如果给定的的数组只有120个元素大小,但是实际上有100个元素需要保存,此时冲突的概率也会比较高,相反的如果给定的的数组有1000个元素大小,但是实际上有100个元素需要保存,此时冲突的概率就会比较低 (负载因子越小,效率越高)
选择更合适的hash函数,一般将数组的长度为素数,哈希冲突会降低很多。
正面应对
a) 闭散列
发现冲突后,就在冲突的位置的周围找到一个空闲位置来进行插入元素。
常见查找位置方法:
1、线性探测(如下图)
2、二次探测
可以左右震荡 (下标以平方的大小增加,45+1(45、46)、45+4(45、46、47、48)、45+9、45+16、45+25.。。。。)
b) 开散列(哈希桶)
开散列的数组中的每个节点相当于一个链表的头结点,当出现hash冲突的时候,就把新的元素插入到链表中。
无论是第一种还是第二种方法,当哈希冲突比较严重的时候,插入查找删除的效率仍然会比较低。
哈希冲突比较严重:
1)扩容(降低负载因子),成本比较高
2)针对开散列来说(链表),把链表替换成更高效的数据结构,比如二叉搜索树(Java HashSet / HashMap),或者哈希表(二次哈希)
性能分析
虽然哈希表一直在和冲突做斗争,但在实际使用过程中,我们认为哈希表的冲突率是不高的,冲突个数是可控的, 也就是每个桶中的链表的长度是一个常数,所以,通常意义下,我们认为哈希表的插入/删除/查找时间复杂度是 O(1)。
哈希表对于空间的利用率是比二叉搜索树、顺序表之类的都低。
哈希表和 java 类集的关系
1. HashMap 和 HashSet 即 java 中利用哈希表实现的 Map 和 Set
2. java 中使用的是哈希桶方式解决冲突的
3. java 会在冲突链表长度大于一定阈值后,将链表转变为搜索树(红黑树)
4. java 中计算哈希值实际上是调用的类的 hashCode 方法,进行 key 的相等性比较是调用 key 的 equals 方 法。所以如果要用自定义类作为 HashMap 的 key 或者 HashSet 的值,必须覆写 hashCode 和 equals 方 法,而且要做到 equals 相等的对象,hashCode 一定是一致的。
哈希表实现
public class MyHashMap {
static class Node{
public int key;
public int value;
Node next;
public Node(int key, int value) {
this.key = key;
this.value = value;
}
}
private Node[] array = new Node[101];
private int size = 0; //哈希表实际元素个数
//负载因子:size/array.length
private int hashFunc(int key){
return key % array.length;
}
//插入
public void put(int key, int value){
//1、根据key带入哈希函数,计算下标,用key%数组长度
int index = hashFunc(key);
//2、根据下标得到对应的链表
Node head = array[index];
//3、先判定当前key是否存在,存在就修改value(不插入新节点)
for( Node cur = head; cur != null; cur = cur.next){
if(cur.key == key){
cur.value = value;
return;
}
}
//4、如果不存在,则插入
//进行链表头插
Node newNode = new Node(key, value);
newNode.next = head;
array[index] = head;
size++;
}
public Integer get(int key){
//1、根据key得到hash值
int index = hashFunc(key);
//2、在对应的链表中查找指定key对应的节点
Node head = array[index];
for (Node cur = head; cur != null; cur = cur.next){
if(cur.key == key){
return cur.value;
}
}
//3、没有找到
return null;
}
//删除
private Node finPrev(int key){ //找到要删除的前驱
int index = hashFunc(key);
Node prev = array[index];
while (prev.next != null){
if(prev.next.key == key){
return prev;
}
prev = prev.next;
}
return null;
}
public void delete(int key){
int index = hashFunc(key);
Node head = array[index];
Node prev = finPrev(key);
if(head.key == key){
head.next = array[index];
return;
}
if(prev == null){
System.out.println("没有这个元素!");
return;
}
Node cur = prev.next;
prev.next = cur.next;
size--;
}
}
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/152991.html