JS数据结构(8)—— 哈希表
1.什么是哈希表?
哈希表的复杂之处,就是它不好理解。它的结构就是数组,但是它神奇的地方在于对下标值的一种变换,这种变换我们可以称之为哈希函数,通过哈希函数可以获取到HashCode。
哈希表是一种存储键值对的数据结构,是基于数组来实现的,哈希表内部是使用一个哈希函数把值转换成一个数字,而这个数字作为键值对的key,通过这个key来完成查询,插入,修改等操作。
2.哈希表的一些概念
哈希化:将大数字转化成数组范围内下标的过程。
哈希函数:通常我们会将单词转换成大数字以及大数字进行哈希化,这两个步骤会封装一个函数,这个函数就是哈希函数。
哈希表:最终将数据插入到的这个数组, 对整个结构的封装,就是一个哈希表。
3.解决冲突
3.1什么是冲突
比如有7个0~100的数字,我们使用10个位置通过哈希表来存储,但是依然可能发生冲突。即两个单词通过哈希函数得到数组的下标值后,发现那个位置上已经存在单词了,这就是冲突。
3.2解决冲突
冲突是不可避免的,我们只能解决冲突。
两种方案:
方案一:链地址法
链地址法解决冲突的办法是每个数组单元中存储的不再是单个数据,而是一个链表或者数组。
一旦发现冲突,则将冲突玄素插入到链表的首端或末端(数组的末端)。
当查询时,先根据哈希化后的下标值找到对应的位置,再取出链表(数组),一次查询要寻找的数据。
方案二:开放地址法
开放地址法的主要工作方式是寻找空白的单元格来添加重发的数据
那么如何来寻找空白的单元格呢,又有三种方法:
(1)线性探测:线性的查找空白的单元
插入:如果插入元素时发现有冲突,则从冲突的位置后面的位置开始一个一个往后查找,直到找到空白的位置来存放该元素。
查询:查询某个元素时,先用哈希化得到该元素的下标值,然后在哈希表中查找该下标值所对应的数据,如果和查询的元素相同则直接返回,若不相同,则开始线性查找,直到查找到或者直到碰到空位置,则停止。
删除:删除操作和插入查询很相似,但是有一个特别的注意点:删除一个数据时,不可以把这个位置的下标的内容设置为null,因为如果设置为null的话,可能会影响我们之后再查询其他的操作,所以通常删除一个位置的数据项时,我们可以将它进行特殊处理(设置为-1)
线性探测的问题:聚集(一连串填充单元)会影响哈希表的性能。
而二次探测可以解决一部分这个问题。
(2)二次探测:主要优化的是探测时的步长
二次探测队步长做了优化,比如从下标值x开始,x+1×1,x+2×2,x+3×3...
这样就可以一次性探测比较长的距离,避免那些聚集带来的影响
二次探测存在的问题:
比如连续插入的是2,12,32,42,52,62,那么它们依次累加的步长是相同的。
会造成不唱不一的一种聚集,还是会影响效率。
而再哈希法可以根本上解决这个问题。
(3)再哈希法:把关键字用另外一个哈希函数,再做一次哈希化,而这个哈希化的结果作为步长
第二次哈希化的特点:
- 和第一个哈希函数不同
- 不能输出为0
stepSize = constant - (key % constant)
这就是第二次哈希函数的计算步长公式,其中constant是质数,且小于数组的容量。
在开发过程中,经常使用链地址法,链地址法效率更高一些。
4.哈希表与数组的比较
哈希表通常是基于数组进行实现的,但是相对于数组,它有很多优势:
- 它可以提供非常快速的插入-删除-查找操作
- 无论有多少数据,插入和删除值需要接近常量的时间:即 O(1)的时间级。实际上,只需要几个机器指令即可完成。
哈希表相对于数组的一些缺点:
- 哈希表中的数据是没有顺序的,所以不能以一种固定的方式来遍历其中的元素
- 通常情况下,哈希表中的key是不允许重复的,不能放置相同的key,用于保存不同的元素
5.哈希表的封装
5.1代码思路
(1)我们在这里采用链地址法来实现(用数组)
(2)我们需要三个属性:
- storage:数组,数组中存放相关的元素
- count:表示当前已经存在了多少数据
- limit:用于标记数组中一共可以存放多少个元素
(3)哈希表的常见方法:
- 哈希函数(hashFunc):将字符串转换成比较大的数字:hashCode,并且将大的数字hashCode压缩到数组范围之内
- 插入/修改(put):传入一个<key,value>时,如果原本不存在key,则插入,否则,则修改
- 获取(get):传入key,如果能找到相同的key则返回对应的value,否则,返回null
- 删除(delete):传入key,如果能找到相同的key则删除这个bucket中对应的key和value,否则返回null
- 其他方法:判断哈希表是否为空,获取哈希表中元素个数
5.2代码实现
function HashTable() {
// 属性
this.storage = [];
this.count = 0;
this.limit = 7;
// 方法
// 哈希函数
HashTable.prototype.hashFunc = function(str, size) {
// 定义 hashCode 变量
var hashCode = 0;
// 根据霍纳算法,计算 hashCode 的值
for(var i = 0; i < str.length; i++) {
hashCode = 37 * hashCode + str.charCodeAt(i);
}
// 取余操作
var index = hashCode % size;
return index;
}
// 插入和修改操作
HashTable.prototype.put = function(key, value) {
// 根据key获取索引值
var index = this.hashFunc(key.toString(), this.limit);
// 根据索引值取出bucket(数组单元中存储的数组)
var bucket = this.storage[index];
// ----如果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[1] = value;
return;
}
}
// 添加操作
bucket.push([key,value]);
this.count ++;
}
// 获取操作
HashTable.prototype.get = function(key) {
// 根据key获取对应的index
var index = this.hashFunc(key.toString(), this.limit);
// 根据index获取对应的bucket
var bucket = this.storage[index];
// 判断bucket是否为null,如果为null,直接返回null
if(bucket == null)
return null;
// 线性查找bucket中的每个key是否等于传入的key
for(var i = 0; i < bucket.length; i++) {
var tuple = bucket[i];
if(tuple[0] == key)
return tuple[1];
}
return null;
}
// 删除操作
HashTable.prototype.delete = function(key) {
// 根据key获取对应的index
var index = this.hashFunc(key.toString(), this.limit)
// 根据index获取对应的bucket
var bucket = this.storage[index];
// 判断bucket是否为null,如果为null,返回false
if(bucket == null) return nul;
// 线性查找bucket中的每个key是否等于传入的key
for(var 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;
}
// 判断哈希表是否为空
HashTable.prototype.isEmpty = function() {
return this.count == 0;
}
// 获取哈希表中元素的个数
HashTable.prototype.size = function() {
return this.count;
}
}
// 测试哈希表:
var hashTable = new HashTable();
// 插入
hashTable.put('name', 'msy');
hashTable.put('age', 18);
hashTable.put('address','shanxi');
hashTable.put('school', 'xian')
console.log(hashTable.storage);
/*结果为:
[
<2 empty items>,
[ [ 'age', 18 ] ],
[ [ 'school', 'xian' ] ],
<1 empty item>,
[ [ 'name', 'msy' ] ],
[ [ 'address', 'shanxi' ] ]
]
*/
// 获取
console.log(hashTable.get('name')); // 结果为:msy
// 修改
hashTable.put('name', 'ppp');
console.log(hashTable.get('name')); // 结果为:ppp
// 删除
hashTable.delete('school');
console.log(hashTable.get('school')); // 结果为:null
console.log(hashTable.storage);
/*
结果为:[
<2 empty items>,
[ [ 'age', 18 ] ],
[],
<1 empty item>,
[ [ 'name', 'ppp' ] ],
[ [ 'address', 'shanxi' ] ]
]
*/
(如果有什么错误,欢迎大佬在评论区指正,thank you~)