字典
在字典中,存储的是[键,值]对,其中键名是用来查询特定元素的。字典和集合很相似,集合以[值,值]的形式存储元素,字 典则是以[键,值]的形式来存储元素。字典也称作映射、符号表或关联数组。
import { defaultToString } from '../util';
import { ValuePair } from './models/value-pair';
export default class Dictionary {
constructor(toStrFn = defaultToString) {
this.toStrFn = toStrFn;//在字典中,理想的情况是用字符串作为键名,值可以是任何类型(从数、字符串等原始类型,到复杂的对象)。但是,由于 JavaScript不是强类型的语言,我们不能保证键一定是字符串。我们 需要把所有作为键名传入的对象转化为字符串,使得从 Dictionary 类中搜索和获取值更简单 (同样的逻辑也可以应用在上一章的 Set 类上)。要实现此功能,我们需要一个将 key 转化为字 符串的函数
this.table = {};
}
//向字典中添加新元素。如果 key 已经存在,那么已存在的 value 会 被新的值覆盖
set(key, value) {
if (key != null && value != null) {
const tableKey = this.toStrFn(key);
this.table[tableKey] = new ValuePair(key, value);
return true;
}
return false;
}
//通过以键值作为参数查找特定的数值并返回
get(key) {
const valuePair = this.table[this.toStrFn(key)];
return valuePair == null ? undefined : valuePair.value;
}
//get的第二种实现方法
get(key) {
if (this.hasKey(key)) {
return this.table[this.toStrFn(key)];//会获取两次 key 的字符串以及访问两次 table 对象,消耗较第一种方法更多
}
return undefined;
}
//如果某个键值存在于该字典中,返回 true,否则返回 false
hasKey(key) {
return this.table[this.toStrFn(key)] != null;//JavaScript只允许我们使用字符串作为对象的键名或属性名
}
//通过使用键值作为参数来从字典中移除键值对应的数据值
remove(key) {
if (this.hasKey(key)) {
delete this.table[this.toStrFn(key)];
return true;
}
return false;
}
//方法返回一个字典包含的所有值构成的数组
values() {
return this.keyValues().map(valuePair => valuePair.value);
}
//将字典所包含的所有键名以数组形式返回
keys() {
return this.keyValues().map(valuePair => valuePair.key);//用所创建的 keyValues 方法来返回一个包含 valuePair 实例的数组,只对valuePair 的 key 属性感兴趣,就只返回它的 key
}
//也可以写成如下形式,map更加简洁,应该适应这种写法。
keys() {
const keys = [];
const valuePairs = this.keyValues();
for (let i = 0; i < valuePairs.length; i++) {
keys.push(valuePairs[i].key);
}
return keys;
}
//以数组形式返回字典中的所有 valuePair 对象
keyValues() {
return Object.values(this.table);//行了 JavaScript的 Object 类内置的 values 方法,ECMAScript 2017中引入
}
//并非所有浏览器都支持 Object.values 方法,也可以用下面的代码来代替
keyValues() {
const valuePairs = [];
for (const k in this.table) { //迭代了 table 对象的所有属性
if (this.hasKey(k)) {
valuePairs.push(this.table[k]); //然后将table 对象中的valuePair 加入结果数组
}
return valuePairs;
};
}
//迭代字典中所有的键值对。callbackFn 有两个参数:key 和 value。该方法可以在回调函数返回false 时被中止
forEach(callbackFn) {
const valuePairs = this.keyValues();
for (let i = 0; i < valuePairs.length; i++) {
const result = callbackFn(valuePairs[i].key, valuePairs[i].value);
if (result === false) {
break;
}
}
}
isEmpty() {
return this.size() === 0;
}
size() {
return Object.keys(this.table).length;//也可以调用keyValues 方法并返回它所返回的数组长度(return this.keyValues().
length
}
clear() {
this.table = {};
}
toString() {
if (this.isEmpty()) {
return '';
}
const valuePairs = this.keyValues();
let objString = `${valuePairs[0].toString()}`;
for (let i = 1; i < valuePairs.length; i++) {
objString = `${objString},${valuePairs[i].toString()}`;//调用 valuePair 的 toString 方法来将它的第一个 valuePair 加入结果字符串
}
return objString;
}
}
defaultToString 函数声明如下。
export function defaultToString(item) {
if (item === null) {
return 'NULL';
} else if (item === undefined) {
return 'UNDEFINED';
} else if (typeof item === 'string' || item instanceof String) {
return `${item}`;
}
return item.toString(); //
}
为了在字典中保存 value,我们将 key 转化为了字符串,而为了保存信息的需要,我们同
样要保存原始的 key。因此,我们不是只将 value 保存在字典中,而是要保存两个值:原始的 key 和 value。为了字典能更简单地通过 toString 方法输出结果,我们同样要为 ValuePair类创建 toString 方法。
ValuePair 类的定义如下。
class ValuePair {
constructor(key, value) {
this.key = key; this.value = value;
}
toString() {
return `[#${this.key}: ${this.value}]`;
}
}
散列表
HashTable 类,也叫 HashMap 类,它是Dictionary 类的一种散列表实现方式。
散列算法的作用是尽可能快地在数据结构中找到一个值。
散列函数的作用是给定一个键值,然后 返回值在表中的地址。
import { defaultToString } from '../util';
import { ValuePair } from './models/value-pair';
export default class HashTable {
constructor(toStrFn = defaultToString) {
this.toStrFn = toStrFn;
this.table = {};
}
//创建散列函数
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 方法简单地调用了 loseloseHashCode 方法,将 key 作为参数传入
hashCode(key) {
return this.loseloseHashCode(key);
}
// lose lose散列函数并不是一个表现良好的散列函数,因为它会产生太多的冲突。一个表现良好的散列函数是由几个方面构成的:插入和检索元素的时间(即性能),以及较低的 冲突可能性
//比 lose lose更好的散列函数是 djb2
/* djb2HashCode(key) {
const tableKey = this.toStrFn(key);
let hash = 5381;//初始化一个 hash 变量并赋值为一个质数
for (let i = 0; i < tableKey.length; i++) {//迭代参数 key
hash = (hash * 33) + tableKey.charCodeAt(i);//将 hash 与 33 相乘(用作一个幻数,幻数是指在编程中指直接使用的常数),并和当前迭代到的字符的 ASCII码值相加,
}
return hash % 1013;
} */
hashCode(key) {
return this.loseloseHashCode(key);
}
//向散列表增加一个新的项(也能更新散列表),put方法和 Dictionary 类中的 set 方法逻辑相似,但是大多数的编程语言会在 HashTable 数据结构中使用 put 方法
put(key, value) {
if (key != null && value != null) {
const position = this.hashCode(key);//用所创建的 hashCode 函数在表中找到 一个位置
this.table[position] = new ValuePair(key, value);
return true;
}
return false;
}
//返回根据键值检索到的特定的值
get(key) {
const valuePair = this.table[this.hashCode(key)];//用所创建的 hashCode 方法获取 key 参数的位置
return valuePair == null ? undefined : valuePair.value;
}
//根据键值从散列表中移除值
remove(key) {
const hash = this.hashCode(key);//因此我们使用 hashCode 函 数来获取 hash
const valuePair = this.table[hash];
if (valuePair != null) {
delete this.table[hash];//还可以将删除的 hash 位置赋值为 null 或 undefined
return true;
}
return false;
}
getTable() {
return this.table;
}
isEmpty() {
return this.size() === 0;
}
size() {
return Object.keys(this.table).length;
}
clear() {
this.table = {};
}
toString() {
if (this.isEmpty()) {
return '';
}
const keys = Object.keys(this.table);
let objString = `{${keys[0]} => ${this.table[keys[0]].toString()}}`;
for (let i = 1; i < keys.length; i++) {
objString = `${objString},{${keys[i]} => ${this.table[keys[i]].toString()}}`;
}
return objString;
}
}
处理冲突的几种方法
- 分离链接
- 线性探查
- 双散列法
对于分离链接和线性探查来说,只需要重写三个方法:put、get 和 remove。
分离链接法包括为散列表的每一个位置创建一个链表并将元素存储在里面。它是解决冲突的最简单的方法,但是在 HashTable 实例之外还需要额外的存储空间。
HashTableSeparateChaining类 :
import { defaultToString } from '../util';
import LinkedList from './linked-list';
import { ValuePair } from './models/value-pair';
export default class HashTableSeparateChaining {
constructor(toStrFn = defaultToString) {
this.toStrFn = toStrFn;
this.table = {};
}
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) {
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));//使用push 方法向 LinkedList 实例中添加一个 ValuePair 实例(键和值)
return true;
}
return false;
}
get(key) {
const position = this.hashCode(key);
const linkedList = this.table[position];
if (linkedList != null && !linkedList.isEmpty()) {//如果该位置上有值存在,我们知道这是一个 LinkedList 实例。现在要做的是迭代这个链表来寻找我们需要的元素
let current = linkedList.getHead();//在迭代之前先要获取链 表表头的引用
while (current != null) {
if (current.element.key === key) {//可以通过current.element.key 来获得Node链表的key属性
return current.element.value;
}
current = current.next;
}
}
return undefined;
}
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;
}
isEmpty() {
return this.size() === 0;
}
size() {
let count = 0;
Object.values(this.table).forEach(linkedList => {
count += linkedList.size();
});
return count;
}
clear() {
this.table = {};
}
getTable() {
return this.table;
}
toString() {
if (this.isEmpty()) {
return '';
}
const keys = Object.keys(this.table);
let objString = `{${keys[0]} => ${this.table[keys[0]].toString()}}`;
for (let i = 1; i < keys.length; i++) {
objString = `${objString},{${keys[i]} => ${this.table[
keys[i]
].toString()}}`;
}
return objString;
}
}
线性探查
线性,是因为它处理冲突的方法是将元素直
接存储到表中,而不是在单独的数据结构中。即当想向表中某个位置添加一个新元素的时候,如果索引为 position 的位置已经被占据了,就尝试 position+1 的位置。如果 position+1 的位置也被占据了,就尝试 position+2 的位 置,以此类推,直到在散列表中找到一个空闲的位置。
第一种方法需要检验是否有必要将一个或多个元素移动到之前的位置。当搜索一个键的时候,这种方法可以避免找到一个空位置。如果移动元素是必要的,我们就需要在散列表中挪动键 值对
import { defaultToString } from '../util';
import { ValuePair } from './models/value-pair';
export default class HashTableLinearProbing {
constructor(toStrFn = defaultToString) {
this.toStrFn = toStrFn;
this.table = {};
}
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) {
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;//声明一个 index 变量并赋值为 position+1
while (this.table[index] != null) {
index++;
}
this.table[index] = new ValuePair(key, value);
}
return true;
}
return false;
}
get(key) {
const position = this.hashCode(key);
if (this.table[position] != null) {
if (this.table[position].key === key) {
return this.table[position].value;
}
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) {
return this.table[position].value;
}
}
return undefined;
}
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;
}
//接收两个参数:被删除的 key 和该 key 被删除的位置。
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++;
}
}
isEmpty() {
return this.size() === 0;
}
size() {
return Object.keys(this.table).length;
}
clear() {
this.table = {};
}
getTable() {
return this.table;
}
toString() {
if (this.isEmpty()) {
return '';
}
const keys = Object.keys(this.table);
let objString = `{${keys[0]} => ${this.table[keys[0]].toString()}}`;
for (let i = 1; i < keys.length; i++) {
objString = `${objString},{${keys[i]} => ${this.table[
keys[i]
].toString()}}`;
}
return objString;
}
}
第二种方法:软删除方法
我们使用一个特殊的值(标记)来表示键值对被删除了(惰性删除或软删除),而不是真的删除它。经过一段时间,散列表被操作过后, 我们会得到一个标记了若干删除位置的散列表。这会逐渐降低散列表的效率,因为搜索键值会 随时间变得更慢。能快速访问并找到一个键是我们使用散列表的一个重要原因。
import { defaultToString } from '../util';
import { ValuePairLazy } from './models/value-pair-lazy';
export default class HashTableLinearProbingLazy {
constructor(toStrFn = defaultToString) {
this.toStrFn = toStrFn;
this.table = {};
}
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) {
if (key != null && value != null) {
const position = this.hashCode(key);
if (
this.table[position] == null
|| (this.table[position] != null && this.table[position].isDeleted)
) {
this.table[position] = new ValuePairLazy(key, value);
} else {
let index = position + 1;
while (this.table[index] != null && !this.table[position].isDeleted) {
index++;
}
this.table[index] = new ValuePairLazy(key, value);
}
return true;
}
return false;
}
get(key) {
const position = this.hashCode(key);
if (this.table[position] != null) {
if (this.table[position].key === key && !this.table[position].isDeleted) {
return this.table[position].value;
}
let index = position + 1;
while (
this.table[index] != null
&& (this.table[index].key !== key || this.table[index].isDeleted)
) {
if (this.table[index].key === key && this.table[index].isDeleted) {
return undefined;
}
index++;
}
if (
this.table[index] != null
&& this.table[index].key === key
&& !this.table[index].isDeleted
) {
return this.table[position].value;
}
}
return undefined;
}
remove(key) {
const position = this.hashCode(key);
if (this.table[position] != null) {
if (this.table[position].key === key && !this.table[position].isDeleted) {
this.table[position].isDeleted = true;
return true;
}
let index = position + 1;
while (
this.table[index] != null
&& (this.table[index].key !== key || this.table[index].isDeleted)
) {
index++;
}
if (
this.table[index] != null
&& this.table[index].key === key
&& !this.table[index].isDeleted
) {
this.table[index].isDeleted = true;
return true;
}
}
return false;
}
isEmpty() {
return this.size() === 0;
}
size() {
let count = 0;
Object.values(this.table).forEach(valuePair => {
count += valuePair.isDeleted === true ? 0 : 1;
});
return count;
}
clear() {
this.table = {};
}
getTable() {
return this.table;
}
toString() {
if (this.isEmpty()) {
return '';
}
const keys = Object.keys(this.table);
let objString = `{${keys[0]} => ${this.table[keys[0]].toString()}}`;
for (let i = 1; i < keys.length; i++) {
objString = `${objString},{${keys[i]} => ${this.table[
keys[i]
].toString()}}`;
}
return objString;
}
}
ES2015 Map 类
可以基于 ES2015的 Map 类开发Dictionary 类。
原生的 Map 类使用:
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')); // true console.log(map.size); // 3 console.log(map.keys()); // 输出{"Gandalf", "John", "Tyrion"} console.log(map.values()); // 输出{"gandalf@email.com", "johnsnow@email.com", "tyrion@email.com"}
console.log(map.get('Tyrion')); // tyrion@email.com
区别:
- ES2015 的 Map 类的 values 方法和 keys 方法都返回Iterator(第 3章提到过),而不是值或键构成的数组。
- 我们实现的 size 方法 返回字典中存储的值的个数,而 ES2015的 Map 类则有一个 size 属性。
- 删除 map 中的元素可以用 delete 方法。 map.delete('John');
ES2105 WeakMap 类和 WeakSet 类
区别:
- WeakSet 或 WeakMap 类相较于Map 和 Set没有 entries、keys 和 values 等方法;
- WeakSet 或 WeakMap 类只能用对象作为键。
优点:
- 性能。WeakSet 和 WeakMap 是弱化的(用对象作为键),没有强引用的键。这使得 JavaScript的垃圾回收器可以从中清除整个入口。
- 必须用键才可以取出值。这些类没有 entries、keys 和 values 等迭代器方法,因此,除非你知道键,否则没有办法取出值。即使用WeakMap 类封装 ES2015类的私有属性。
WeakMap 类也可以用 set 方法,但不能使用数、字符串、布尔值等基本数据类型,
需要将名字转换为对象