哈希表通常是基于数组进行实现的,但是相对于数组,它也很多的优势和不足.
优势:
不足:
将大数字转化成数组范围内下标的过程,我们就称之为哈希化.
通常我们会将字符串转成大数字,大数字在进行哈希化的代码实现放在一个i
最终将数据插入到的这个数组,对整个结构的封装,我们就称之为是一个哈希表
元素通过哈希化了获得的数组下标跟其他的已有的元素下标重复,就叫做冲突
常见的解决冲突的方法有两种:
每个下标处存储一个链表或数组,当获取到下标值后,在遍历链表/数组依次查找元素
效率上也差不多
这种情况最好采用链表
,因为数组在首位插入数据是需要所有其他项后移的,链表就没有这样的问题开放地址法主要工作方式是寻找空白的单元格来添加重复的数据
寻找空白位置有三种方法:
线性探测即线性的查找空白的单元格
插入32
查询32
空位置,就停止
.删除32
影响我们之后查询其他操作
,所以通常删除一个位置的数据项
时,我们可以将它进行特殊处理
(比如设置为-1).继续查询
,但是插入时这个位置可以放置数据.解释:
因为按照习惯我们在查询的一般都是遇到了空位置就停止查询了,那么如果我们要查询元素的前一个位置的内容之前被删掉,就会影响这个元素的查找
线性探测的问题
线性探测有一个比较严重的问题,就是聚集.什么是聚集呢?
探测时的步长
,什么意思呢?线性探测
,我们可以看成是步长为1的探测,比如从下标值x开始,那么线性测试就是x+1,x+2,x+3依次探测二次探测
,对步长做了优化,比如从下标值x开始,x+12,x+23,x+33.一次性探测比较长的距离
,比避免那些聚集带来的影响.为了消除线性探测和二次探测中无论步长+1还是步长+平法中存在的问题,我们可以使用再哈希法
再哈希法:
二次探测的算法产生的探测序列步长是固定的:1,4,9,16,依次类推
现在需要一种方法:产生一种依赖关键字的探测序列,而不是每个关键字都一样
那么不同的关键字即使映射到相同的数组下标,也可以使用不同的探测序列
再哈希法的做法就是:把关键字用另外一个哈希函数,再做一次哈希化,用这次哈希化的结果作为步长
对于指定的关键字,步长在整个探测中是不变的,不过不同的关键字使用不同的步长
第二次哈希化需要具备如下特点:
stepSize = constant - (key - constant)
stepSize = 5 -(key % 5)
,满足需求,并且结果不可能为0.哈希表中执行插入和搜索操作效率是非常高的
如果没有产生冲突
,那么效率就会更高。如果发生冲突
,存取时间就依赖后来的探测长度
。
平均探测长度以及平均存取时间,取决于填装因子
,随着填装因子变大,探测长度也越来越长。
随着填装因子变大,效率下降的情况,在不同开放地址法方案中比链地址法更严重,所以我们来对比一下他们的效率,再决定我们选取的方案.
在分析效率之前,我们先了解一个概念:装填因子
.
包含的数据项
和整个哈希表长度
的比值
装填因子=总数据项/哈希表长度
开放地址法的装填因子最大是1
,因为它必须寻找到空白的单元才能将元素放入,总数据项最多等于哈希表长度链地址法的装填因子可以大于1
,因为拉链法可以无限的延伸下去,只要你愿意.(当然后面效率就变低了)下面的等式显示了线性探测时,探测序列§和填装因子(L)的关系
对成功的查找:P=(1+1/(1-L)^2)/2
对不成功的查找:P=(1+1/(1-L))/2
图片解析.:
当填装因子是1/2时,成功的搜索需要1.5次比较,不成功的搜索需要2.5次
当填装因子为2/3时,分别需要2.0次和5.0次比较
如果填装因子更大,比较次数会非常大。
应该使填装因子保持在2/3以下,最好在1/2以下,另一方面,填装因子越低,对于给定数量的数据项,就需要越多的空间。
实际情况中,最好的填装因子取决于存储效率和速度之间的平衡,随着填装因子变小,存储效率下降,而速度上升。
二次探测和再哈希法的性能相当。它们的性能比线性探测略好。
对成功的搜索,公式是: -log2(1 - loadFactor) / loadFactor
对于不成功的搜搜,公式是:1/(1-loadFactor)
图片解析:
当填装因子是0.5时,成功和不成的查找平均需要2次比较
当填装因子为2/3时,分别需要2.37和3.0次比较
当填装因子为0.8时,分别需要2.9和5.0次
因此对于较高的填装因子,对比线性探测,二次探测和再哈希法还是可以忍受的。
链地址法的效率分析有些不同,一般来说比开放地址法简单,效率也比开放地址法好。我们来分析一下这个公式应该是怎么样的
arraySize
个数据项,每个数据项有一个链表,在表中一共包含N个数据项N / arraySize
个数据项怎么求出查找成功和不成功的次数:
所以在真实开发中,使用链地址法的情况较多
快速的计算
hashCode
非常重要.hashCode
均匀的分布
让哈希函数中尽量减少乘法和除法
,因为它们的性能是比较低的。
使用霍纳法则
优化多项式,减少乘法和除法
- 未优化前: a(n)x^n+a(n-1)x^(n-1)+...a(1)x+a(0); + 乘法次数:n+(n-1)+...+1=n(n+1)/2 + 加法次数:n次 + 时间复杂度:O(n^2) - 优化后 Pn(x)=anx^n+a(n-1)x^(n-1)+...+a1x+a0= ((...(((anx+an-1)x+an-2)x+an-3)...)x+a1)x+a0 + 乘法次数:n次 + 加法次数:n次 + 时间复杂度:O(n)
理映射到相同下标值
的情况:链地址法或者开放地址法均匀分布
使用常量的地方
,尽量使用质数
.质数的使用: (质数是指在大于1的自然数中,除了1和它本身以外不再有其他因数的自然数。)
**思考:**为什么他们使用质数.会让哈希表分布更加均匀呢?
// 实现哈希函数 // 1. 将字符串转换为较大的数字:hashCode // 2. 将数字hashCode压缩到数组范围之内 function hashfun(str,size){ var hashCode = 0; // 霍纳法则公式 for (let i = 0; i < str.length; i++) { // string.charCodeAt(index): 返回字符串index位字符的Unicode 编码 // 相当于霍纳法则里的:anx+an-1 // ((...(((anx+an-1)x+an-2)x+an-3)...)x+a1)x+a0 // 37是一个质数,也可以写其他质数,质数比较常用37 hashCode = 37 * hashCode + str.charCodeAt(i); console.log(str.charCodeAt(i)); } // 取余 var index = hashCode % size; return index; }
我们这里采用链地址法
来实现哈希表:
代码解析
我们定义了三个属性:
storage
作为我们的数组,数组中存放相关的元素count
表示当前已经存在了多少数据limit
用于标记数组中一共可以存放多少个元素.function Hash(){ var storage = []; var count = 0; var limit = 7; }
实现哈希函数
// 实现哈希函数 // 1. 将字符串转换为较大的数字:hashCode // 2. 将数字hashCode压缩到数组范围之内 function hashfun(str,size){ var hashCode = 0; // 霍纳法则公式 for (let i = 0; i < str.length; i++) { // string.charCodeAt(index): 返回字符串index位字符的Unicode 编码 // 相当于霍纳法则里的:anx+an-1 // ((...(((anx+an-1)x+an-2)x+an-3)...)x+a1)x+a0 // 37是一个质数,也可以写其他质数,质数比较常用37 hashCode = 37 * hashCode + str.charCodeAt(i); console.log(str.charCodeAt(i)); } // 取余 var index = hashCode % size; return index; }
哈希表的插入和修改操作是同一个函数:
代码解析
hashCode
,也就是数组的index
index
位置中取出桶(另外一个数组)bucket
是否为null
nul
, 表示之前在该位置没有放置过任何的内容,那么就新建一个数组key
对应的value
override
来记录是否是修改操作bucket
中push
新的[key value]
即可count+1
,因为数据增加了一项// 插入&修改操作 HashTable.prototype.put = function(key, value){ // 1. 根据key获取对应的hashcode,也就是数组的下标 var index = this.hashfun(key, this.limit); // 2. 根据index取出对应的bucket var bucket = this.storage[index]; // 3. 查看`bucket`是否为`null` if (bucket == null) { // 如果为空,就创建一个数组,存放到index位置 bucket = []; this.storage[index] = bucket; } // 4. 判断是否是修改数据 for (let i = 0; i < bucket.length; i++) { // if (bucket[i][0] == key) { // bucket[i][1] = value; // }else if (bucket[i] == null) { // bucket[i][0] = key; // bucket[i][1] = value; // bucket.push([key,value]); // } // return // 定义一个数组存放bucket里的元素 var tuple = bucket[i]; if (tuple[0] == key) { tuple[1] = value; return; } } // 添加 bucket.push([key,value]); this.count += 1; }
// 获取操作 HashTable.prototype.get = function(key){ // 1. 通过传来的key获取hashCode,查找到对应的元素 this.index = this.hashfun(key,this.limit); // 2. 根据index取出对应的bucket var bucket = this.storage[index]; // 3. 判断bucket是否为空 if (bucket == null) return null; // 4. 查找元素 for (let i = 0; i < bucket.length; i++) { var tuple = bucket[i]; if (tuple[0] == key) { return tuple[1]; } return null; } }
// 删除操作 HashTable.prototype.delete = function(key){ // 1. 通过传来的key获取hashCode,查找到对应的元素 this.index = this.hashfun(key,this.limit); // 2. 根据index取出对应的bucket var bucket = this.storage[index]; // 3. 判断bucket是否为空 if (bucket == null) return null; // 4. 查找元素 for (let i = 0; i < bucket.length; i++) { var tuple = bucket[i]; if (tuple[0] == key) { bucket.splice(i,1); this.count--; return tuple[1]; } return null; } }
var hash = new HashTable(); hash.set('abc', 123); hash.set('cds', 234); hash.get('abc');
为什么需要扩容?
loadFactor
可以大于1,所以这个哈希表可以无限制的插入新数据
loadFactor
就是前面第五点哈希化的效率中提到过的:装填因子如何进行扩容?
hashCode=12
的数据项,在length=8
的时候,index=4
.在长度为16的时候呢? index=12
什么情况下扩容呢?
loadFactor>0.75
的时候进行扩容.2*8
,2小于sqrt(16),也就是4, 8大于4。而4*4
都是等于sqrt(n)。
function prime(num){ var temp = parseInt(Math.sqrt(num)); for (let i = 0; i <= temp; i++) { if (num % i == 0) { return false; } } // for (let i = 2; i < num; i++) { // if (num % i == 0) { // return false; // } // } return true; } }
新增方法
// 判断某个数字是否是质数 HashTable.prototype.isPrime = function (num){ var temp = parseInt(Math.sqrt(num)); for (let i = 0; i <= temp; i++) { if (num % i == 0) { return false; } } return true; } // 获取质数 HashTable.prototype.getPrime = function(num){ while(this.getPrime(num)){ num++; } return num; }
修改set方法
// 6. 判断是否需要扩容 if (this.count > this.limit * 0.75) { var newSize = this.limit * 2; var newPrime = this.getPrime(newSize); this.resize(newPrime); }
修改delete方法
// 判断是否需要减小容量 if (this.limit > 7 && this.count < this.limit * 0.25) { var newSize = Math.floor(this.limit / 2); var newPrime = this.getPrime(newSize); this.resize(newPrime); }
完整代码