集合:由一组无序且唯一的项组成。
Tip:集合 是数学中的概念,但应用在计算机科学的数据结构中。
通常集合有如下方法:
笔者实现如下:
class Set { constructor() { this.items = {} } has(item) { return this.items.hasOwnProperty(item) } add(item) { // 添加之前先检查是否已存在 if (!this.has(item)) { this.items[item] = item; return true; } // 表示没有添加 return false; } delete(item) { // 添加之前先检查是否已存在 if (this.has(item)) { delete this.items[item]; return true; } return false; } size() { // 返回自身可枚举属性组成的数组 return Object.keys(this.items).length } values() { return Object.values(this.items) } clear() { this.item = {} } }
测试:
let set = new Set() console.log(set.add(5)) // true console.log(set.add("5")) // false set.add(6) console.log(set.values()) // [5, 6] console.log(set.size()) // 2
这个实现有一个缺陷,数字 5 和字符串 "5" 被认为是同一个元素。因为属性的键名,即使传入的是数字,最后也会自动转化为字符串,所以 set[5]
和 set["5"]
在 Set 看来是同一个。
Tip:有些同学可能会说,我用数组来表示集合(this.items = []),不就可以区分数字5和字符串"5"。就像这样:
class Set { constructor() { this.items = [] } add(item) { if (!this.items.includes(item)) { this.items.push(item) return true } // 表示没有添加 return false; } }
最终,es6 还是新增了 Set
类型,允许存储任何类型的唯一值。当然,还附有其他特性,比如 Set 类型是可以迭代的...
es6 中的 Set 类的用法,核心和我们的 Set 类很相似。请看示例:
let set = new Set() set.add(2) set.add("2") console.log(set.size) // 2 console.log(set.has(2)) // true set.delete(2) console.log(set.values()) // [Set Iterator] { '2' } set.clear() console.log(set.size) // 0
es6 的 Set 和我们的实现有一些差异。比如:
注:Set 构造函数可以接受所有可迭代对象作为参数,例如数组、Set 集合、Map都是可迭代对象。
数组中的 forEach() 非常好用,所以 es6 也给 Set 集合添加了同样的方法:
new Set(["a", "b", "c"]).forEach(item => console.log(item)) // a // b // c
我们不能像数组一样通过索引访问集合中的元素,如果需要,可以将 Set 集合先转成一个数组:
const set = new Set(["a", "b", "c"]) const arr = [...set] // arr: [ 'a', 'b', 'c' ] console.log('arr: ', arr)
Tip:更多用法请看 mdn Set
我们可以对集合进行如下运算:
并集
:对于两个集合,返回一个包含两个集合中所有元素的新集合交集
:对于两个集合,返回一个包含两个集合中共有元素的新集合差集
:对于两个集合,返回一个包含所有存在于第一个集合且不存在于第二个集合的元素的新集合子集
:如果一个集合(S1)中的每个元素都在另一个集合(S2)中,则 S1 是 S2 的子集超集
:如果一个集合(S2)中的每个元素都在另一个集合(S1)中,而 S1 中可能包含 S2 中没有的元素,则 S1 就是 S2 的一个超集// 并集 function union(setA, setB) { let _union = new Set(setA); for (let elem of setB) { _union.add(elem); } return _union; } // 交集 function intersection(setA, setB) { let _intersection = new Set(); for (let elem of setB) { if (setA.has(elem)) { _intersection.add(elem); } } return _intersection; } // 差集 function difference(setA, setB) { let _difference = new Set(setA); for (let elem of setB) { _difference.delete(elem); } return _difference; } // 超集 function isSuperset(set, subset) { for (let elem of subset) { if (!set.has(elem)) { return false; } } return true; }
测试:
const set1 = new Set([2, 3, 4]) const set2 = new Set([3, 4, 5]) const set3 = new Set([3, 4]) // Set(4) { 2, 3, 4, 5 } console.log(union(set1, set2)) // Set(2) { 3, 4 } console.log(intersection(set1, set2)) // Set(1) { 2 } console.log(difference(set1, set2)) // true console.log(isSuperset(set1, set3))
有一种计算并集、交集、差集和超集的简便方法,就是使用 扩展运算符
。重写以上方法:
function union(setA, setB) { return new Set([...set1, ...set2]) } function intersection(setA, setB) { return new Set([...setA].filter(item => setB.has(item))) } function difference(setA, setB) { return new Set([...setA].filter(item => !setB.has(item))) } function isSuperset(set, subset) { return [...subset].every(item => set.has(item)) }
将对象存储在 Set 的实例中与存储在变量中完全一样,只要 Set 实例中的引用存在,垃圾回收机制就不能释放该对象的空间:
let set = new Set() let key = {} set.add(key) key = null // {1} console.log(set.size) // 1 {2}
但有时,我们希望当其他所有引用都不存在时(行{1}),Set 中的这些引用也随之消失(行{2} 输出为0),于是 es6 新增了 WeakSet
类型,与 Set 用法类似,最主要区别是 WeakSet 保存的是对象的弱引用。重写上面的示例:
let set = new WeakSet() let key = {} set.add(key) // 移除对象 key 的最后一个强引用(Weak Set 中的引用也会自动移除) key = null
测试难以进行下去,但你要相信 javascript 引擎。
WeakSet 集合与普通 Set 集合还有以下几个差异:
字典和集合很相似,集合以 [值,值]
的形式存储元素,字典则以 [键,值]
的形式存储元素。
集合中,我们感兴趣的是每个值本身,并将它当作主元素;而对于字典,我们通常通过键名来查询特定的元素。
现实生活中,这种数据结构的应用有:字典、通讯录等等
通常字典有如下方法:
[键,值]
对以数组形式返回class Dictionary { constructor() { this.table = {}; } set(key, value) { if (key != null && value != null) { this.table[key] = value; return true; } return false; } get(key) { return this.table[key] } has(key) { return this.table.hasOwnProperty(key) } remove(key) { if (this.hasKey(key)) { delete this.table[key]; return true; } return false; } values() { return Object.values(this.table) } keys() { return Object.keys(this.table) } keyValues() { // 可以在对象字面量中使用可计算属性名,语法是使用方括号([]) return this.keys().map(key => ({ [key]: this.table[key] })) } isEmpty() { return this.size() === 0; } size() { return Object.keys(this.table).length; } clear() { this.table = {}; } }
测试:
const d = new Dictionary() d.set('age', 18) d.set('name', 'aaron') console.log(d) // Dictionary { table: { age: 18, name: 'aaron' } } console.log(d.hasKey('age')) // true console.log(d.size()) // 2 console.log(d.keys(), d.values()) // [ 'age', 'name' ] [ 18, 'aaron' ] console.log(d.keyValues()) // [ { age: 18 }, { name: 'aaron' } ]
Tip:字典也叫关联数组,或许在 javascript 中允许我们使用方括号([]
)获取对象的属性。
es6 中的 Map 的用法和我们的 Dictionary 类很相似。请看示例:
const map = new Map() map.set('age', 18) map.set('name', 'aaron') console.log(map.get('age')) // 18 console.log(map.has('age')) // true map.delete('name') console.log(map.size) // 1
es6 的 Map 和我们的实现有一些差异。比如:
注:Map 的键名和值都可以是任意类型,而且可以给 Map 构造函数传入数组来初始化:
const map = new Map([['age', 18], ['name', 'aaron']]) map.set({}, 1) map.set({}, null) console.log(map.size) // 4
Map 的 forEach() 与 Set 和 Array 中的 forEach() 方法类似,回调函数都支持 3 个参数:
const map = new Map([['age', 18], ['name', 'aaron']]) map.forEach((value, key, aMap) => { console.log(key + ' ' + value) }) // age 18 // name aaron const set = new Set(['age', 'name']) set.forEach((value, key, aMap) => { console.log(key + ' ' + value) }) // age age // name name const arr = new Array('age', 'name') arr.forEach((value, key, aMap) => { console.log(key + ' ' + value) }) // 0 age // 1 name
Tip:更多用法请看 mdn Map
WeakSet 是弱引用 Set 集合,相对的,WeakMap 是弱引用 Map,也用于存储对象的弱引用。请看示例:
let map = new WeakMap() let key = {} map.set(key, 'empty object') key = null // 此时 WeakMap 为空
WeakMap 与普通 Map 还有以下几个差异:
注:WeakMap 和 一种特殊的 Map,同样的,WeakSet 也是一种特殊的 Set,存放的都是对象的弱引用,当该对象的强引用都被清除时,弱引用的键以及对应的值也会自动被垃圾回收。
散列表 和字典一样,也是以 [键,值]
对的形式存储数据。但是键名需要通过一个函数(称之为散列函数)转换一下。
Tip:通常要在一个数据结构中获取一个值,就需要迭代整个数据结构来查找,而如果通过散列函数,就能知道值的具体位置,因此能够快速的查找该值。
以下是一个简单的散列表,只包括添加、查找和删除:
class HashTable { constructor() { this.table = {} } // 散列函数 hashCode(key) { // 将每个字符的 Unicode 编码值相加 const hash = [...String(key)].reduce((pre, curr) => curr.codePointAt(0) + pre, 0) return hash // 避免操作数超过变量最大范围 // return hash % 100 } put(key, value) { if (key != null && value != null) { const position = this.hashCode(key); this.table[position] = value return true; } return false; } get(key) { return this.table[this.hashCode(key)] } remove(key) { const hash = this.hashCode(key) if (this.table.hasOwnProperty(hash)) { delete this.table[hash] return true } return false } }
测试:
let h = new HashTable() h.put('abc', 'abc') h.put('a', 'a') console.log(h) // HashTable { table: { '97': 'a', '294': 'abc' } } console.log(h.remove('a')) // true console.log(h.get('abc')) // abc
有时候,一些键会返回相同的值:
let h = new HashTable() h.put('abc', 'abc') // {1} h.put('cba', 'cba') // {2} console.log(h) // HashTable { table: { '294': 'cba' } }
由于不同的键(abc 和 cba)都对应着 294,所以造成了数据的丢失。
显然,使用一个数据结构来保存数据的目的不是丢失这些数据,因此,我们需要解决冲突。
解决冲突最简单的方法是 分离链接
法,即散列表中不直接存储 value,而是存储一个链表数据结构,在将 value 存入链表中。示意如下:
class HashTableSeparateChaining { constructor(toStrFn = defaultToString) { this.table = {}; } hashCode(key) {} put(key, value) { if (key != null && value != null) { const position = this.hashCode(key); if (this.table[position] == null) { // LinkedList 是链表数据结构 this.table[position] = new LinkedList(); } this.table[position].push(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 === value) { return current.element; } 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 === value) { linkedList.remove(current.element); if (linkedList.isEmpty()) { delete this.table[position]; } return true; } current = current.next; } } return false; } }
Tip:一个良好的散列函数应该有较低冲突的可能性