哈希表
一丶概念
首先为什么会有哈希表呢?
在这之前,我们发现,在顺序结构中,如果我们想要知道一个元素是否存在,那么基本就要遍历整个结构,这样它的时间复杂度就是O(N)。如果是树形结构,那么我们就要根据想要找的元素然后从根节点开始,接着不断向下调整,这样的话它的时间复杂度就是O(logN),这里是log以2为底。
那么能不能有一种数据结构,让我们不经过任何比较,仅仅通过一种映射关系就能够使得元素的存储位置和它的关键码一一对应呢?也就是能够直接一步查找到位?
有的,那就是我们这篇博客所讲的HashMap。
所以在这里,我们就有了如下定义:
这
里
的
映
射
关
系
,
也
就
是
转
换
函
数
叫
做
哈
希
(
散
列
)
函
数
,
构
\color{red}{这里的映射关系,也就是转换函数叫做哈希(散列)函数,构}
这里的映射关系,也就是转换函数叫做哈希(散列)函数,构
造
出
来
的
结
构
就
是
哈
希
表
,
也
叫
(
散
列
表
)
\color{red}{造出来的结构就是哈希表,也叫(散列表)}
造出来的结构就是哈希表,也叫(散列表)
二丶基本操作–问题引出
那么很明显了,如果说我们要建立一种一一对应的关系,那么基本的底层结构一定要有数组,事实上也确实如此。
那么对于这种数据结构基础操作无非就是两种----插入和删除。
但是不论你要插入还是删除我们都需要有一个前提,那就是查找,你最起码需要先找到插入或者删除的位置才能够进行接下来的操作吧。那么通过这点,我们就有了接下来问题的引出。
关于哈希冲突
我们如果说要查找一个元素,那么我们需要通过哈希函数来计算出它在哈希表当中存储的位置。那么这就有一个问题需要我们思考:
会不会有不同的元素储存在同一个位置呢?
肯定会的,因为除了不同的元素可能通过哈希函数计算之后会储存在相同的位置,元素里面也可能有相同的。我们把这种情况叫做哈希冲突。我们把不同关键字通过相同哈希函数计算出相同地址的两个数据元素成为“同义词”。
那么如果说有一个哈希地址被多个元素重复填充会发生什么情况呢?无非就是数据不断被覆盖。那么接下来就要对这个情况进行解决。
哈希冲突–避免
避免是在设计阶段就要考虑的问题,我们应该通过适合的哈希函数来进行哈希地址的计算。通过合理的哈希函数来计算哈希地址,我们可以很大程度上来避免这种情况的发生。但是是不是可以完全避免呢?
注意,哈希表底层容量基本都是小于实际要储存的关键字的数量,所以哈希冲突无法避免。
(1)设计原理
我们的哈希函数设计肯定是要遵循一定的准则的,也就是要遵守最起码的几个点。
1.哈希函数定义域要包括需要储存的全部关键字码。如果说我们有N个关键字,也
就是有N个元素,那么哈希表的容量要在(0,N - 1]。
2.在所有元素被存储到哈希表之后,要使得他能均匀分布
3.哈希函数应尽量简单
(2)常见的哈希函数
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种不同的符号在各位上出现的频率不一定相同,可能在某些位上分布比较均匀,每种符号出现的机会均等,在某些位上分布不均匀只有某几种符号经常出现。可根据散列表的大小,选择其中各种符号分布均匀的若干位作为散列地址。
目前的话就先介绍这几种,但是主要还是前面两种,后面的了解即可。
(3)负载因子调节
这里需要引申一个概念,就是关于散列表,也就是哈希表的载荷因子。
载荷因子 = 填入表中的元素个数 / 散列表长度
这里的意思就是:
填入表中的元素越多,那么载荷因子就越大。如果填入的元素过少,那么载荷因子就越小。
如果太大,那么就证明哈希冲突及其严重,如果过小,那么就证明空间利用率不够。
所以我们要把载荷因子调节在一个合理的范围内。一般我们是在0.7 ~ 0.8一下。但是具体取值还是要看情况而定。
所以归根结底,解决哈希冲突实际上也就是降低载荷因子。可是元素个数我们不能改变,那就只能改变哈希表中数组的大小了。
哈希冲突–解决
哈希冲突无法避免,只能尽力避免。那么我们尽力避免之后,就把冲突降的很低了。接下来,我们就开始着手解决。
(1)冲突解决–闭散列
闭散列也叫开放地址法,这里的闭散列我们用一句话总结:
当发生哈希冲突时,从发生哈希冲突的位置寻找下一个空位置。
那么这里需要探讨的地方也就很明显了,我们要怎样寻找下一个空位置呢?
<1>线性探测
所谓线性探测,就是从发生哈希冲突的地方,一个一个挨着往下寻找。
如图所示:
插
入
操
作
:
\color{red}{插入操作:}
插入操作:
如果说我要插入元素:14 24 34
(PS:这里我的哈希函数计算用对应元素除数组长度之后得到的余数,也就是求模运算)
那插入之后的数字应该是这个样子的:
14 % 10 = 4,4号位置为空,所以元素14在4号位置。
24 % 10 = 4,但是4号位置有元素了。所以就往后检测,检测到5号位置为空,就插入到位置5。
34 % 10 = 4, 4号元素有位置,所以就往后检测,检测6号元素为空,插入到六号元素。
删 除 操 作 : \color{red}{删除操作:} 删除操作:
关于删除操作我们有一个地方需要注意:
如果是删除操作,那么我们能直接删除元素嘛?
答案是:不可以,因为你随便删除已有元素会影响其他元素的搜索,比如我们删除上面的14,删除之后查找元素24,这个时候你检测到4号位元素为空,那么是不是就会返回一个false,但是其实24号元素是存在的。
所以我们不能随便删除,那么我们怎样解决呢?
解决方法:给每一个位置的元素都附带一个boolean类型的变量flag,初始为false。以后检测是否能够插入就检测该位置的变量是否是false就行。如果插入成功之后,就把该位置的falg赋值为true。如果删除,就不用删除元素,仅仅把flag改变为false就行。
线 性 探 测 总 结 \color{red}{线性探测总结} 线性探测总结
优点:处理哈希冲突的方式及其简单--挨个往后找
缺点:容易发生数据的堆积--一旦一个冲突,那么势必就会影响全局。
<2>二次探测
为了解决上面的问题,就有了二次探测。
那么什么是二次探测呢?
还是从哈希冲突的位置开始查找空的位置,但是这次不是一个一个找了,而是跳着找。
图示如下:
这里是一个示例,比如说我们可以跳两格查找。跳两格查找就是上面的这个样子。
插 入 和 删 除 操 作 参 考 上 面 , 下 面 讲 优 缺 点 \color{red}{插入和删除操作参考上面,下面讲优缺点} 插入和删除操作参考上面,下面讲优缺点
优点:解决了线性探测数据堆积问题
缺点:虽然表中元素增多,那么二次探测需要的查找次数也会增多。
(2)冲突解决–开散列 / 哈希桶
所谓开散列,又叫做链地址法。
通过哈希函数对关键码进行运算得出哈希地址,然后如果说不同的元素有着相同的哈希地址,那么把这些元素归于同一个子集和,每个子集和称为一个桶。桶中的元素用链表连接,链表头结点储存在哈希表中。
所以这里的开散列,就是把一个大范围的搜索问题转化为一个小的搜索问题。
<1>关于继续优化
这里的话就单纯提供一下思路,就是如果说我们的小范围数据还是过大,还要继续往小的转化的话。这个时候就可以用别的方式解决,比如说:
1.每个桶的背后是另一个哈希表
2.每个桶的背后是一棵二叉搜索树(再加点限制条件就变成红黑树了)
<2>关于代码实现
这一部分就给出具体代码了,如下:
public class HashBucket {
//首先构造节点
public static class ListNode{
Integer key;
Integer value;
ListNode next;
public ListNode(Integer key,Integer value){
this.key = key;
this.value = value;
}
}
//然后对于哈希表底层结构进行设置
ListNode[] table;
int size;//设定有效元素个数
public HashBucket(int cap){
if(cap < 0){
cap = 16;
}
table = new ListNode[cap];
}
//首先给出hash函数计算方法
public int hashfunc(int key){
return key % table.length;
}
//接着给出扩容的方法
public void enSureCapcity(){
int newlength = table.length * 2;
if(size >= table.length){
ListNode[] newtable = new ListNode[newlength];
for(int i = 0;i < table.length;i++){
ListNode cur = table[i];//先保存节点
while(cur != null){
table[i] = cur.next;
int newindex = cur.key % newlength;
cur.next = newtable[newindex];
newtable[newindex] = cur;
cur = table[i];
}
}
table = newtable;
}
}
//get()方法,查找一个元素是否存在
public Integer get(Integer key){
int index = hashfunc(key);
ListNode cur = table[index];
while(cur != null){
if(key.equals(cur.key)){
return cur.value;
}
cur = cur.next;
}
return null;
}
//put()方法,放入一个元素
public Integer put(Integer key,Integer value){
int index = hashfunc(key);
if(index > table.length){//如果情况不对,就要扩容
enSureCapcity();
}
ListNode cur = table[index];
while(cur != null){
if(key.equals(cur.key)){
Integer fac = cur.value;
cur.value = value;
return fac;
}
}
ListNode newNode = new ListNode(key,value);//如果key对应的节点不存在,那么就要插入了,用头插法
newNode.next = table[index];
table[index] = newNode;
size++;
return newNode.value;
}
//返回key对应的value值,如果为空,就返回你设置的值
public Integer getOrDefault(Integer key,Integer val){
Integer fac = get(key);
if(fac == null){
return val;
}
return fac;
}
//删除元素
public Integer remove(Integer key){
int index = hashfunc(key);
ListNode cur = table[index];
ListNode pre = null;
while(cur != null){
if(key.equals(cur.key)){
Integer oldvalue = cur.value;
if(table[index] == cur){
table[index] = cur.next;
}else{
pre.next = cur.next;
}
size--;
cur.next = null;
return oldvalue;
}
pre = cur;
cur = cur.next;
}
return null;
}
//检测key存不存在
public boolean containsKey(Integer key){
return get(key) != null;
}
//检测value值存不存在
public boolean containsValue(Integer val){
for (int i = 0;i < table.length;i++){
ListNode cur = table[i];
while(cur != null){
if(val.equals(cur.value)){
return true;
}
cur = cur.next;
}
}
return false;
}
//检测当前有效元素个数
public int size(){
return size;
}
}