JavaScript【哈希表】
创始人
2025-05-29 10:41:37

一、哈希表的认识

哈希表通常是基于数组实现的

1.哈希表的优势:

  • 哈希表可以提供非常快速的插入-删除-查找操作
  • 无论多少数据,插入和删除值都只需要非常短的时间,即O(1)的时间级。实际上,只需要几个机器指令即可完成;
  • 哈希表的速度比树还要快,基本可以瞬间查找到想要的元素。但是相对于树来说编码要简单得多。

2.哈希表的不足

  • 哈希表中的数据是没有顺序的,所以不能以一种固定的方式(比如从小到大 )来遍历其中的元素。
  • 通常情况下,哈希表中的key是不允许重复的,不能放置相同的key,用于保存不同的元素。

3.哈希表是什么?

  • 哈希表并不好理解,不像数组、链表和树等可通过图形的形式表示其结构和原理。
  • 哈希表的结构就是数组,但它神奇之处在于对下标值的一种变换,这种变换我们可以称之为哈希函数,通过哈希函数可以获取HashCode

4.相关概念

1.哈希化

大数字转化成数组范围内的下标【小数化】的过程。

2.哈希函数

单词转化成大数字,大数字在进行哈希化的代码实现放在一个函数中,这个函数称之为哈希函数

3.哈希表

最终将数据插入到的这个数组【这个数组基本上比实际需求的大】,对整个结构的封装, 称之为一个哈希表

 5.通过以下案例了解哈希表:

  • 案例一:公司想要存储1000个人的信息,每一个工号对应一个员工的信息。若使用数组,增删数据时比较麻烦;使用链表,获取数据时比较麻烦。有没有一种数据结构,能把某一员工的姓名转换为它对应的工号,再根据工号查找该员工的完整信息呢?没错此时就可以使用哈希表的哈希函数来实现。

  • 案例二:存储联系人和对应的电话号码:当要查找张三(比如)的号码时,若使用数组:由于不知道存储张三数据对象的下标值,所以查找起来十分麻烦,使用链表时也同样麻烦。而使用哈希表就能通过哈希函数把张三这个名称转换为它对应的下标值,再通过下标值查找效率就非常高了。

  • 案例三:50000个单词 

 也就是说:哈希表最后还是基于数据来实现的,只不过哈希表能够通过哈希函数把字符串转化为对应的下标值建立字符串和下标值的对应关系

6.将单词转为数字

1.方法一:数字相加

2.方法二:幂的连乘 

3.两种方案总结 

 

仍然需要解决的问题

  • 哈希化过后的下标依然可能重复,如何解决这个问题呢?这种情况称为冲突,冲突是不可避免的,我们只能解决冲突

二、冲突

1.什么是冲突

2.解决冲突的方法一:链地址法(拉链法)

我们将每一个数字都对10进行取余操作,则余数的范围0~9作为数组的下标值。并且,数组每一个下标值对应的位置存储的不再是一个数字了,而是存储由经过取余操作后得到相同余数的数字组成的数组链表

 这样可以根据下标值获取到整个数组或链表,之后继续在数组或链表中查找就可以了。而且,产生冲突的元素一般不会太多。

总结:链地址法解决冲突的办法是每个数组单元中存储的不再是单个数据,而是一条链条,这条链条常使用的数据结构为数组或链表,两种数据结构查找的效率相当(因为链条的元素一般不会太多)。

3.解决冲突的方法二:开放地址法 

开放地址法的主要工作方式是寻找空白的单元格来放置冲突的数据项。

 根据探测空白单元格位置方式的不同,可分为三种方法:

  • 线性探测
  • 二次探测
  • 再哈希法

1.线性探测法

线性探测插入数据:

线性探测查询数据:

线性探测删除数据:

 线性探测的问题:

2.二次探测【在线性探测基础上优化】 

二次探测的问题:

 3.再哈希法:

第二次哈希化需要具备以下特点:

 4.哈希化的效率

 装填因子:

 线性探测效率:

可以看到,随着装填因子的增大,平均探测长度呈指数形式增长,性能较差。实际情况中,最好的装填因子取决于存储效率和速度之间的平衡,随着装填因子变小,存储效率下降,而速度上升。

二次探测/再哈希法效率:

二次探测和再哈希法性能相当,它们的性能比线性探测略好。由下图可知,随着装填因子的变大,平均探测长度呈指数形式增长,需要探测的次数也呈指数形式增长,性能不高。

 链地址法效率:

可以看到随着装填因子的增加,平均探测长度呈线性增长,较为平缓。在开发中使用链地址法较多,比如Java中的HashMap中使用的就是链地址法

5.哈希函数 

1.特点

2.性能高的哈希函数的优点:

  • 快速的计算
  • 均匀的分布

 3.优化方法

霍纳法则

求多项式的值时,首先计算最内层括号内一次多项式的值,然后由内向外逐层计算一次多项式的值。这种算法把求n次多项式f(x)的值就转化为求n个一次多项式的值。

变换之前

  • 乘法次数:n(n+1)/2次;
  • 加法次数:n次;

变换之后:

  • 乘法次数:n次;
  • 加法次数:n次;

如果使用大O表示时间复杂度的话,直接从变换前的O(N2)降到了O(N)

4.均匀分布

(1)均匀的分布

(2)质数的使用

5.哈希表的长度 

三、哈希函数的实现

哈希函数的设计

    // 设计哈希函数// 1.将字符串转成比较大的数字:hashCode// 2.将大的数字hashCode压缩到数组范围(大小)之内function hashFunc(str, size) {// 1.定义hashCode变量,记录转换成较大的数字var hashCode = 0// 2.霍纳算法,来计算hashCode的值// cats->Unicode编码for (var i = 0; i < str.length; i++) {// hashCode一般都是选择为素数hashCode = 37 * hashCode + str.charCodeAt(i)}// 3.取余操作var index = hashCode % sizereturn index}

测试代码

    //测试哈希函数console.log(hashFunc('123', 7));  //4console.log(hashFunc('NBA', 7));  //5console.log(hashFunc('CBA', 7));  //3console.log(hashFunc('CMF', 7));  //2

四、哈希表的封装

(1)实现方法:链地址法

(2)代码解析:

  

(3)哈希表的插入&修改——>put(key,value)

哈希表的插入和修改操作是同一个函数:因为,当使用者传入一个时,如果原来不存在该key,那么就是插入操作,如果原来已经存在该key,那么就是修改操作。

实现思路:

  • 首先,根据key获取索引值index,目的为将数据插入到storage的对应位置;
  • 然后,根据索引值取出bucket,如果bucket不存在,先创建bucket,随后放置在该索引值的位置;
  • 接着,判断新增还是修改原来的值。如果已经有值了,就修改该值;如果没有,就执行后续操作。
  • 最后,进行新增数据操作。

代码实现

      // 插入和修改操作HashTable.prototype.put = function (key, value) {// 1.根据key获取对应的indexvar index = this.hashFunc(key, this.limit)// 2.根据index取出对应bucketvar bucket = this.storage[index]// 3.判断该bucket是否为nullif (bucket == null) {// 给空的bucket中添加一个新的数组bucket = []// 将新产生的数组放入bucket(桶中)this.storage[index] = bucket}// 4.判断是否修改数据for (var i = 0; i < bucket.length; i++) {// 将遍历到的数据赋值给tuplevar tuple = bucket[i]// 对桶中有的数据进行查看,是否已经存在// tuple[0]就是指向keyif (tuple[0] == key) {// tuple[1]就是指valuetuple[1] = valuereturn}}// 5.进行添加操作bucket.push([key,value])this.count+=1// 6.判断是否需要扩容操作if (this.count > this.limit * 0.75) {this.resize(this.limit * 2)}}

代码测试

    //测试哈希表//1.创建哈希表let ht = new HashTable()//2.插入数据ht.put('class1','Tom')ht.put('class2','Mary')ht.put('class3','Gogo')ht.put('class4','Tony')ht.put('class4', 'Vibi')console.log(ht);

(4)哈希表的获取——>get(key)

实现思路

  • 首先,根据key通过哈希函数获取它在storage中对应的索引值index;
  • 然后,根据索引值获取对应的bucket;
  • 接着,判断获取到的bucket是否为null,如果为null,直接返回null;
  • 随后,线性遍历bucket中每一个key是否等于传入的key。如果等于,直接返回对应的value;
  • 最后,遍历完bucket后,仍然没有找到对应的key,直接return null即可。

代码实现

   //获取操作HashTable.prototype.get = function(key){//1.根据key获取对应的indexlet index = this.hashFunc(key, this.limit)//2.根据index获取对应的bucketlet bucket = this.storage[index]//3.判断bucket是否等于nullif (bucket == null) {return null}//4.有bucket,那么就进行线性查找for (let i = 0; i < bucket.length; i++) {let tuple = bucket[i];//tuple[0]存储key,tuple[1]存储valueif (tuple[0] == key) {return tuple[1]}}//5.依然没有找到,那么返回nullreturn null}

测试代码

    //测试哈希表//1.创建哈希表let ht = new HashTable()//2.插入数据ht.put('class1','Tom')ht.put('class2','Mary')ht.put('class3','Gogo')ht.put('class4','Tony')//3.获取数据console.log(ht.get('class3'));console.log(ht.get('class2'));console.log(ht.get('class1'));

(5)哈希表的删除操作——>remove(key)

实现思路

  • 首先,根据key通过哈希函数获取它在storage中对应的索引值index;
  • 然后,根据索引值获取对应的bucket;
  • 接着,判断获取到的bucket是否为null,如果为null,直接返回null;
  • 随后,线性查找bucket,寻找对应的数据,并且删除;
  • 最后,依然没有找到,返回null;

代码实现

   //删除操作HashTable.prototype.remove = function(key){//1.根据key获取对应的indexlet index = this.hashFunc(key, this.limit)//2.根据index获取对应的bucketlet bucket = this.storage[index]//3.判断bucket是否为nullif (bucket == null) {return null}//4.有bucket,那么就进行线性查找并删除for (let i = 0; i < bucket.length; i++) {let tuple = bucket[i]if (tuple[0] == key) {// splice删除元素(下标值,删除几位数)bucket.splice(i,1)this.count -= 1 return tuple[1]// 缩小容量if(this.limit>7 && this.count 

代码测试

    //测试哈希表//1.创建哈希表let ht = new HashTable()//2.插入数据ht.put('class1', 'Tom')ht.put('class2', 'Mary')ht.put('class3', 'Gogo')ht.put('class4', 'Tony')//3.删除数据console.log(ht.remove('class2'));

(6)isEmpty()、size()

代码实现

//判断哈希表是否为nullHashTable.prototype.isEmpty = function(){return this.count == 0}//获取哈希表中元素的个数HashTable.prototype.size = function(){return this.count}

测试代码

    //测试哈希表//1.创建哈希表let ht = new HashTable()//2.插入数据ht.put('class1','Tom')ht.put('class2','Mary')ht.put('class3','Gogo')ht.put('class4','Tony')//3.测试isEmpty()console.log(ht.isEmpty());//4.测试isEmpty()console.log(ht.size());console.log(ht);

五、哈希表的扩容

1.扩容与压缩

为什么需要扩容?

  • 前面我们在哈希表中使用的是长度为7的数组,由于使用的是链地址法,装填因子(loadFactor=当前哈希表中的数值/可以存放的数值个数)可以大于1,所以这个哈希表可以无限制地插入新数据。
  • 但是,随着数据量的增多,storage中每一个index对应的bucket数组(链表)就会越来越长,这就会造成哈希表效率的降低

什么情况下需要扩容?

  • 常见的情况是loadFactor > 0.75的时候进行扩容;

如何进行扩容?

  • 简单的扩容可以直接扩大两倍(关于质数,之后讨论);
  • 扩容之后所有的数据项都要进行同步修改;【之前插入的数据要重新插入】

实现思路:

 

  • 首先,定义一个变量,比如oldStorage指向原来的storage;
  • 然后,创建一个新的容量更大的数组,让this.storage指向它;
  • 最后,将oldStorage中的每一个bucket中的每一个数据取出来依次添加到this.storage指向的新数组中;

代码实现

在插入函数中添加一个判断是否要进行扩容

            // 插入和修改操作HashTable.prototype.put = function (key, value) {// 1.根据key获取对应的indexvar index = this.hashFunc(key, this.limit)// 2.根据index取出对应bucketvar bucket = this.storage[index]// 3.判断该bucket是否为nullif (bucket == null) {// 给空的bucket中添加一个新的数组bucket = []// 将新产生的数组放入bucket(桶中)this.storage[index] = bucket}// 4.判断是否修改数据for (var i = 0; i < bucket.length; i++) {// 将遍历到的数据赋值给tuplevar tuple = bucket[i]// 对桶中有的数据进行查看,是否已经存在// tuple[0]就是指向keyif (tuple[0] == key) {// tuple[1]就是指valuetuple[1] = valuereturn}}// 5.进行添加操作bucket.push([key, value])this.count += 1// 6.判断是否需要扩容操作if(this.count>this.limit*0.75){this.resize(this.limit*2)}}    // 哈希表的扩容HashTable.prototype.resize=function(newLimit){// 1.保留旧的数组内容var oldStorage=this.storage// 2.重置所有的属性this.storage=[]this.count=0;this.limit=newLimit// 3.遍历oldStorage中所有的bucketfor(var i=0;i

注意点:

上述定义的哈希表的resize方法,既可以实现哈希表的扩容,也可以实现哈希表容量的压缩

装填因子 = 哈希表中数据 / 哈希表长度,即 loadFactor = count / HashTable.length

  • 通常情况下当装填因子laodFactor > 0.75时,对哈希表进行扩容。在哈希表中的添加方法(push方法)中添加如下代码,判断是否需要调用扩容函数进行扩容:
     //判断是否需要扩容操作if(this.count > this.limit * 0.75){this.resize(this.limit * 2)}
  • 装填因子laodFactor < 0.25时,对哈希表容量进行压缩。在哈希表中的删除方法(remove方法)中添加如下代码,判断是否需要调用扩容函数进行压缩:
    //缩小容量if (this.limit > 7 && this.count < this.limit * 0.25) {this.resize(Math.floor(this.limit / 2))}

2.选择质数作为容量

判断质数的方法:

注意1不是质数

  • 方法一:针对质数的特点:只能被1和num整除,不能被2 ~ (num-1)整除。遍历2 ~ (num-1) 
    function isPrime(num){if(num <= 1 ){return false} for(let i = 2; i <= num - 1; i++){if(num % i ==0){return false}}return true}
  • 方法二:只需要遍历2 ~ num的平方根即可。
   function isPrime(num){if (num <= 1) {return false}//1.获取num的平方根:Math.sqrt(num)//2.循环判断for(var i = 2; i<= Math.sqrt(num); i++ ){if(num % i == 0){return false;}}return true;}

3.实现扩容后的哈希表容量为质数

实现思路:

2倍扩容之后,通过循环调用isPrime判断得到的容量是否为质数,不是则+1,直到是为止。比如原长度:7,2倍扩容后长度为14,14不是质数,14 + 1 = 15不是质数,15 + 1 = 16不是质数,16 + 1 = 17是质数,停止循环,由此得到质数17。

代码实现:

  • 第一步:首先需要为HashTable类添加判断质数的isPrime方法和获取质数的getPrime方法:
  //判断传入的num是否质数HashTable.prototype.isPrime = function(num){if (num <= 1) {return false}//1.获取num的平方根:Math.sqrt(num)//2.循环判断for(var i = 2; i<= Math.sqrt(num); i++ ){if(num % i == 0){return false;}}return true;}//获取质数的方法HashTable.prototype.getPrime = function(num){//7*2=14,+1=15,+1=16,+1=17(质数)while (!this.isPrime(num)) {num++}return num}
  • 第二步:修改添加元素的put方法和删除元素的remove方法中关于数组扩容的相关操作:

在put方法中添加如下代码:

      //判断是否需要扩容操作if(this.count > this.limit * 0.75){let newSize = this.limit * 2let newPrime = this.getPrime(newSize)this.resize(newPrime)}

在remove方法中添加如下代码:

          //缩小容量if (this.limit > 7 && this.count < this.limit * 0.25) {let newSize = Math.floor(this.limit / 2)let newPrime = this.getPrime(newSize)this.resize(newPrime)}

六、完整代码


Document

相关内容

热门资讯

cad打印线条粗细设置 cad... 004-线型(下)打印样式设置和线型文件使用一、线宽设置方法制图规范里边的线宽要求,我们已经定义好,...
荼蘼什么意思 岁月缱绻葳蕤生香... 感谢作者【辰夕】的原创独家授权分享编辑整理:【多肉植物百科】百科君坐标:云南 曲靖春而至,季节流转,...
应用未安装解决办法 平板应用未... ---IT小技术,每天Get一个小技能!一、前言描述苹果IPad2居然不能安装怎么办?与此IPad不...
阿西吧是什么意思 阿西吧相当于... 即使你没有受到过任何外语培训,你也懂四国语言。汉语:你好英语:Shit韩语:阿西吧(아,씨발! )日...
长白山自助游攻略 吉林长白山游... 昨天介绍了西坡的景点详细请看链接:一个人的旅行,据说能看到长白山天池全凭运气,您的运气如何?今日介绍...