目录解读:
- 哈希函数
- 解决哈希冲突
- 最终实现
哈希Map数据结构为什么存在?
如下代码:
const a = {};
let b = {}, c = {};
a[b] = 1, a[c] = 2;
console.log(a) // {'[object Object]': 2}
在上面的代码中,b和c所指的不是同一块内存,我们希望能让b和c当成不同的键值存于对象a中,但是js的对象的键值只会存储 String
类型的,也就是会默认调用 toString()
方法转化成字符串并以该字符串作为键值存储。
所以我们需要一种能将任意类型当键值的数据结构。ES6中引入了新类型Map,用于存储键值对。
实现思路:
用 {}
实现
-
定义一个哈希函数,将键转换为一个整数,作为键值对在哈希表中的索引。哈希函数需要满足以下条件:
- 对于相同的键,哈希函数返回的整数必须相同。
- 对于不同的键,哈希函数返回的整数应尽可能不同。
-
创建一个对象作为哈希表,每个元素都是一个链表,用于存储哈希冲突的键值对。
-
插入键值对时,先将键通过哈希函数转换为索引,然后将键值对插入到对应索引位置的链表中。如果该索引位置已经存在键值对,则将新的键值对插入到链表头部。
-
获取值时,先通过哈希函数获取键的索引,然后遍历对应索引位置的链表,查找对应的键值对。
-
删除键值对时,先通过哈希函数获取键的索引,然后遍历对应索引位置的链表,找到对应的键值对并删除。
哈希函数
哈希函数是一种将任意长度的数据映射为固定长度值的函数,这里将键映射为一个整数
function toHash(key) {
key = typeof key === 'string' ? key : JSON.stringify(key); // 转化对象类型
let hash = 0;
for(let i = 0; i < key.length; i++) {
hash += (hash << 5) + key.charCodeAt(i);
}
return hash.toString();
}
注: 在哈希函数中,左移位操作可以用来增加随机性,从而提高哈希函数的均匀性和抗冲突能力。左移 5 位是一种比较常见的位移操作,它可以产生一些随机性,但也可以根据具体需求进行调整。
也可以用 djb2 哈希算法
// 其中,5381 是一个任意的起始值,33 是一个经验值,可以有效地降低哈希冲突的概率。
// djb2 算法在实际应用中具有较高的效率和良好的均匀性,常用于字符串的哈希处理。
function djb2Hash(str) {
str = typeof str === 'string' ? str : JSON.stringify(str); // 转化对象类型
let hash = 5381;
for (let i = 0; i < str.length; i++) {
hash = (hash * 33) + str.charCodeAt(i);
}
return hash;
}
哈希冲突
哈希冲突指的是在哈希表中,不同的键值(用户名加key)被哈希函数映射到了同一个索引上的情况。由于哈希表的设计目的是将键值通过哈希函数映射到尽可能唯一的索引上,因此哈希冲突会对哈希表的性能和正确性产生负面影响。
举例:
对于键值为 [1] 和 [1],虽然它们存储内存块不同,但是它们经过哈希函数处理得到的索引是相同的
可以通过开放寻址和链地址表法来解决。
- 开放寻址法(Open Addressing):在发生哈希冲突时,顺序地向后查找下一个可用的槽位,直到找到一个空闲的槽位为止。如果哈希表的负载因子(load factor)较小,则该方法的效率较高。开放寻址法有三种实现方式:线性探测、二次探测和双重散列。
- 链地址法(Chaining):在哈希表的每个槽位中,存储一个链表(或其他数据结构),用于存储哈希函数映射到该槽位的键值。当发生哈希冲突时,只需要将新的键值插入到对应的链表中即可。链地址法的优点是对于任意负载因子都能保证常数级别的复杂度,缺点是在链表长度过长时会影响性能。
这里我们用链地址法实现:
也就是哈希表存储的是链表:{key : key, value: value, next: {}},
key代表传入的键值,value代表值,next代表哈希冲突后的下一链表
最终实现
class _Map {
constructor() {
this._data = {},
this._keys = [], // 存放key值
this._size = 0 // 记录大小
}
_toHash(key) {
key = typeof key === 'string' ? key : JSON.stringify(key);
let hash = 0;
for(let i = 0; i < key.length; i++) {
hash += (hash << 5) + key.charCodeAt(i);
}
return hash;
}
size() {
return this._size;
}
set(key, value) { // :Map
const index = this._keys.indexOf(key);
if(index === -1) { // 新插入的数据放前面
this._keys.push(key);
this._size++;
}
const hash = this._toHash(key);
if(this._data[hash]) {
let node = this._data[hash];
// 处理哈希冲突,用next实现
while(node.next) {
if (node.next.key === key) {
node.next.value = value;
return this;
}
node = node.next;
}
node.next = {key, value};
} else this._data[hash] = {key, value};
return this;
}
get(key) { // :any
const index = this._keys.indexOf(key);
if(index === -1) return undefined;
const hash = this._toHash(key);
let node = this._data[hash];
return (function() {
while(node.key !== key) {
node = node.next;
}
return node.value;
})()
}
has(key) { // :boolean
return this._keys.includes(key);
}
keys() { // :Generator
let _keys = this._keys;
return (function * K(_keys) {
yield * _keys;
})(_keys)
}
delete(key) { // :boolean
const index = this._keys.indexOf(key);
if(index === -1) return false;
this._keys.splice(index, 1); // 删除
this._size--;
const hash = this._toHash(key);
let node = this._data[hash];
if(node.key === key) {
node.next ? this._data[hash] = node.next : delete this._data[hash];
return true;
}
while(node.next.key !== key) {
node = node.next;
}
node.next = node.next.next;
return true;
}
clear() { // :void
this._data = {};
}
}
const a = new _Map()
let b = [1,2], g = [1,2], c = [1,3], e = {}
a.set(b, 1);
a.set(c, 2);
a.set(g, 4);
a.set(e, 3);
a.delete(b); // true
console.log(a._data);
// {
// '4184': { key: {}, value: 3 },
// '109729383': { key: [ 1, 2 ], value: 1, next: { key: [Array], value: 4 } },
// '109729416': { key: [ 1, 3 ], value: 2 }
// }
console.log([...a.keys()]); // [ [ 1, 3 ], [ 1, 2 ], {} ]