哈希表(Hash)又称“散列表”,在JavaScript中对象字面量的实现就是基于哈希表,哈希表是一种算法简单、性能优越的一种查找算法。
以JavaScript对象字面量为例:比如: 'name':'chn' 这一个键值对属性想插入对象中,首先需要根据键name计算出其对应的一个“编号”,而这个编号就是后面需要查找的地址编号,所以这个编号如何获取就比较的重要;当然有很多种办法,比如比较常用的根据字符的ASCII码进行相加得到一个编号;例如 'name' ASCII码相加就是:110+97+109+101=417,这就是name这个属性的地址编号;但是很快就会出现冲突的问题,比如ASCII码相加等于417的字符串不只name这一个吧?那比如下一次想插入一个同样编号为417的键但是键不是name的,就可以直接将name属性值覆盖了,这就不对了吧?
于是为了保证使用ASCII码得出来的地址编号能够唯一,可以采用幂的连乘;比如:name=110*37^3+97*37^2+109*37+101=5708757,得到一个比较大的值,而通过此方法得出来的值基本上是唯一的,那既然唯一性保证了,还差什么呢?差就差在得出来的数字太大了,一是不方便存储,二是很容易造成计算机的“溢出”,于是必须使用一种方法来压缩这个数,将他变成一个较小的数字,而压缩的方法就是哈希函数。
注意:幂的连乘对计算机来说是比较消耗性能的,可以根据霍纳法则,将公共的质数提取出来;
例如:110*37^3+97*37^2+109*37+101 简化成:((110*37+97)*37+109)*37+101
哈希函数:此方法是最常用的构造哈希函数方法,对于基础长度为size的哈希函数公式为:
f(key) = key % size (size为数组的长度,最好为质数)
假设数组长度是7,此时上面那个巨大的数5708757 % 7 = 5,得出来一个5就是最终的地址编号,将name存在数组下标为5的地方即可;
再比如:
我们使用上述公式,将这12个数字依次模除12,得到的下标如表1所示,此时你以为一切都风平浪静的时候,冲突发生了,比如我此时再插入一个key:18,18%12=6,下标为6,但是下标为6的已经存在78了,难不成覆盖?
甚至更极端点:
这些key模除12得到的下标全部是0,这不仅产生了冲突还产生了聚集。这对后面查找性能来说都是致命打击。
而这一切都是因为模除的size没有选好,上面公式说过了,size最好选取质数,就是这个原因,这也是那些大神多年总结的经验,如果我们将模除的size换成11,那么情况就好太多了:
所以我们就宁可将数组的长度选择一个质数。
解决冲突
如上述表3,我们即使选择了质数,但是模除得出来的下标仍然会出现相同的,1就出现了两次,这就说明:冲突不可避免,只能解决冲突。
解决冲突的办法有两种:
一、开放地址法
核心思想就是:当产生了同样的key时,就让第二个key去寻找下一个最近的空地址。
公式为:
f(key)=f(key+step) % size (setp=1,2,3,...,size-1)
比如:我们插入前五个:12、67、56、16、25
此时没有发生冲突,但是当我们插入到第六个数37,37%12=1,位置1存在了25,发生冲突,根据开放地址法公式:(37+1) % 12 = 2,即存入2,而且2刚好也是空地址,于是将其插入;但是如果2也名花有主了呢?那就(37+2)%12=3...依次类推,按照这种方法,一个一个去探测下一个地址是否为空称为“线性探测”
显然线性探测性能有限,总要一个一个去遍历,属实太麻烦。于是想出来了前后双向遍历:
f(key)=f(key+step) % size (step=1^2,-1^2,2^2,-2^2,3^2,...q^2,-q^2)
增加平方的运算使得关键字不聚集,这种方法叫做“二次探测”
当然还有“再哈希法”,核心公式就是:
step=Prime-(key% Prime)(Prime为一个小于数组长度的质数)
当产生了冲突的时候,就在准备一个哈希函数,对key在进行一次哈希求key,将这个key作为下一次探测的步长。
二、链地址法
核心思路是:产生冲突后,不寻炸新地址,而是在该地址后面开辟新地址存储,可以使用数组也可以使用链表。
如图:
形似拉链,实际上每个地址存储的都是每个链表的头结点或第一个结点的引用。
完整源码:
'use strict'
/*本次案例采用链地址法,所以需要用到链表,
在此之前已经封装过一个链表,在此可以直接导入,
获取链表源码请前往JavaScript数据结构之链表*/
import LinkList from "./test.js"
//封装哈希函数
const HashTable=function(){
const BASE=37 //定义幂的连乘底数,最好选取质数,一般算法都选取37
function HashTable(){
//数组总长-质数
this.size=7;
//当前数组个数
this.count=0;
//动态分配数组空间
this.table=new Array(this.size);
}
//哈希函数
HashTable.prototype.Hash=function(key,length){
//如果key是数字
if (typeof key === 'number') {
return key % length;
}
let code = 0;
for (let i = 0; i < key.length; i++) {
code = code * BASE + key.charCodeAt(i);
}
return code % length;
}
//插入
HashTable.prototype.insertElem=function(key,value){
//根据key获取地址
const addr=this.Hash(key,this.size);
//判断该位置是否为空
const data=this.table[addr];
if(!data){ //如果为空,尾插法插入
const newNode=new LinkList();
this.table[addr]=newNode;
//规定链表存放的数据域是一个存储键值对数组,0为key,1为value
newNode.insertListByTail([key,value]);
this.count++;
if ((this.count / this.size).toFixed(2) >= 0.75) {
let newSize = this.size * 2;
newSize=this.returnPrime(newSize);
this.extendTable(newSize);
}
return;
}
/*如果该位置上存在数据
1.key已经存在,则对key的value进行更新
2.key不存在,直接尾插
*/
let p=this.table[addr].head;
// while(p){ ---可忽略
// if(p.data[0]===key){ ---可忽略
// p.data[1]=value; ---可忽略
// return; ---可忽略
// } ---可忽略
// p=p.next; ---可忽略
// } ---可忽略
//重新用findNode方法封装寻找结点的部分代码
const res=this.findNode(function(p1){
if (p1.data[0] === key) {
p1.data[1] = value;
return 'update';
}
},p);
if(res==='update')
return;
data.insertListByTail([key,value]);
this.count++;
//判断填充因子,是否需要扩容
if((this.count/this.size).toFixed(2)>=0.75){
let newSize=this.size*2;
newSize=this.returnPrime(newSize);
this.extendTable(newSize);
}
}
//以键取值
HashTable.prototype.getElem=function(key){
const addr=this.Hash(key,this.size);
if(!this.table[addr]){
return null;
}
let p=this.table[addr].head;
// while(p){ ---可忽略
// if(p.data[0]===key){ ---可忽略
// return p.data[1]; ---可忽略
// }
// p=p.next; ---可忽略
// }
const node=this.findNode(function(p1){
// console.log(p1);
if(p1.data[0]===key){
return p1.data[1];
}
},p);
return node;
}
//删除
HashTable.prototype.deleteElem=function(key){
const addr=this.Hash(key,this.size);
if(!this.table[addr]){
console.log(new Error('不能删除一个不存在的值'));
return;
}
let p=this.table[addr].head;
let Pos=1;
while(p){
if(p.data[0]===key){
this.table[addr].deleteData(Pos);
//如果对应的头结点为空,则删除该位置的链表引用
if(!this.table[addr].head){
delete this.table[addr];
}
this.count--;
//判断填充因子是否需要将数组减半
if((this.count/this.size).toFixed(2)<=0.25 && this.size > 7){
let newSize = Math.ceil(this.size/2);
newSize=this.returnPrime(newSize);
this.extendTable(newSize);
}
return true;
}
Pos++;
p=p.next;
}
console.log(new Error('不能删除一个不存在的值'));
return;
}
/**
* 工厂方法-查询节点
* fn为业务逻辑 @type function
* p为结点、Pos为结点位置
* */
HashTable.prototype.findNode = function (fn, p,Pos=1) {
let res;
while (p) {
res=fn.call(this,p);
if(res){
return res;
}
p=p.next;
}
}
/**重要:
* 对基础数组进行扩容
* 填充因子=当前数组个数/数组总长
* 当填充因子>0.75 此时线性查找效率很低,必须扩容
* 当填充因子<0.25 此时如果数组空间很大而实际插入的数据很少就会造成空间浪费,此时必须缩小个数
* */
HashTable.prototype.extendTable=function(size){
const temp = this.table;
this.table = [];
this.count = 0;
this.size = size;
temp.forEach(item => {
let p = item.head;
while (p) {
this.insertElem(p.data[0], p.data[1]);
p = p.next;
}
});
}
//判断质数
HashTable.prototype.isPrime=function(data){
const _data=Math.ceil(Math.sqrt(data));
for(let i=2;i<=_data;i++){
if(data%i===0){
return false;
}
}
return true;
}
//返回合适的质数
HashTable.prototype.returnPrime=function(data){
while(!this.isPrime(data)){
data++;
}
return data;
}
return HashTable;
}();