学习github地址
今天来学习使用字典和散列表来存储唯一值(不重复的值)的数据结构
字典
在字典中,存储的是[键,值]对,其中键名是用来查询特定元素的, 字典也称作映射、符号表或关联数组, 在计算机科学中, 字典经常用来保存对象的引用地址
- 创建字典类
与 Set 类相似,ECMAScript6 同样包含了一个 Map 类的实现,即我们所说的字典
今天要实现的类就是以 ES6 中 Map 类的实现为基础的。你会发现它和 Set 类很相似,但不同于存储[值,值]对的形式,我们将要存储的是[键,值]对
在字典中,理想的情况是用字符串作为键名,值可以是任何类型, 但是,由于 JavaScript 不是强类型的语言,我们不能保证键一定是字符串。我们需要把所有作为键名传入的对象转化为字符串
// 转化为字符串
const 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;
};
// 打一个骨架
class Dictionary {
constructor(toStrFn = defaultToString) {
this.toStrFn = toStrFn;
this.table = {};
}
}
// 不是只将 value 保存在字典中,而是要保存两个值:原始的key 和 value
class ValuePair {
constructor(key, value) {
this.key = key;
this.value = value;
}
toString() {
return `[#${this.key}: ${this.value}]`;
}
}
-
声明一些字典所使用的方法
-
set(key,value):
向字典中添加新元素。如果 key 已经存在,那么已存在的 value 会被新的值覆盖 -
remove(key):
通过使用键值作为参数来从字典中移除键值对应的数据值 -
hasKey(key):
如果某个键存在与字典中, 返回 true, 否则返回 false -
get(key):
通过键值为参数查找特定的值返回 -
clear():
删除字典中的所有值 -
size():
返回字典包含值的数量 -
isEmpty():
在 size 等于 0 的时候为 true -
keys():
将字典所包含的所有键名以数组形式返回 -
values():
将字典所包含的所有数值以数组形式返回 -
keyValues():
将字典中所有[键,值]以数组形式返回 -
forEach(callbackFn):
迭代字典所有的键值对,callbackFn
有两个参数: key 和 value. 该方法可以在回调函数返回 false 时被中止
-
实现上面的方法
// 字典的实现
class Dictionary {
constructor(toStrFn = defaultToString) {
this.toStrFn = toStrFn;
this.table = {};
}
// 已经存在一个给定键名的键值对(表中的位置不是 null 或 undefined),那么返回 true
hasKey(key) {
return this.table[this.toStrFn(key)] != null;
}
set(key, value) {
if (key != null && value != null) {
const tableKey = this.toStrFn(key); // 获取 key 的字符串
this.table[tableKey] = new ValuePair(key, value); // 创建一个新的键值对并将其赋值给 table 对象上的 key属性(tableKey)
return true;
}
return false;
}
// 检索一个值
get(key) {
// 首先检索存储在给定key 属性中的对象,
const valuePair = this.table[this.toStrFn(key)];
return valuePair == null ? undefined : valuePair.value;
}
// 这个也可以检索一个值: 获取两次key的字符串以及访问两次 table 对象
get2(key) {
if (this.hasKey(key)) {
return this.table[this.toStrFn(key)].value;
}
return undefined;
}
remove(key) {
if (this.hasKey(key)) {
delete this.table[this.toStrFn(key)];
return true;
}
return false;
}
keyValues() {
return Object.values(this.table);
}
// 兼容版
keyValues2() {
const valuePairs = [];
for (const key in this.table) {
if (this.hasKey(key)) {
valuePairs.push(this.table[key]);
}
}
return valuePairs;
}
keys() {
return this.keyValues().map((i) => i.key);
}
values() {
return this.keyValues().map((i) => i.value);
}
// 迭代字典中的每个键值对
forEach(callbackFn) {
const valuePairs = this.keyValues();
for (let i = 0; i < valuePairs.length; i++) {
// 执行以参数形式传入 forEach 方法的 callbackFn 函数, 保存结果,
const result = callbackFn(valuePairs[i].key, valuePairs[i].value);
// 回调函数返回了 false,我们会中断 forEach 方法的执行
if (result === false) {
break;
}
}
}
size() {
return Object.keys(this.table).length;
}
isEmpty() {
return this.size() === 0;
}
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()}`;
}
return objString;
}
}
散列表
HashTable 类,也叫 HashMap 类,它是 Dictionary 类的一种散列表实现方式, 散列算法的作用是尽可能快地在数据结构中找到一个值, 散列函数的作用是给定一个键值,然后返回值在表中的地址
-
应用
- 它是字典的一种实现,所以可以用作关联数组
- 也可以用来对数据库进行索引, 创建一个索引来更快地查询到记录的 key
- 使用散列表来表示对象, JavaScript 语言内部就是使用散列表来表示每个对象
创建散列表
使用一个关联数组(对象)来表示我们的数据结构,和我们在 Dictionary 类中所做的一样
// 打一个骨架, 和字典差不多
class HashTable {
constructor(toStrFn = defaultToString) {
this.toStrFn = toStrFn;
this.table = {};
}
}
-
给类添加方法
-
put(key, value):
向散列表增加一个新的项(也能更新散列表) -
remove(key):
根据键值从散列表中移除值 -
get(key):
返回根据键值检索到的特定的值
-
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; // 防止 key 是一个对象而不是字符串。我们需要一个 hash 变量来存储这个总和
// charCodeAt() 方法可返回指定位置的字符的 Unicode 编码,返回值是 0 - 65535 之间的整数
// 遍历 key 并将从 ASCII表中查到的每个字符对应的 ASCII 值加到 hash 变量中
for (let i = 0; i < tableKey.length; i++) {
hash += tableKey.charCodeAt(i);
}
// 为了得到比较小的数值,我们会使用 hash 值和一个任意数做除法的余数(%)——这可以规避操作数超过数值变量最大表示范围的风险。
return hash % 37;
}
hashCode(key) {
return this.loseloseHashCode(key);
}
// 将键和值加入散列表
put(key, value) {
if (key != null && value != null) {
//对于给出的 key 参数,我们需要用所创建的 hashCode 函数在表中找到一个位置
const position = this.hashCode(key);
// 为了信息备份将原始的 key 保存下来
this.table[position] = new ValuePair(key, value);
}
}
// 从散列表中获取一个值
get(key) {
const valuePair = this.table[this.hashCode(key)];
return valuePair == null ? undefined : valuePair.value;
}
// 移除一个值
remove(key) {
const hash = this.hashCode(key);
const valuePair = this.table[hash];
if (valuePair != null) {
delete this.table[hash];
return true;
}
return false;
}
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;
}
}
// 使用 HashTable
const hash = new HashTable();
hash.put("Gandalf", "[email protected]");
hash.put("John", "[email protected]");
hash.put("Tyrion", "[email protected]");
console.log(hash.hashCode("Gandalf") + " - Gandalf"); // 19 - Gandalf
console.log(hash.hashCode("John") + " - John"); // 29 - John
console.log(hash.hashCode("Tyrion") + " - Tyrion"); // 16 - Tyrion
- 散列表和散列集合
在一些编程语言中,还有一种叫作散列集合的实现。散列集合由一个集合构成,但是插入、移除或获取元素时,使用的是 hashCode 函数, 可以使用散列集合来存储所有的英语单词(不包括它们的定义)。和集合相似,散列集合只存储不重复的唯一值
- 处理散列表中的冲突
有时候,一些键会有相同的散列值。不同的值在散列表中对应相同位置的时候,我们称其为冲突
const hash = new HashTable();
hash.put("Ygritte", "[email protected]"); // 4
hash.put("Jonathan", "[email protected]"); // 5
hash.put("Jamie", "[email protected]"); // 5
hash.put("Jack", "[email protected]"); // 7
hash.put("Jasmine", "[email protected]"); // 8
hash.put("Jake", "[email protected]"); // 9
hash.put("Nathan", "[email protected]"); // 10
hash.put("Athelstan", "[email protected]"); //7
hash.put("Sue", "[email protected]"); // 5
hash.put("Aethelwulf", "[email protected]"); // 5
hash.put("Sargeras", "[email protected]"); // 10
- 处理冲突有几种方法:分离链接、线性探查和双散列法 (了解为主, 后续补充)
ES6 Map 类
ECMAScript 2015 新增了 Map 类。可以基于 ES2015 的 Map 类开发我们的 Dictionary 类
ES6 WeakMap 和 WeakSet 类
- Map 和 Set 与其弱化版本之间仅有的区别是:
- WeakSet 或 WeakMap 类没有 entries、keys 和 values 等方法
- 只能用对象作为键