这是我参与8月更文挑战的第7天,活动详情查看:8月更文挑战算法
几乎全部的编程语言都有直接或间接地应用哈希表这种数据结构。哈希表一般是基于数组实现的,它神奇的地方在于对下标值的一种变换,这种变换可称为哈希函数,经过哈希函数能够获取到HashCode
。相对于数组,它的优点在于速度快,效率更高。编程
unicode
),将大数字转换为哈希化的代码的过程封装成的函数称为哈希函数哈希表中的每个key都是惟一值,并且每个key都有其对应的索引值,经过这个索引值直接在哈希表中找到这个key的value值,大大地提升了查找的效率。若是数据庞大的是否,每个索引值对应的就不止一个key,此时就会产生冲突。这个冲突常见有2种解决方式:链地址法和开放地址法。数组
在下面的封装中,采用的基于数组的链地址法。markdown
在哈希表封装的时候,设置哈希表的总长度。通常将总长度设置为存放个数的两倍,好比有50个数据,那咱们设置这个哈希表的总长度为100。数据结构
function HashTable() {
this.storage = []; // 存放元素
this.count = 0; // 存放个数
this.limit = 7; // 当前哈希表的总长度
}
复制代码
哈希函数在哈希表中是相当重要的,几乎每个操做都须要它。它的做用在于获取某个key在哈希表中的索引值,而后经过一些计算方法来计算出它的hashCode
,再对这个hashCode
进行取余(hashCode
的值是很大的,这里是为了让数据量变小),而这个size,就是哈希表的this.limit
,即哈希表的长度。编程语言
这里的37实际上是一个底数,好比abc = 1*37^2 + 2*37^1 + 3
。这里取一个合适的数就行了,不必定就要37,不过最好是质数。函数
HashTable.prototype.hashFunc = function (str, size) {
var hashCode = 0;
// 霍纳算法,计算hashCode的值
for (var i = 0; i < str.length; i++) {
hashCode = 37 * hashCode + str.charCodeAt(i);
}
// 取余
var index = hashCode % size;
return index;
}
复制代码
这里使用的是链地址法(基于数组,即每个下标值对应的是一个数组)post
由于在哈希表中,是没有重复的值的,因此当用户传入一个(key, value)
时,若是原来不存在这个key,就是插入操做;若是存在,就是修改操做。操做步骤:测试
根据key获取索引值(经过哈希算法),将数据插入到对应的位置this
var index = this.hashFunc(key, this.limit);
复制代码
根据索引值取出这个位置:先判断该位置是否存在一个数组,若是不存在就建立
var bucket = this.storage[index];
if (bucket == null) {
bucket = [];
this.storage[index] = bucket;
}
复制代码
判断是新增仍是修改原来的值 (key同样就是修改,不同就是新增)
若是已经有这个key了,那么就修改该key的值;
这里的bucket是哈希表中其中一个索引值的引用,存储多个key: value
的数组 (这些key经过哈希函数计算出来的索引值是想经过的)
for (var i = 0; i < bucket.length; i++) {
var tuple = bucket[i]
if (tuple[0] == key) {
tuple[1] = value;
return;
}
}
复制代码
若是没有,执行后续的添加操做;注意这里push的是数组对象
bucket.push([key, value]);
this.count += 1;
复制代码
这里的前两个步骤跟上面的添加或修改数据是相同的。先经过哈希函数算出这个key对应的索引值,根据这个索引值找到这个key所在的数组,若是存在,再从这个数组里面找这个key对应的value。
根据key获取对应的index
根据index获取对应的位置bucket(数组)
判断bucket是否为null,若是为null直接返回null
if (bucket == null) return null;
复制代码
线性查找bucket中每个key是否等于传入的key
若是等于就返回对应的value;若是遍历结束了还没找到就返回null
for (var i = 0; i < bucket.length; i++) {
var tuple = bucket[i]
if (tuple[0] == key) {
return tuple[1];
}
}
return null;
复制代码
删除操做在这里思路是差很少的,只不过获取操做获取到这个key对应的value就返回,而删除操做时把它删除;再判断语句中加入下面两行代码对获取到的value值连带key删除掉便可。
bucket.splice(i, 1);
this.count -= 1;
复制代码
其中,0, 3, 4, 5
分别为转换以后的哈希表索引值,当他们的索引值相同时,就会被以key = value
的形式存入到一个数组中。
var hash = new HashTable();
hash.put('mannqo', 123);
hash.put('mama', 222);
hash.put('Ytao', 321);
hash.put('fafa', 213);
hash.put('neinei', 123);
hash.remove('neinei');
console.log(hash.get('neinei'));
console.log(hash.get('mannqo'));
console.log(hash);
复制代码
输出结果:
能够看到,删除后的元素再次
get
的话返回的是空,并且在哈希表中也看不到了。而索引值相同的mama
和fafa
被存放到索引值为0的数组当中。
随着数据量的增多,每个索引值对应的bucket(数组)长度也会愈来愈长,会致使效率的下降。这个是否就须要扩容哈希表。在合适的状况下对数组扩容能够提升效率,通常是在loadFactor(存放个数/哈希表长度)>0.75
的时候进行扩容。在扩容以后,要记得对全部数据项的位置进行修改(从新调用哈希函数,获取不一样的位置),由于扩容以后每个元素的位置也会发生改变。
保存旧的数据内容 var oldStorage = this.storage;
重置全部的属性
this.storage = [];
this.count = 0;
this.limit = newLimt;
复制代码
遍历oldStorage
中全部的bucket
这里要先判断每个旧的bucket有没有东西,若是没有就继续执行程序,若是有就把它存放到新的bucket中。
for (var i = 0; i < oldStorage.length; i++) {
var bucket = oldStorage[i];
if (bucket == null) {
continue;
}
for (var j = 0; j < bucket.length; j++) {
var tuple = bucket[j];
this.put(tuple[0], tuple[1]);
}
}
复制代码
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))
}
复制代码