集合
集合是由一组无序且唯一(即不能重复)的项组成的
创建集合类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
| class Set { constructor() { this.items = {}; } has(element) { return element in items; } add(element) { if (!this.has(element)) { this.items[element] = element; return true; } return false; } delete(element) { if (this.has(element)) { delete this.items[element]; return true; } return false; } clear() { this.items = {}; } size() { return Object.keys(this.items).length; } values() { return Object.values(this.items); } }
|
集合运算
(1)并集:对于给定的两个集合,返回一个包含两个集合中所有元素的新集合。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| union(otherSet) { const unionSet = new Set(); this.values().forEach(value => unionSet.add(value)); otherSet.values().forEach(value => unionSet.add(value)); return unionSet; } const setA = new Set(); setA.add(1); setA.add(2); setA.add(3); const setB = new Set(); setB.add(3); setB.add(4); setB.add(5); setB.add(6); const unionAB = setA.union(setB); console.log(unionAB.values());
|
(2)交集:对于给定的两个集合,返回一个包含两个集合中共有元素的新集合。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| intersection(otherSet) { const intersectionSet = new Set(); const values = this.values(); for (let i = 0; i < values.length; i++) { if (otherSet.has(values[i])) { intersectionSet.add(values[i]); } } return intersectionSet; }
const setA = new Set(); setA.add(1); setA.add(2); setA.add(3); const setB = new Set(); setB.add(2); setB.add(3); setB.add(4); const intersectionAB = setA.intersection(setB); console.log(intersectionAB.values());
intersection(otherSet) { const intersectionSet = new Set(); const values = this.values(); const otherValues = otherSet.values(); let biggerSet = values; let smallerSet = otherValues;
if (otherValues.length - values.length > 0) { biggerSet = otherValues; smallerSet = values; } smallerSet.forEach(value => { if (biggerSet.includes(value)) { intersectionSet.add(value); } }); return intersectionSet; }
|
(3)差集:对于给定的两个集合,返回一个包含所有存在于第一个集合且不存在于第二个集合的元素的新集合。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| difference(otherSet) { const differenceSet = new Set(); this.values().forEach(value => { if (!otherSet.has(value)) { differenceSet.add(value); } }); return differenceSet; }
const setA = new Set(); setA.add(1); setA.add(2); setA.add(3); const setB = new Set(); setB.add(2); setB.add(3); setB.add(4); const differenceAB = setA.difference(setB); console.log(differenceAB.values());
|
(4)子集:验证一个给定集合是否是另一集合的子集
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| isSubsetOf(otherSet) { if (this.size() > otherSet.size()) { return false; } let isSubset = true; this.values().every(value => { if (!otherSet.has(value)) { isSubset = false; return false; } return true; }); return isSubset; }
const setA = new Set(); setA.add(1); setA.add(2); const setB = new Set(); setB.add(1); setB.add(2); setB.add(3); const setC = new Set(); setC.add(2); setC.add(3); setC.add(4); console.log(setA.isSubsetOf(setB));
console.log(setA.isSubsetOf(setC));
|
使用扩展运算符
1 2 3 4 5 6 7 8
| console.log(new Set([...setA, ...setB]));
console.log(new Set([...setA].filter((x) => setB.has(x))));
console.log(new Set([...setA].filter((x) => !setB.has(x))));
|
字典和散列表
字典
在字典中,存储的是[键,值]对,其中键名是用来查询特定元素的。
字典和集合很相似,集合以[值,值]的形式存储元素,字典则是以[键,值]的形式来存储元素。
字典也称作映射、符号表或关联数组
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94
| 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(); }
class ValuePair { constructor(key, value) { this.key = key; this.value = value; } toString() { return `[#${this.key}: ${this.value}]`; } }
import { defaultToString } from '../util'; export default class Dictionary { constructor(toStrFn = defaultToString) { this.toStrFn = toStrFn; this.table = {}; } hasKey(key) { return this.table[this.toStrFn(key)] != null; } set(key, value) { if (key != null && value != null) { const tableKey = this.toStrFn(key); this.table[tableKey] = new ValuePair(key, value); return true; } return false; } remove(key) { if (this.hasKey(key)) { delete this.table[this.toStrFn(key)]; return true; } return false; } get(key) { const valuePair = this.table[this.toStrFn(key)]; return valuePair == null ? undefined : valuePair.value; } keyValues() { return Object.values(this.table); } keys() { return this.keyValues().map((valuePair) => valuePair.key); } values() { return this.keyValues().map((valuePair) => valuePair.value); } 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; } } } 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; } }
|
散列表
散列算法的作用是尽可能快地在数据结构中找到一个值
散列函数的作用是给定一个键值,然后返回值在表中的地址
散列表有一些在计算机科学中应用的例子:
(1)用来对数据库进行索引。当我们在关系型数据库(如 MySQL、Microsoft SQL Server、Oracle,等等)中创建一个新的表时,一个不错的做法是同时创建一个索引来更快地查询到记录的 key。在这种情况下,散列表可以用来保存键和对表中记录的引用
(2)使用散列表来表示对象。JavaScript 语言内部就是使用散列表来表示每个对象。此时,对象的每个属性和方法(成员)被存储为 key 对象类型,每个 key 指向对应的对象成员。
散列函数 —- lose lose 散列函数

HashTable 和 Dictionary 类很相似。不同之处在于在 Dictionary 类中,我
们将 valuePair 保存在 table 的 key 属性中(在它被转化为字符串之后),而
在 HashTable 类中,我们由 key(hash)生成一个数,并将 valuePair 保存
在 hash 位置(或属性)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58
| 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(key) { return this.loseloseHashCode(key); } 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) { 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; } }
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');
|
散列集合
散列集合由一个集合构成,但是插入、移除或获取元素时,使用的是 hashCode 函数
散列集合和散列表的不同之处在于,不再添加键值对,而是只插入值而没有键。例如,可以使用散列集合来存储所有的英语单词(不包括它们的定义)。和集合相似,散列集合只存储不重复的唯一值。
散列表中的冲突
有时候,一些键会有相同的散列值。不同的值在散列表中对应相同位置的时候,我们称其为冲突
处理冲突有几种方法:分离链接、线性探查和双散列法。
(1)分离链接
分离链接法包括为散列表的每一个位置创建一个链表并将元素存储在里面。
它是解决冲突的最简单的方法,但是在 HashTable 实例之外还需要额外的存储空间

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62
| class HashTableSeparateChaining { constructor(toStrFn = defaultToString) { this.toStrFn = toStrFn; this.table = {}; } 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(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(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; } }
|
(2)线性探查
它处理冲突的方法是将元素直接存储到表中,而不是在单独的数据结构中
当想向表中某个位置添加一个新元素的时候,如果索引为 position 的位置已经被占据了,就尝试 position+1 的位置。如果 position+1 的位置也被占据了,就尝试 position+2 的位置,以此类推,直到在散列表中找到一个空闲的位置

线性探查技术分为两种。
第一种是软删除方法。我们使用一个特殊的值(标记)来表示键值对被删除了(惰性删除或软删除),而不是真的删除它。经过一段时间,散列表被操作过后,我们会得到一个标记了若干删除位置的散列表。这会逐渐降低散列表的效率,因为搜索键值会随时间变得更慢。能快速访问并找到一个键是我们使用散列表的一个重要原因

源代码
第二种方法需要检验是否有必要将一个或多个元素移动到之前的位置。当搜索一个键的时候,这种方法可以避免找到一个空位置。如果移动元素是必要的,我们就需要在散列表中挪动键值对。

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86
| class HashTableSeparateChainingMoveKey { constructor(toStrFn = defaultToString) { this.toStrFn = toStrFn; this.table = {}; } 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(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; } 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++; } } }
|
更好的散列函数
一个表现良好的散列函数是由几个方面构成的:插入和检索元素的时间(即性能),以及较低的冲突可能性。
1 2 3 4 5 6 7 8 9 10 11 12
| 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 类
1 2 3 4 5 6 7 8 9 10 11
| 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()); "tyrion@email.com"} console.log(map.get('Tyrion')); map.delete('John');
|
ES2105 WeakMap 类和 WeakSet 类
Map 和 Set 与其弱化版本之间仅有的区别是:
(1)WeakSet 或 WeakMap 类没有 entries、keys 和 values 等方法
(2)WeakSet 和 WeakMap 只能用对象作为键
1 2 3 4 5 6 7 8 9 10
| const map = new WeakMap(); const ob1 = { name: 'Gandalf' }; const ob2 = { name: 'John' }; const ob3 = { name: 'Tyrion' }; map.set(ob1, 'gandalf@email.com'); map.set(ob2, 'johnsnow@email.com'); map.set(ob3, 'tyrion@email.com'); console.log(map.has(ob1)); console.log(map.get(ob3)); map.delete(ob2);
|
注意:WeakMap 类也可以用 set 方法,但不能使用数、字符串、布尔值等基本数据类型,需要将名字转换为对象