哈希表通常是基于数组实现的
- 哈希表可以提供非常快速的插入-删除-查找操作;
- 无论多少数据,插入和删除值都只需要非常短的时间,即O(1)的时间级。实际上,只需要几个机器指令即可完成;
- 哈希表的速度比树还要快,基本可以瞬间查找到想要的元素。但是相对于树来说编码要简单得多。
- 哈希表中的数据是没有顺序的,所以不能以一种固定的方式(比如从小到大 )来遍历其中的元素。
- 通常情况下,哈希表中的key是不允许重复的,不能放置相同的key,用于保存不同的元素。
- 哈希表并不好理解,不像数组、链表和树等可通过图形的形式表示其结构和原理。
- 哈希表的结构就是数组,但它神奇之处在于对下标值的一种变换,这种变换我们可以称之为哈希函数,通过哈希函数可以获取HashCode。
将大数字转化成数组范围内的下标【小数化】的过程。
将单词转化成大数字,大数字在进行哈希化的代码实现放在一个函数中,这个函数称之为哈希函数。
最终将数据插入到的这个数组【这个数组基本上比实际需求的大】,对整个结构的封装, 称之为一个哈希表。
案例一:公司想要存储1000个人的信息,每一个工号对应一个员工的信息。若使用数组,增删数据时比较麻烦;使用链表,获取数据时比较麻烦。有没有一种数据结构,能把某一员工的姓名转换为它对应的工号,再根据工号查找该员工的完整信息呢?没错此时就可以使用哈希表的哈希函数来实现。
案例二:存储联系人和对应的电话号码:当要查找张三(比如)的号码时,若使用数组:由于不知道存储张三数据对象的下标值,所以查找起来十分麻烦,使用链表时也同样麻烦。而使用哈希表就能通过哈希函数把张三这个名称转换为它对应的下标值,再通过下标值查找效率就非常高了。
也就是说:哈希表最后还是基于数据来实现的,只不过哈希表能够通过哈希函数把字符串转化为对应的下标值,建立字符串和下标值的对应关系。
- 哈希化过后的下标依然可能重复,如何解决这个问题呢?这种情况称为冲突,冲突是不可避免的,我们只能解决冲突。
我们将每一个数字都对10进行取余操作,则余数的范围0~9作为数组的下标值。并且,数组每一个下标值对应的位置存储的不再是一个数字了,而是存储由经过取余操作后得到相同余数的数字组成的数组或链表。
这样可以根据下标值获取到整个数组或链表,之后继续在数组或链表中查找就可以了。而且,产生冲突的元素一般不会太多。
总结:链地址法解决冲突的办法是每个数组单元中存储的不再是单个数据,而是一条链条,这条链条常使用的数据结构为数组或链表,两种数据结构查找的效率相当(因为链条的元素一般不会太多)。
开放地址法的主要工作方式是寻找空白的单元格来放置冲突的数据项。
根据探测空白单元格位置方式的不同,可分为三种方法:
- 线性探测
- 二次探测
- 再哈希法
线性探测插入数据:
线性探测查询数据:
线性探测删除数据:
线性探测的问题:
二次探测的问题:
第二次哈希化需要具备以下特点:
可以看到,随着装填因子的增大,平均探测长度呈指数形式增长,性能较差。实际情况中,最好的装填因子取决于存储效率和速度之间的平衡,随着装填因子变小,存储效率下降,而速度上升。
二次探测和再哈希法性能相当,它们的性能比线性探测略好。由下图可知,随着装填因子的变大,平均探测长度呈指数形式增长,需要探测的次数也呈指数形式增长,性能不高。
可以看到随着装填因子的增加,平均探测长度呈线性增长,较为平缓。在开发中使用链地址法较多,比如Java中的HashMap中使用的就是链地址法。
- 快速的计算;
- 均匀的分布;
霍纳法则
求多项式的值时,首先计算最内层括号内一次多项式的值,然后由内向外逐层计算一次多项式的值。这种算法把求n次多项式f(x)的值就转化为求n个一次多项式的值。
- 乘法次数:n(n+1)/2次;
- 加法次数:n次;
- 乘法次数:n次;
- 加法次数:n次;
如果使用大O表示时间复杂度的话,直接从变换前的O(N2)降到了O(N)。
(1)均匀的分布
(2)质数的使用
// 设计哈希函数// 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
哈希表的插入和修改操作是同一个函数:因为,当使用者传入一个
时,如果原来不存在该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);
- 首先,根据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'));
- 首先,根据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'));
//判断哈希表是否为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);
- 前面我们在哈希表中使用的是长度为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
//判断是否需要扩容操作if(this.count > this.limit * 0.75){this.resize(this.limit * 2)}
//缩小容量if (this.limit > 7 && this.count < this.limit * 0.25) {this.resize(Math.floor(this.limit / 2))}
注意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}
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;}
2倍扩容之后,通过循环调用isPrime判断得到的容量是否为质数,不是则+1,直到是为止。比如原长度:7,2倍扩容后长度为14,14不是质数,14 + 1 = 15不是质数,15 + 1 = 16不是质数,16 + 1 = 17是质数,停止循环,由此得到质数17。
//判断传入的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方法中添加如下代码:
//判断是否需要扩容操作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