什么是散列表?
- 散列表是Dictionary(字典)的一种散列表实现方式,字典传送门
- 一个很常见的应用是使用散列表来表示对象。Javascript语言内部就是使用散列表来表示每个对象。此时,对象的每个属性和方法(成员)被存储为key对象类型,每个key指向对应的对象成员。
- 以字典中使用的电子邮件地址簿为例。我们将使用最常见的散列函数:lose lose散列函数,方法是简单的将每个键值中的每个字符的ASCII值相加,如下图所示:

创建散列表
class HashTable {
this.table = {};
}
实现几个简单方法
- toStrFn() 转字符串 和字典中一样
toStrFn (key){
if (key === null) {
return 'NULL';
} else if (key === undefined) {
return 'UNDEFINED';
} else if (typeof key === 'string' || key instanceof String) {
return `${key}`;
}else if ( Object.prototype.toString.call(key)==='[object Object]' ){
return JSON.stringify(obj)
}
return key.toString();
}
- hashCode(key) 创建散列函数
loseloseHashCode(key) {
if (typeof key === 'number') {
return key;
}
const tableKey = this.toStrFn(key);
let hash = 0;
for (let i = 0; i < tableKey.length; i++) {
hash += tableKey.charCodeAt(i);
}
return hash % 37;
}
hashCode(key) {
return this.loseloseHashCode(key);
}
- put(key,value) 将键和值加入散列表
put(key, value) {
if (key != null && value != null) {
const position = this.hashCode(key);
this.table[position] = new ValuePair(key, value);
return true;
}
}
return false;
- get(key)从散列表中获取一个值
get(key) {
const valuePair = this.table[this.hashCode(key)];
return valuePair == null ? undefined : valuePair.value;
}
- remove(key) 从散列表中移除一个值
remove(key) {
const hash = this.hashCode(key);
const valuePair = this.table[hash];
if (valuePair != null) {
delete this.table[hash];
return true;
}
return false;
}
使用 HashTable 类
const hash = new HashTable();
hash.put('Gandalf', 'gandalf@email.com');
hash.put('John', 'johnsnow@email.com');
hash.put('Tyrion', 'tyrion@email.com');
console.log(hash.hashCode('Gandalf') + ' - Gandalf');
console.log(hash.hashCode('John') + ' - John');
console.log(hash.hashCode('Tyrion')+' - Tyrion');
console.log(hash.get('Gandalf'));
console.log(hash.get('Loiane'));
hash.remove('Gandalf');
console.log(hash.get('Gandalf'));
处理散列表中的冲突(解决上面的坑)
- 有时候,一些键会有相同的散列值。不同的值在散列表中对应相同位置的时候,我们称其为
冲突。来看一下下面代码的输出结果:
const hash = new HashTable();
hash.put('Jonathan', 'jonathan@email.com'); 0
hash.put('Jamie', 'jamie@email.com');
通过对每个提到的名字调用 hash.hashCode 方法,输出结果如下。
5 - Jonathan
5 - Jamie
- Jonathan和Jamie有相同的散列值5。
- 由于 Jamie是最后一个被添加的,它将是在 HashTable 实例中占据位置 5 的元素。
- 如果调用Hash.get(Jonathan)后输出的是'jonathan@email.com'还是'jamie@email.com'呢?
- 有两种处理冲突的方法:分离链接和线性探查。
分离链接
- 分离链接法包括为散列表的每一个位置创建一个链表并将元素存储在里面。它是解决冲突的
最简单的方法,但是在 HashTable 实例之外还需要额外的存储空间。
- 重写一下三个方法:put、get和remove。
- put()
put(key, value) {
if (key != null && value != null) {
const position = this.hashCode(key);
if (this.table[position] == null) {
this.table[position] = new LinkedList();
}
this.table[position].push(new ValuePair(key, value));
return true;
}
return false;
}
链表传送门
- get()
get(key) {
const position = this.hashCode(key);
const linkedList = this.table[position];
if (linkedList != null && !linkedList.isEmpty()) {
let current = linkedList.getHead();
while (current != null) {
if (current.element.key === key) {
return current.element.value;
}
current = current.next;
}
}
return undefined;
}
- remove()
remove(key) {
const position = this.hashCode(key);
const linkedList = this.table[position];
if (linkedList != null && !linkedList.isEmpty()) {
let current = linkedList.getHead();
while (current != null) {
if (current.element.key === key) {
linkedList.remove(current.element);
if (linkedList.isEmpty()) {
delete this.table[position];
}
return true;
}
current = current.next;
}
}
return false;
}
线性探查
- 另一种解决冲突的方法是线性探查。之所以称作线性,是因为它处理冲突的方法是将元素直 4
接存储到表中,而不是在单独的数据结构中。
- 当想向表中某个位置添加一个新元素的时候,如果索引为 position 的位置已经被占据了,就尝试 position+1 的位置。如果 position+1 的位置也被占据了,就尝试 position+2 的位
置,以此类推,直到在散列表中找到一个空闲的位置。
- 想象一下,有一个已经包含一些元素的散列表,我们想要添加一个新的键和值。我们计算这个新键的 hash,并检查散列表中对应的位置
是否被占据。如果没有,我们就将该值添加到正确的位置。如果被占据了,我们就迭代散列表,
直到找到一个空闲的位置。
- 同样的也需要重写一下三个方法
- put()
put(key, value) {
if (key != null && value != null) {
const position = this.hashCode(key);
if (this.table[position] == null) {
this.table[position] = new ValuePair(key, value);
} else {
let index = position + 1;
while (this.table[index] != null) {
index++;
}
this.table[index] = new ValuePair(key, value);
}
return true;
}
return false;
}
- get()
get(key) { const position = this.hashCode(key);
if (this.table[position] != null) {
if (this.table[position].key === key) {
return this.table[position].value;
}
while (this.table[index] != null && this.table[index].key !== key) {
let index = position + 1;
index++;
}
if (this.table[index] != null && this.table[index].key === key) {
return this.table[position].value;
}
return undefined;
}
}
- remove() 和get方法基本相同
verifyRemoveSideEffect(key, removedPosition) {
const hash = this.hashCode(key);
let index = removedPosition + 1;
while (this.table[index] != null) {
const posHash = this.hashCode(this.table[index].key);
if (posHash <= hash || posHash <= removedPosition) {
this.table[removedPosition] = this.table[index];
delete this.table[index];
removedPosition = index;
}
index++;
}
}
remove(key) {
const position = this.hashCode(key);
if (this.table[position] != null) {
if (this.table[position].key === key) {
delete this.table[position];
this.verifyRemoveSideEffect(key, position);
return true;
}
let index = position + 1;
while (this.table[index] != null && this.table[index].key !== key ) {
index++;
}
if (this.table[index] != null && this.table[index].key === key) {
delete this.table[index];
this.verifyRemoveSideEffect(key, index);
return true;
}
}
return false;
}
创建更好的散列函数
- 我们实现的散列函数并不是一个表现良好的散列函数,因为它会产生太多的冲突。
一个表现良好的散列函数是由几个方面构成的:
- 插入和检索元素的时间(即性能)
- 较低的冲突可能性。
- 另一个可以实现的更好的散列函数:
djb2HashCode(key) {
const tableKey = this.toStrFn(key);
let hash = 5381;
for (let i = 0; i < tableKey.length; i++) {
hash = (hash * 33) + tableKey.charCodeAt(i);
}
return hash % 1013;
}
ES2015 Map 类
- 和我们的 Dictionary 类不同,ES2015 的 Map 类的 values 方法和 keys 方法都返回
Iterator,而不是值或键构成的数组。
- 另一个区别是,我们实现的 size 方法返回字典中存储的值的个数,而 ES2015 的 Map 类则有一个 size 属性。
const map = new Map();
map.set('Gandalf', 'gandalf@email.com');
map.set('John', 'johnsnow@email.com');
map.set('Tyrion', 'tyrion@email.com');
console.log(map.has('Gandalf'));
console.log(map.size);
console.log(map.keys());
console.log(map.values());
console.log(map.get('Tyrion'));
map.delete('Gandalf');
console.log(map.has('Gandalf'));
ES2105 WeakMap 类和 WeakSet 类
- 除了 Set 和 Map 这两种新的数据结构,ES2015还增加了它们的弱化版本,WeakSet 和 WeakMap。
- 创建和使用这两个类主要是为了性能。WeakSet 和 WeakMap 是弱化的(用对象作为键), 没有强引用的键。这使得 JavaScript 的垃圾回收器可以从中清除整个入口。
- 另一个优点是,必须用键才可以取出值。这些类没有 entries、keys 和 values 等迭代器