8.哈希表
8.1哈希表概念
8.2哈希表的特点
8.3哈希表的冲突
8.3.1什么是哈希冲突?
例如现要存储某个数,通过哈希函数得到其下标值后,发现此下标已存在有值,这种情况就为哈希冲突,为了能使一个下标对应一个数据项我们只能解决此冲突。另外,通常把这种具有不同关键字而具有相同哈希地址的元素称为同义词,这种冲突也称为同义词冲突
8.3.2如何解决这种冲突?
8.3.2.1链地址法(也称拉链法)
把所有同义词用单链表或数组链接起来
8.3.2.2开放地址法
通过寻找空白的单元格来添加同义词
如:有一集合为(16,74,60,43,54,29)其哈希函数为h(k)=k mod 13,其中16与29哈希化后为同一下标,因16在29前插入,所以在插入29时会发现下标3已经有值,这时就要通过其他的方法去往后寻找空白位置将29插入,方法为以下三种:
1)线性探测法
概念:从发生冲突的地址开始,依次探测下一个地址(通过下标加1的方法往后查找空白位置)
插入29时:如下图所示,当我们插入29时发现index=3的位置已经有了16,我们就要通过index+1往后查找空白的位置将29放入??
查找29时:
删除29时:
因为在查找时所设条件是当发现离index=3近的第一个单元格为空时就应该停止查找,所以在删除时不能将29所在位置设置为null而是应该设置为其他能判断在此之后还有数值的值,避免当29删除后要查找处在29之后的同义词程序却提示不存在现象。
2)二次探测法
概念:在线性探测的基础上对探测步长进行优化,例如线性探测步长依次为x+1,x+2,而二次探测步长依次为x+1^2,x+2^2从而避免聚集
3)再哈希法
概念:第一次哈希化的值为下标,将关键字用另一个哈希函数再一次进行哈希化,用此次哈希化的结果作为步长,对于指定的关键字,步长在整个探测中是不变的,但是不同的关键字使用不同的步长
特点:哈希函数与第一次哈希化所用函数不同,且输出不为0(为什么输出不能为0?因为当步长为0时每次探测的都是同一个位置,会使程序进行死循环)
哈希函数:step=constant-(key%constant),constant为质数且小于数组的容量
8.4什么是装填因子
-
装填因子表示当前哈希表中已经包含的数据项和整个哈希表长度的比值 -
装填因子=总数据项/哈希表长度 -
开放地址法的装填因子为1,因为它必须寻找到空白单元才能将元素存入 -
链地址法装填因子可以大于1,因为拉链法可以无限延伸下去 -
平均探测长度以及平均存取时间都取决于装填因子,装填因子变大,探测长度也越来越长
8.5性能排行
二次探测,再哈希法>线性探测
8.6哈希函数
8.6.1好的哈希函数应该具备哪些优点
8.6.2哈希函数的实现
哈希函数主要为了实现以下事情:
-
使所有元素尽可能分布在m个连续内存单元上 -
将字符串转成较大的数字:hashCode -
将hashCode压缩到数组范围之内
function hasFunc (str, size) {
? ?var hasCode = 0//定义hashCode变量
? ?for (var i = 0; i < str.lenght; i++) {
? ? ? ?hasCode = 37 * hasCode + str.charCodeAt(i)//先对单词进行编码
? ? ? ?//37为质数,通过质数来确保元素尽可能分布在连续内存单元上
? }
? ?var index = hasCode % size//取余操作
? ?return index
}
8.7哈希表的实现
使用链地址法进行哈希表构造如下图所示
8.7.1哈希表整体实现预览
function hasMap () {
? ?//属性
? ?this.storage = [] //存储元素
? ?this.count = 0 //记录表中已存在多少元素
? ?//装载因子大于0.75时要进行扩容,当小于0.25时要进行容量缩减
? ?this.limit = 7 //表的最大容量是多少
? ?hasMap.prototype.hasFunc = function (str, size) {}
? ?hasMap.prototype.put = function (key, value) {}
? ?hasMap.prototype.get = function (key) {}
? ?hasMap.prototype.del = function (key) {}
hasMap.prototype.resize = function (newLimit) {}
hasMap.prototype.isPrime = function (num) {}
hasMap.prototype.getPrime = function (num) {}
? }
8.7.2插入或修改操作
思路:
//插入与修改操作
? ?hasMap.prototype.put = function (key, value) {
? ? ? ?//1.根据key获取对应的index
? ? ? ?var index = this.hasFunc(key, this.limit)
? ? ? ?//2.根据index取出对应的bucket
? ? ? ?var bucket = this.storage[index]
? ? ? ?//3.判断该bucket是否存在
? ? ? ?if (bucket == null) {
? ? ? ? ? ?bucket = []
? ? ? ? ? ?this.storage[index] = bucket
? ? ? }
? ? ? ?//判断是否是修改数据
? ? ? ?for (var i = 0; i < bucket.length; i++) {
? ? ? ? ? ?var tuple = bucket[i]
? ? ? ? ? ?if (tuple[0] == key) {//tuple[0]==key说明bucket中已经存在有该数据所以进行修改
? ? ? ? ? ? ? ?tuple[1] = value
? ? ? ? ? ? ? ?return
? ? ? ? ? }
? ? ? }
? ? ? ?//进行添加操作
? ? ? ?bucket.push([key, value])
? ? ? ?this.count += 1
? ? ? ?//当装载因子>0.75时进行扩容
? ? ? ?if (this.count > this.limit * 0.75) {
? ? ? ? ? ?var newLimit = this.getPrime(this.limit * 2)
? ? ? ? ? ?this.resize(newLimit)
? ? ? }
? }
8.7.3获取数据操作
思路:
hasMap.prototype.get = function (key) {
? ? ? ?//1.根据key获取对应的index
? ? ? ?var index = this.hasFunc(key, this.limit)
? ? ? ?//2.根据index取出对应的bucket
? ? ? ?var bucket = this.storage[index]
? ? ? ?//3.判断该bucket是否为null
? ? ? ?if (bucket == null) return false
? ? ? ?//4.有bucket则进行线性查找
? ? ? ?for (var i = 0; i < bucket.length; i++) {
? ? ? ? ? ?var tuple = bucket[i]
? ? ? ? ? ?if (tuple[0] == key)
? ? ? ? ? ? ? ?return tuple[1]
? ? ? }
? ? ? ?return false
? }
8.7.4删除数据操作
思路:
hasMap.prototype.del = function (key) {
? ? ? ?//1.根据key获取对应的index
? ? ? ?var index = this.hasFunc(key, this.limit)
? ? ? ?//2.根据index取出对应的bucket
? ? ? ?var bucket = this.storage[index]
? ? ? ?//3.判断该bucket是否为null
? ? ? ?if (bucket == null) return false
? ? ? ?//4.有bucket则进行线性查找
? ? ? ?for (var i = 0; i < bucket.length; i++) {
? ? ? ? ? ?var tuple = bucket[i]
? ? ? ? ? ?if (tuple[0] == key)
? ? ? ? ? ? ? ?bucket.splice(i, 1)
? ? ? ? ? ?this.count--
? ? ? ? ? ? //缩小容量
? ? ? ? ? ? ? ?if (this.limit > 7 && this.count < this.limit * 0.25)
? ? ? ? ? ? ? {var newLimit = this.getPrime(Math.floor(this.limit / 2))
? ? ? ? ? ? ? ?this.resize(newLimit)
? ? ? ? ? ? ? }
? ? ? ? ? ?return tuple[1]
? ? ? }
? ? ? ?return false
? }
8.7.5扩容操作
8.7.5.1为什么要扩容
8.7.5.2扩容操作
思路:
-
先将原先的数组(storage)进行保存 -
再对原有哈希表中的属性进行重置操作 -
再将原数组的值重新赋值进行插入操作(因为index与limit有关,当limit改变之后各个元素所对应的index也会发现改变) -
注:为了保证哈希表中的元素能够均匀分布,所以在进行扩容操作时要为新limit赋质数(只能被1和自身整除的数) -
判断某数是否为质数 hasMap.prototype.isPrime = function (num) {
? ? ? ?var temp = parseInt(Math.sqrt(num))
? ? ? ?for (var i = 2; i <= temp; i++) {
? ? ? ? ? ?if (num % i == 0) {
? ? ? ? ? ? ? ?return false
? ? ? ? ? }
? ? ? }
? ? ? ?return true
? }
-
扩容 hasMap.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++) {
? ? ? ? ? ?//取出对应的bucket
? ? ? ? ? ?var bucket = oldstorage[i]
? ? ? ? ? ?//判断bucket是否为null
? ? ? ? ? ?if (bucket == null) continue
? ? ? ? ? ?//bucket中有数据,取出数据,重新插入
? ? ? ? ? ?for (var j = 0; j < bucket.length; j++) {
? ? ? ? ? ? ? ?var tuple = bucket[j]
? ? ? ? ? ? ? ?this.put(tuple[0], tuple[1])
? ? ? ? ? }
? ? ? }
? }
|