目录
1.1.在以下二种应用场景中,我们来逐渐认识hash表,并具体在这种场景下,你该选择哪种数据结构
一.对哈希表的整体认识
几乎所有的编程语言直接或者简介应用到哈希表这种数据结构,所以哈希表是一种非常重要的数据结构
1.1.在以下二种应用场景中,我们来逐渐认识hash表,并具体在这种场景下,你该选择哪种数据结构
1.假如一个公司中有两千名员工,现在我们需要用一种数据结构存储这两千名员工的各种信息,年龄性别身高等等,首先选择数组,但是在后期的先获取某一个员工的信息时,这个时候就会带来一个问题,我们必须通过数组的下标来获取员工信息,那么岂不是查一个信息就要问一下这个员工你的‘下标’是什么
2.联系人和电话,还是同样的问题,我们再用数组进行存储时,在获取任意一个联系人的信息时,就必须先获取到在数组中的下标
以上两种场景下,都会带来同样的问题,解决办法就是将员工的姓名直接转化为数组的下标和将联系人的姓名也同样转化为数组的下标
1.2引出 了如何将字符串转为下标值这个问题
方案一:将字符串每一项的字符的编码求和,比如说cats为3+1+20+19那么43就作为数组下标值,但这样会引发第二个问题,很多单词的下标值都为43,这样就有了方案二
方案二:求字符串的每一项的字符编码值,采用幂的连乘比如7654=7*10^3+6*10^2+5*10+4,解决了下标太少的问题,这样会引发第二个问题,下标值过大,这样会引发内存浪费,
综上所述这两种方案一个产生的下标太少不能用,一个产生的下标值太多,在实际应用中不可取
但是给我们提供了思路,只要能同时能解决这两个问题,那么上面那两个场景问题都能迎刃而解
1.3取余操作
1.现在需要一种方法,能将幂的连乘得到的整数范围,能尽量小,比如说,我们要存储5000个单词,最好需要5000个下标来存储单词,当然这只是一种理想状态,但趋于5000的需求也被提出来
顺着思路,我们将采用对其取余来解决这些问题,比如说:
我们在0--199中取5个数放在长度为十的数组中,将五个数对10取余,这样的话这五个数被放在同一位置也有可能,但是这种概率相比于求和就很小
1.4认识哈希表
通过上述三个模块的介绍,我们相当于对哈希表的思路自己做了遍,我来正式哈希表的概念
1.哈希化:将大数字转化为数组范围的下标的过程,我们就称之为哈希化。
2.哈希函数:通常我们会将单词转为大数字,大数字在转向数组下标所用的的函数(幂的连乘和取余操作所用的公式)
3.通过哈希化得到下标,然后将根据下标把数据填进去,得到一个完整的哈希表
1.5解决哈希表的冲突问题
1.虽然我们对哈希表的建立已经有了初步认识,但是哈希表的冲突问题还待解决
2.通过对哈希表的理解,在我看来哈希表的冲突问题就是哈希函数带来的,那么采用最为恰当的哈希函数,在本文中最为关键。·
1.6解决哈希表的冲突问题
1.链地址法
从图中可以看出链地址法的核心思想就是将原来数组的每个数组单元放置一个个链表,
当数据有了重复的下标,那么就将数据插入对应链表的首端/末端
图例:
2.开放地址法
1.我在这里用自己的逻辑简述一下,这个方法的核心思想就是从当我们利用下标位置存储数据时比如出现下面情况
如图所示我们想要插入一个142,但是下标位置为2已经被占,可以通过线性探测,从index+1的位置开始,直到找到一个null位置,再将142放进去
同时查询,删除操作都是同样的思想
但这种方法也同样出现问题,聚集问题,就是大量连续的下标值聚集在一起,这个时候数据的增删查效率过低
利用二次探测解决聚集问题,简单来说就是每一次线性探测,将步长拉长提高效率
但还是会造成步长不一的聚集
所以终极办法再哈希化向你走来
再哈希化就是对步长进行再优化,通过一种算法:
stepSize=con-(key%con) ( con为质数且小于数组容量)
stepSize就为每一次做探测的步长
1.7优秀的哈希函数
说了这么多,我们不难发现哈希表的核心在于能否有一个优秀的哈希函数,而优秀两字在哪里体现呢
1.快速计算,要求我们设计的哈希函数在计算hashcode(对应的下标)能够快速被计算出来
2.均匀分布,尽可能的让得到的hashcode(对应的下标)不同
1.8实现优秀的哈希函数
第一步将字符串转为比较大的hascode
第二步将hashcode通过取余操作压缩到数组大小范围中
HashTable.prototype.hashFun=function (str,size){
var hascode=0;
for(var i=0;i<str.length;i++){
//37为质数
//通过这种算法可以满足比较大的hashcode值
hascode=37*hascode+str.charCodeAt(i)
}
//通过取余操作可以将hascode压缩到数组大小范围中
var index=hascode%size;
return index;
}
以上就是我们哈希函数的设计
二.哈希表的封装
通过第一部分的介绍我们循序渐进对哈希表的认识,最终的完成了hash表最核心的哈希函数的实现,我们已经对哈希表的发动机(哈希函数)创造好了,接下来将对手写一个哈希表
2.1采用那种方法实现
链地址法
话不多说直接上代码
function HashTable (){
//定义hasCode
//基于数组实现
this.storage=[];
//当前我们已经存在了多少数据
this.count=0;
//长度
this.limit=7;
//霍纳算法,来计算hascode的值
HashTable.prototype.hashFun=function (str,size){
var hascode=0;
for(var i=0;i<str.length;i++){
//37为质数
//通过这种算法可以满足比较大的hashcode值
hascode=37*hascode+str.charCodeAt(i)
}
//通过取余操作可以将hascode压缩到数组大小范围中
var index=hascode%size;
return index;
}
一个最基本的哈希表就封装好了
2.2哈希表的插入和修改操作
在这里有几个要点,首先为什莫要将插入 操作和修改操作放在一起讲:
因为当你传入一个[k,v]时再插入之前必须先判断表中原来是否存在k,
存在做修改操作,反之做插入操作
// 插入和修改操作
hascode.prototype.put=function(key,value) {
//获取index
var index=hashFun(key,this.limit);
//取出indexd对应的子数组(取出桶)
var bucket=this.storage[index]
//判断当前bucket是否为空
if(bucket==null){
bucket=[];
//将'桶'指向index位置
this.storage[index]=bucket
count++
}
//判断是否修改数据
for(var i=0;i<bucket.length;i++){
//桶的每个位置存放的也是数组,即[k,v]
var tuple=bucket[i];
if(tuple[0]==key){
tuple[1]=value;
return
}
}
//如果没有修改操作,直接给对应的桶添加数据
bucket.push([key,value])
this.value+=1;
//判断是否需要扩容操作
if(this.count>this.limit*0.75){
this.resize(this.limit*2)
}
以上就是对插入与修改方法的封装
2.3获取操作
这个方法和插入与修改大同小异,直接上代码
HashTable.prototype.get=function(){
var index=this.hashFun(key,this.limit)
var bucket=this.storage[index]
if(bucket==null){
return null;
}
for(var i=0;i<bucket.length;i++){
var tuple=bucket[i];
if(tuple[0]==key){
return tuple[1]
}
}
}
2.4删除操作
在这里还是直接上代码
//删除操作
HashTable.prototype.remove=function(){
//获取到index
var index=this.hashFun(key,this.limit)
var bucket=this.storage[index]
if(bucket==null){
return null;
}
for(var i=0;i<bucket.length;i++){
var tuple=bucket[i];
if(tuple[0]==key){
//删除此处位置的[k,v]
bucket.splice=(i,1);
//将总量减一
this.count--;
return tuple[1];
}
//当数组容量过大,但元素过少,这个时候要进行缩小容量
if(this.limit>7&&this.limit*0.25<this.count){
this.resize(Math.floor(this.limit/2));
}
}
//等你遍历完成之后依然没有找到,返回空
return null;
}
2.5其他方法
//判断hash表是否为空
HashTable.prototype.isEmsty=function(){
return this.count==0;
}
//获取哈希表中元素的个数
HashTable.prototype.size=function(){
return this.count;
}
2.6扩容思想
一个完整的hash表就这样封装完成了,但是在认识哈希表的过程中,我们知道对于hash表我们对效率的要求非常高,以我们实现的hash表为例,随着存储的数据与来越多即 count越来越大,这样会造成一个问题,我们的桶即bucket会越来越长,将将会造成效率降低的问题
这时,我们不难想到,只有扩大limit才能避免这样的情况,所以在插入和修改操作中,要引入扩容操作
如何进行扩容:有人可能说直接手动增大limit,就完事了,但是并不是这样,在增大limit之后,要重新调用hash函数,将所有的存储得的数据项,重新获取位置,所以这是一个费时的过程,但是这是必要的
什么情况下需要扩容
比较常见的情况下 loadFoactor(装填因子)>0.75时进行扩容
HashTable.prototype.resize=function(newLimit){
//保存旧的hash中数组的数据
var oldStorge=this.storage;
//将新增一个哈希表
this.storage=[];
this.count=0;
this.limit=newLimit;
//将旧的哈希表中的每一个桶取出来
for(var i=0;i<oldStorge.length;i++){
var bucket=oldStorge[i]
if(oldStorge[i]==null){
continue;
}
//取出bucket中的数据,那抹取出数据,重新插入新的hash表中
for(var j=0;j<bucket.length;j++){
var tuple=bucket[j];
this.put(tuple[0],tuple[1]);
}
}
}
}
一个扩容操作就封装好了,在插入与修改方法中调用即可,详情代码就在插入修改方法中
//判断是否需要扩容操作
//(this.count/this.limit>0.75)
if(this.count>this.limit*0.75){
this.resize(this.limit*2)
}
}
这就完了,当然没有,我们知道当我们装填因子大于0.75会有效率问题,那小于某个数会有效率 问题吗,当然会有,这也就是之前说到的数组下标过少,导致空间浪费,上代码
//当数组容量过大,但元素过少,
//(this.count/this.limit>0.75)
if(this.limit>7&&this.limit*0.25<this.count){
this.resize(Math.floor(this.limit/2));
}
}
//根据key获取索引值:将数据插入对应位置
//根据索引值取出bucket:如果桶不在,创建桶,并且防止在该索引的位置
//判断新增还是修改原来的值
//哈希表中key值不能相等:所以再添加元素的同时,
//加载因子:利用率问题:
//设计哈希函数
//将字符串转化为比较大的数字:hascode
//将大的数字hascode压缩进数组范围大小之内
//扩容三问:为什么需要扩容随着加载因子的慢慢增大
function HashTable (){
//定义hasCode
//基于数组实现
this.storage=[];
//当前我们已经存在了多少数据
this.count=0;
//长度
this.limit=7;
//霍纳算法,来计算hascode的值
HashTable.prototype.hashFun=function (str,size){
var hascode=0;
for(var i=0;i<str.length;i++){
//37为质数
//通过这种算法可以满足比较大的hashcode值
hascode=37*hascode+str.charCodeAt(i)
}
//通过取余操作可以将hascode压缩到数组大小范围中
var index=hascode%size;
return index;
}
// 插入和修改操作
hascode.prototype.put=function(key,value) {
//获取index
var index=hashFun(key,this.limit);
//取出indexd对应的子数组(取出桶)
var bucket=this.storage[index]
//判断当前bucket是否为空
if(bucket==null){
bucket=[];
//将'桶'指向index位置
this.storage[index]=bucket
count++
}
//判断是否修改数据
for(var i=0;i<bucket.length;i++){
//桶的每个位置存放的也是数组,即[k,v]
var tuple=bucket[i];
if(tuple[0]==key){
tuple[1]=value;
return
}
}
//如果没有修改操作,直接给对应的桶添加数据
bucket.push([key,value])
this.value+=1;
//判断是否需要扩容操作
//(this.count/this.limit>0.75)
if(this.count>this.limit*0.75){
this.resize(this.limit*2)
}
}
//获取方法
//根据key通过哈希函数获取对应的index
//根据index获取对应的bucket(桶)
//判断桶是否为空
//通过线性查找bucket中每一个key是否等于传入的key
HashTable.prototype.get=function(){
var index=this.hashFun(key,this.limit)
var bucket=this.storage[index]
if(bucket==null){
return null;
}
for(var i=0;i<bucket.length;i++){
var tuple=bucket[i];
if(tuple[0]==key){
return tuple[1]
}
}
}
//删除操作
HashTable.prototype.remove=function(){
//获取到index
var index=this.hashFun(key,this.limit)
var bucket=this.storage[index]
if(bucket==null){
return null;
}
for(var i=0;i<bucket.length;i++){
var tuple=bucket[i];
if(tuple[0]==key){
//删除此处位置的[k,v]
bucket.splice=(i,1);
//将总量减一
this.count--;
return tuple[1];
}
//当数组容量过大,但元素过少,
//(this.count/this.limit>0.75)
if(this.limit>7&&this.limit*0.25<this.count){
this.resize(Math.floor(this.limit/2));
}
}
//等你遍历完成之后依然没有找到,返回空
return null;
}
//判断hash表是否为空
HashTable.prototype.isEmsty=function(){
return this.count==0;
}
//获取哈希表中元素的个数
HashTable.prototype.size=function(){
return this.count;
}
//哈希表的扩容
HashTable.prototype.resize=function(newLimit){
//保存旧的hash中数组的数据
var oldStorge=this.storage;
//将新增一个哈希表
this.storage=[];
this.count=0;
this.limit=newLimit;
//将旧的哈希表中的每一个桶取出来
for(var i=0;i<oldStorge.length;i++){
var bucket=oldStorge[i]
if(oldStorge[i]==null){
continue;
}
//取出bucket中的数据,那抹取出数据,重新插入新的hash表中
for(var j=0;j<bucket.length;j++){
var tuple=bucket[j];
this.put(tuple[0],tuple[1]);
}
}
}
}
数据结构yyds!!!!!!!