目录
1 概念
2 冲突-概念
3 冲突-避免
4 冲突-避免-哈希函数设计
(1)直接定制法--(常用)
(2)除留余数法--(常用)
(3)平方取中法--(了解)
(4)折叠法--(了解)
(5)随机数法--(了解)
(6)数学分析法--(了解)
5 冲突-避免-负载因子调节(重点掌握)
6 冲突-解决
(1)冲突-解决-闭散列
(2)冲突-解决-开散列/哈希桶(重点掌握)
(3)冲突严重时的解决办法
7 开散列/哈希桶实现
(1)MyHashMap 类
(2)新增 & 修改
(3)扩容
(4)查找是否包含key值
(5)删除指定key值
1 概念
顺序结构以及平衡树中,元素关键码与其存储位置之间没有对应的关系,因此在查找一个元素时,必须要经过关键码的多次比较。顺序查找时间复杂度为O(N),平衡树中为树的高度,即O( ),搜索的效率取决于搜索过程中元素的比较次数。理想的搜索方法:可以 不经过任何比较,一次直接从表中得到要搜索的元素。 如果构造一种存储结构,通过某种函数(hashFunc)使元素的存储位置与它的关键码之间能够建立一一映射的关系,那么在查找时通过该函数可以很快找到该元素。
- 插入元素:根据待插入元素的关键码,以此函数计算出该元素的存储位置并按此位置进行存放
- 搜索元素:对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置取元素比较,若关键码相等,则搜索成功
该方式即为哈希(散列)方法,哈希方法中使用的转换函数称为哈希(散列)函数,构造出来的结构称为哈希表(HashTable)(或者称散列表)
2 冲突-概念
对于两个数据元素的关键字和 (i != j),有 != ,但有:Hash( ) == Hash( ),即: 不同关键字通过相同哈希哈数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞。把具有不同关键码而具有相同哈希地址的数据元素称为“同义词”。
3 冲突-避免
首先,我们需要明确一点,由于我们哈希表底层数组的容量往往是小于实际要存储的关键字的数量的,这就导致一个问题, 冲突的发生是必然的 ,但我们能做的应该是尽量的降低冲突率。
4 冲突-避免-哈希函数设计
(1)直接定制法--(常用)
取关键字的某个线性函数为散列地址:Hash(Key)= A*Key + B 优点:简单、均匀 缺点:需要事先知道关键字的分布情况 使用场景: 适合查找比较小且连续的情况
(2)除留余数法--(常用)
设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,按照哈希函数:Hash(key) = key% p(p<=m),将关键码转换成哈希地址。
(3)平方取中法--(了解)
假设关键字为1234,对它平方就是1522756,抽取中间的3位227作为哈希地址; 再比如关键字为4321,对它平方就是18671041,抽取中间的3位671(或710)作为哈希地址 平方取中法比较适合: 不知道关键字的分布,而位数又不是很大的情况
(4)折叠法--(了解)
折叠法是将关键字从左到右分割成位数相等的几部分(最后一部分位数可以短些),然后将这几部分叠加求和,并按散列表表长,取后几位作为散列地址。
折叠法适合 事先不需要知道关键字的分布,适合关键字位数比较多的情况
(5)随机数法--(了解)
选择一个随机函数,取关键字的随机函数值为它的哈希地址,即H(key) = random(key),其中random为随机数函数。 通常应用于 关键字长度不等时采用此法 。
(6)数学分析法--(了解)
设有n个d位数,每一位可能有r种不同的符号,这r种不同的符号在各位上出现的频率不一定相同,可能在某些位上分布比较均匀,每种符号出现的机会均等,在某些位上分布不均匀只有某几种符号经常出现。可根据散列表的大小,选择其中各种符号分布均匀的若干位作为散列地址。数字分析法通常适合 处理关键字位数比较大的情况,事先知道关键字的分布且关键字的若干位分布较均匀的情况
【注意】哈希函数设计的越精妙,产生哈希冲突的可能性就越低,但是无法避免哈希冲突
5 冲突-避免-负载因子调节(重点掌握)
所以当冲突率达到一个无法忍受的程度时,我们需要通过降低负载因子来变相的降低冲突率。已知哈希表中已有的关键字个数是不可变的,那我们能调整的就只有哈希表中的数组的大小。
6 冲突-解决
(1)冲突-解决-闭散列
闭散列:也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以 把 key存放到冲突位置中的“下一个” 空位置中去 。那如何寻找下一个空位置呢?
(1)插入通过哈希函数获取待插入元素在哈希表中的位置(2)如果该位置中没有元素则直接插入新元素,如果该位置中有元素发生哈希冲突,使用线性探测找到下一个空位置,插入新元素
采用闭散列处理哈希冲突时,不能随便物理删除哈希表中已有的元素,若直接删除元素会影响其他元素的搜索。比如删除元素4,如果直接删除掉,44查找起来可能会受影响。因此线性探测采用标记的伪删除法来删除一个元素。
文章来源:https://www.toymoban.com/news/detail-410522.html
研究表明:当表的长度为质数且表装载因子a不超过0.5时,新的表项一定能够插入,而且任何一个位置都不会被探查两次。因此只要表中有一半的空位置,就不会存在表满的问题。在搜索时可以不考虑表装满的情况,但在插入时必须确保表的装载因子a不超过0.5,如果超出必须考虑增容。 文章来源地址https://www.toymoban.com/news/detail-410522.html
因此:比散列最大的缺陷就是空间利用率比较低,这也是哈希的缺陷。
(2)冲突-解决-开散列/哈希桶(重点掌握)
开散列法又叫链地址法(开链法),首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中。
(3)冲突严重时的解决办法
哈希桶其实可以看作将大集合的搜索问题转化为小集合的搜索问题了,那如果冲突严重,就意味着小集合的搜索性能其实也时不佳的,这个时候我们就可以将这个所谓的小集合搜索问题继续进行转化,例如:
- 1. 每个桶的背后是另一个哈希表
- 2. 每个桶的背后是一棵搜索树
7 开散列/哈希桶实现
(1)MyHashMap 类
public class MyHashMap {
private static class Node {
int key;
int value;
Node next;
public Node(int key, int value) {
this.key = key;
this.value = value;
}
}
// 当前哈希表中有效元素个数
private int size;
// 取模数,简单起见,和数组长度保持一致
private int M;
// 保存Node元素的数组
private Node[] data;
// 负载因子
private static final double LOAD_FACTOR = 0.75;
public MyHashMap(){
this.data = new Node[16];
}
public MyHashMap(int c){
this.data = new Node[c];
this.M = c;
}
}
(2)新增 & 修改
// 在当前哈希表中添加一对新元素,返回添加前的值
public int put(int key,int value){
// 1.首先计算出当前新元素的下标
int index = hash(key);
// 2.在当前的子链表中判断key值是否存在,若存在,只需要更新value即可
for (Node x = data[index];x != null;x = x.next) {
if (x.key == key) {
// 存在,只需要更新value
int oldValue = x.value;
x.value = value;
return oldValue;
}
}
// 3.此时key第一次出现,头插到当前的子链表中
Node node = new Node(key,value);
node.next = data[index];
data[index] = node;
size ++;
// 4.判断当前哈希表的冲突情况,是否需要扩容
if (size >= this.data.length * LOAD_FACTOR) {
resize();
}
return -1;
}
// 计算hash值
private int hash(int key) {
return key % M;
}
(3)扩容
private void resize() {
Node[] newData = new Node[data.length * 2];
this.M = newData.length;
// 搬移原数组的所有节点
for (int i = 0; i < data.length; i++) {
for (Node x = data[i]; x != null;) {
Node next = x.next;
// 当前x搬移到新数组的对应位置
int index = hash(x.key);
// 头插到新数组的对应位置
x.next = newData[index];
newData[index] = x;
// 继续搬移原数组的下一个节点
x = next;
}
}
// 更新data的指向
data = newData;
}
(4)查找是否包含key值
// 查看是否包含key
public boolean containsKey(int key){
int index = hash(key);
for (Node x = data[index]; x != null ; x = x.next) {
if(x.key == key){
return true;
}
}
return false;
}
(5)删除指定key值
// 在当前哈希表中删除指定的key值节点
public boolean removeKey(int key){
// 1.先求索引
int index = hash(key);
// 判空
if(data[index] == null){
return false;
}
// 剩下就是链表的删除问题
if (data[index].key == key) {
data[index] = data[index].next;
size --;
return true;
}
Node prev = data[index];
while (prev.next != null) {
if (prev.next.key == key) {
prev.next = prev.next.next;
size --;
return true;
}
}
// 此时不存在指定的key值
return false;
}
到了这里,关于【数据结构】用Java实现哈希表的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!