Js实现哈希表
1.哈希表是什么?
哈希表的结构就是数组,它神奇的地方在于对下标值得变换,这种变换称之为哈希函数,通过哈希函数可以获取HashCode.
2.哈希表的一些概念
- 哈希化:将大数字转化成为数组范围内下标的过程。
- 哈希函数:通常我们将单词转成大数字,大数字在进行哈希化的代码实现放在一个函数中,这个函数就是哈希函数。
- 哈希表:最终将数据插入到这个数组,对整个结构的封装,称之为一个哈希表。
3.在哈希化过程中,出现冲突的解决方案
在哈希化的过程中,将大数字转成数组范围的下标时,比如:遇到两个不同的单词,转大数字,再转下标时,如果下标一样,我们称发生了冲突。对于冲突的解决方案有两种:链地址法和开放地址法。
-
链地址法
链地址法解决冲突的方法是每个数组单元中存储的不再是单个数据,而是一个链条(常见的是数组或链表),以链表为例:每个数组单元存储这一个链表,一旦发现重复,将重复元素插入到链表的首段或末端。当查询时,先根据哈希化后的下标找到对应位置,再取出链表,依次查询数据。
对于数组单元中存储的链条,可以是数组,也可以是链表,在做查询时,二者效率差不多,都是线性查找。 -
开放地址法
开放地址法工作方式是寻找空白单元格来添加重读数据。在遇到重复元素的时候,会向后探测一定步长来放置这个重复元素。探索这个位置的方法有三种:线性探测,二次探测以及再哈希法。 -
线性探测
线性地查找空白单元。
在插入元素的时候发现这个单元已经有了其他元素,那么线性探测就是从index位置+1开始一点点查找合适的位置来放置这个重复元素。
查询元素时,首先经过哈希化得到index,比较index位置的元素和要查的元素是否相同,相同就返回,不相同,则线性查找,从index位置+1逐个查找,在查询过程中,如果查询到空位置,则停止(因为线性探测是在Index位置+1找空白单元格来放置重复元素的,在查找中遇到空位置就证明后续不可能会再放置重复元素了)。
删除元素的时候,不可以将这个位置的下标内容设置为null(因为设置为空会影响查找),可以将其进行特殊处理(例如设置成为-1)。
对于线性探测的问题:它容易产生聚集(一连串填充单元),聚集会影响哈希表的性能。 -
二次探测
在线性探测中,如果数据是连续插入的,新的待插入元素需要探测很长的距离找到合适的位置,为了解决这个问题,提出了二次探测,二次探测的主要优化是探测时的步长,线性探测可以看成步长为1的探测,比如从下标值x开始,那么线性探测就是x+1,x+2,x+3一次探测;对于二次探测,比如下标值从x开始,那么探测就是x+1^2 , x+2^2, x+3^2,这样可以一次性探测比较长的步长,避免了聚集的影响。
对于二次探测的问题,会产生步长不一的一种聚集 -
再哈希法
为了消除线性探测和二次探测中无论步长+1还是步长+平方的问题,给出了另一种解决方案:再哈希法。
再哈希法的做法:把关键字用另一个哈希函数做一次哈希化,用这次哈希化的结果作为步长。第二次哈希化的函数不能和第一次哈希化的函数相同(不然结果还是原来的位置),不能输出为0(将会没有步长,每次探测原地踏步,进入死循环),最后给出这个哈希函数:
stepSize=constant-(key%constant)
其中constant是质数,并且小于数组的容量
4.哈希化效率对比
填装因子
填装因子表示当前哈希表中已包含的数据项和整个哈希表长度的比值。
填装因子=总数据项/哈希表长度
平均探测长度及平均存取时间,取决于填装因子,随着填装因子变大,探测长度也越长。
开放地址发的填装因子最大为1,链地址法的填装因子可以大于1.
开放地址法哈希化效率
不管是线性探测还是二次探测,随着填装因子的增大,平均探测长度成指数增大,二次探测稍微好点。
链地址法哈希化效率
对于链地址法,随着填装因子的增大,平均探测长度成线性增大。所以链地址法相对于开放地址法来讲,效率要好。它的成功查找次数只需要查找链表的一半,不成功查找次数可能要将整个链表查完才知道。
5.实现哈希表
①哈希表的插入和修改
- 图解:
- 代码实现:
//插入或修改操作
HashTable.prototype.put=function(key,value){
//1.根据key求index值
var index=this.hashFunc(key,this.limit);
var bucket=this.storage[index];
//2.判断在index这个位置有没有bucket
if(bucket==null){
//3.没有就在index位置创建一个bucket,
var bucket=[];
this.storage[index]=bucket;
}
//4.判断是否为修改数据
for(var i=0;i<bucket.length;i++){
var tuple=bucket[i];
if(tuple[0]==key){
tuple[1]=value;
return true;
}
}
//5.遍历完bucket,发现不是修改,那么把key和value组成的数组插入到bucket末尾
bucket.push([key,value]);
//6.增加数据个数
this.count+=1;
}
②哈希表数据的获取操作
//获取操作
HashTable.prototype.get=function(key){
//1.获取对应的index
var index=this.hashFunc(key,this.limit);
//2.根据index查看对应位置的bucket存不存在
var bucket=this.storage[index];
if(bucket==null){
//如果不存在bucket,直接返回null
return null;
}
//3.如果存在bucket,遍历bucket,直到找到key
for(var i=0;i<bucket.length;i++){
var tuple=bucket[i];
if(tuple[0]==key){
return tuple[1];
}
}
//4.遍历完都没找到,返回null
return null;
}
③哈希表数据的删除操作
//删除操作
HashTable.prototype.remove=function(key){
//1.获取对应的index
var index=this.hashFunc(key,this.limit);
//2.根据index查看对应位置的bucket存不存在
var bucket=this.storage[index];
if(bucket==null){
//如果不存在bucket,直接返回null
return null;
}
//3.如果存在bucket,遍历bucket,直到找到key然后删除对应的数组
for(var i=0;i<bucket.length;i++){
var tuple=bucket[i];
if(tuple[0]==key){
bucket.splice(i,1);
this.count-=1;
return tuple[1];
}
}
//4.遍历完都没找到,返回null
return null;
}
④判断哈希表是不是空
//判断哈希表是不是空
HashTable.prototype.IsEmpty=function(){
return this.count?false:true;
}
⑤返回哈希表里的数据个数
//返回哈希表里的数据个数
HashTable.prototype.Size=function(){
return this.count;
}
6.哈希表的扩容
哈希表扩容思想
前面提到填装因子越大,越影响哈希表的效率,在链地址法实现哈希表中,随着插入的数据增多,每个index对应的bucket会越来越长,它的效率越低。因此,在合适的情况下对数组storage进行扩容。
当填装因子>0.75的时候进行扩容;当填装因子<0.25的时候进行缩容。
不管是扩容还是缩容,要保证哈希表容量恒为质数,因为只有是质数,才能在让数据分布更平均。
哈希表扩容实现
思路:让oldStorage指向原来的哈希表,再创建一个新的(扩容的)哈希表,把oldStorage所有的数据重新插入到新的哈希表中。
//哈希表的扩容
HashTable.prototype.resize=function(newLimit){
//1.保存旧的数组内容
var oldStorage=this.storage;
//2.重置所有属性
this.storage=[];
this.count=0;
this.limit=newLimit;
//3.遍历oldStorage所有的bucket
for(var i=0;i<oldStorage.length;i++){
var bucket=oldStorage[i];
//4.如果bucket为null,则跳过本轮循环
if(bucket==null){
continue;
}
//5.如果不为空,则遍历bucket
for(var j=0;j<bucket.length;j++){
var tuple=bucket[j];
this.put(tuple[0],tuple[1]);
}
}
}
}
具体使用扩容的地方是哈希表的插入,新增部分如下:
//判断是否要扩容
if(this.count>this.limit*0.75){
var newSize=this.limit*2;
newSize=this.GetPrime(newSize);
this.resize(newSize);
}
具体使用缩容的地方是哈希表的删除,新增部分如下:
//判断是否要缩小容量
if(this.limit>7&&this.count<this.limit*0.25){
var newSize=Math.floor(this.limit/2);
newSize=this.GetPrime(newSize);
this.resize(newSize);
}
寻找恒为质数的函数如下:
//判断一个数是不是质数
HashTable.prototype.IsPrime=function(num){
var n=parseInt(Math.sqrt(num));
for(var i=2;i<=n;i++){
if(n%i==0){
return false;
}
}
return true;
}
//获取最近的质数
HashTable.prototype.GetPrime=function(num){
while(this.IsPrime(num)==false){
num+=1;
}
return num;
}
最后附上哈希表的基本操作:
//哈希表
//采用链地址法来实现哈希表,每个单元存储的是数组
function HashTable(){
//属性
this.storage=[];//放置桶bucket
this.count=0;//已包含数据个数
this.limit=7;
//方法
//哈希函数
HashTable.prototype.hashFunc=function(str,size){
//1.定义hashCode
var hashCode=0;
//2.霍纳算法,计算hashCode
for(var i=0;i<str.length;i++){
hashCode=37*hashCode+str.charCodeAt(i);
}
//3.取余
hashCode=hashCode%size;
return hashCode;
}
//插入或修改操作
HashTable.prototype.put=function(key,value){
//1.根据key求index值
var index=this.hashFunc(key,this.limit);
var bucket=this.storage[index];
//2.判断在index这个位置有没有bucket
if(bucket==null){
//3.没有就在index位置创建一个bucket,
var bucket=[];
this.storage[index]=bucket;
}
//4.判断是否为修改数据
for(var i=0;i<bucket.length;i++){
var tuple=bucket[i];
if(tuple[0]==key){
tuple[1]=value;
return true;
}
}
//5.遍历完bucket,发现不是修改,那么把key和value组成的数组插入到bucket末尾
bucket.push([key,value]);
//6.增加数据个数
this.count+=1;
//判断是否要扩容,保证每次扩容后的容量恒为质数
if(this.count>this.limit*0.75){
var newSize=this.limit*2;
newSize=this.GetPrime(newSize);
this.resize(newSize);
}
}
//获取操作
HashTable.prototype.get=function(key){
//1.获取对应的index
var index=this.hashFunc(key,this.limit);
//2.根据index查看对应位置的bucket存不存在
var bucket=this.storage[index];
if(bucket==null){
//如果不存在bucket,直接返回null
return null;
}
//如果存在bucket,遍历bucket,直到找到key
for(var i=0;i<bucket.length;i++){
var tuple=bucket[i];
if(tuple[0]==key){
return tuple[1];
}
}
//遍历完都没找到,返回null
return null;
}
//删除操作
HashTable.prototype.remove=function(key){
//1.获取对应的index
var index=this.hashFunc(key,this.limit);
//2.根据index查看对应位置的bucket存不存在
var bucket=this.storage[index];
if(bucket==null){
//如果不存在bucket,直接返回null
return null;
}
//3.如果存在bucket,遍历bucket,直到找到key然后删除对应的数组
for(var i=0;i<bucket.length;i++){
var tuple=bucket[i];
if(tuple[0]==key){
bucket.splice(i,1);
this.count-=1;
//判断是否要缩小容量,保证容量恒为质数
if(this.limit>7&&this.count<this.limit*0.25){
var newSize=Math.floor(this.limit/2);
newSize=this.GetPrime(newSize);
this.resize(newSize);
}
return tuple[1];
}
}
//4.遍历完都没找到,返回null
return null;
}
//判断哈希表是不是空
HashTable.prototype.IsEmpty=function(){
return this.count?false:true;
}
//返回哈希表里的数据个数
HashTable.prototype.Size=function(){
return this.count;
}
//哈希表的扩容
HashTable.prototype.resize=function(newLimit){
//1.保存旧的数组内容
var oldStorage=this.storage;
//2.重置所有属性
this.storage=[];
this.count=0;
this.limit=newLimit;
//3.遍历oldStorage所有的bucket
for(var i=0;i<oldStorage.length;i++){
var bucket=oldStorage[i];
//4.如果bucket为null,则跳过本轮循环
if(bucket==null){
continue;
}
//5.如果不为空,则遍历bucket
for(var j=0;j<bucket.length;j++){
var tuple=bucket[j];
this.put(tuple[0],tuple[1]);
}
}
}
//判断一个数是不是质数
HashTable.prototype.IsPrime=function(num){
var n=parseInt(Math.sqrt(num));
for(var i=2;i<=n;i++){
if(n%i==0){
return false;
}
}
return true;
}
//获取最近的质数
HashTable.prototype.GetPrime=function(num){
while(this.IsPrime(num)==false){
num+=1;
}
return num;
}
}
//使用哈希表
var hashTable=new HashTable();
hashTable.put('abc',3);
hashTable.put('@@@',56);
hashTable.put('hello',3);
hashTable.put('?',13);
console.log(hashTable.get('abc'));//3
console.log(hashTable.get('@@@'));//56
console.log(hashTable.get('hello'));//3
console.log(hashTable.remove('?'));//13
console.log(hashTable.get('?'));//null
console.log(hashTable.IsEmpty());//false
console.log(hashTable.Size());//3
结果: