JavaScript数据结构——集合的实现与应用
与数学中的集合概念类似,集合由一组无序的元素组成,且集合中的每个元素都是唯一存在的。可以回顾一下中学数学中集合的概念,我们这里所要定义的集合也具有空集(即集合的内容为空)、交集、并集、差集、子集的特性。
在es6中,原生的set类已经实现了集合的全部特性,稍后我们会介绍它的用法。
我们使用javasctipt的对象来表示集合,下面是集合类的主要实现方法:
class set { constructor () { this.items = {}; } add (value) { // 向集合中添加元素 if (!this.has(value)) { this.items[value] = value; return true; } return false; } delete (value) { // 从集合中删除对应的元素 if (this.has(value)) { delete this.items[value]; return true; } return false; } has (value) { // 判断给定的元素在集合中是否存在 return this.items.hasownproperty(value); } clear() { // 清空集合内容 this.items = {}; } size () { // 获取集合的长度 return object.keys(this.items).length; } values () { // 返回集合中所有元素的内容 return object.values(this.items); } }
在使用javascript对象{ }来表示集合时,我们可以像操作数组一样通过[ ]来设置和获取集合内元素的值。通过这种方式,在设置集合元素的值时,如果元素不存在,则创建一个新元素,如果元素存在,则修改对应的值;在获取集合元素的值时,如果元素存在,则返回对应的值,如果元素不存在,则返回undefined。此外,javascript对象还提供了一些基础方法以方便我们对集合进行一些操作,例如hasowenproperty()方法返回一个表明对象是否具有特定属性的布尔值,object.keys()方法返回指定对象的所有属性名称的数组,object.values()方法方法指定对象的所有属性值的数组。
上述代码很简单,这里就不再详细解释了。下面是一些测试用例和测试结果:
let set = new set(); set.add(1); console.log(set.values()); // [ 1 ] console.log(set.has(1)); // true console.log(set.size()); // 1 set.add(2); console.log(set.values()); // [ 1, 2 ] console.log(set.has(2)); // true console.log(set.size()); // 2 set.delete(1); console.log(set.values()); // [ 2 ] set.delete(2); console.log(set.values()); // []
下面我们来看看集合的数学运算:并集、交集、差集、子集。
并集
对于给定的两个集合,并集返回一个包含两个集合中所有元素的新集合。示意图如下:
并集的实现代码:
union (otherset) { // 并集 let unionset = new set(); this.values().foreach(value => unionset.add(value)); otherset.values().foreach(value => unionset.add(value)); return unionset; }
首先遍历第一个集合,将所有的元素添加到新集合中,然后再遍历第二个集合,将所有的元素添加到新集合中。然后返回新集合。不用担心会添加重复的元素,因为集合的add()方法会自动排除掉已添加的元素。
测试用例及结果:
let seta = new set(); seta.add("first"); seta.add("second"); seta.add("third"); let setb = new set(); setb.add("third"); setb.add("fourth"); setb.add("fifth"); setb.add("sixth"); console.log(seta.union(setb).values()); // [ 'first', 'second', 'third', 'fourth', 'fifth', 'sixth' ]
交集
对于给定的两个集合,交集返回一个包含两个集合*有元素的新集合。示意图如下:
交集的实现代码:
intersection (otherset) { // 交集 let intersectionset = new set(); this.values().foreach(value => { if (otherset.has(value)) intersectionset.add(value); }); return intersectionset; }
遍历第一个集合,如果元素出现在第二个集合中,则将它添加到新集合。然后返回新集合。
测试用例及结果:
let seta = new set(); seta.add("first"); seta.add("second"); seta.add("third"); let setb = new set(); setb.add("second"); setb.add("third"); setb.add("fourth"); console.log(seta.intersection(setb).values()); // [ 'second', 'third' ]
差集
对于给定的两个集合,差集返回一个包含所有存在于第一个集合且不存在于第二个集合的元素的新集合。示意图如下:
差集的实现代码:
difference (otherset) { // 差集 let differenceset = new set(); this.values().foreach(value => { if (!otherset.has(value)) differenceset.add(value); }); return differenceset; }
遍历第一个集合,如果元素没有出现在第二个集合中,则将它添加到新集合。然后返回新集合。
测试用例及结果:
let seta = new set(); seta.add("first"); seta.add("second"); seta.add("third"); let setb = new set(); setb.add("second"); setb.add("third"); setb.add("fourth"); console.log(seta.difference(setb).values()); // [ 'first' ]
子集
验证一个给定集合是否是另一个集合的子集,即判断给定的集合中的所有元素是否都存在于另一个集合中,如果是,则这个集合就是另一个集合的子集,反之则不是。示意图如下:
子集的实现代码:
subset (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; }
如果集合a比集合b的长度大,则直接返回false,因为这种情况a不可能是b的子集。然后使用every()函数遍历集合a的所有元素,一旦碰到其中的元素没有在集合b中出现,则直接返回false,并终止遍历。这里我们没有使用foreach来遍历集合a,是因为你无法根据某个条件来终止foreach循环。考虑下面这种情况:
var arr = ["first", "second", "third", "fourth"]; arr.foreach(item => { if(item === "third") return true; console.log(item); });
输出结果是:
first second fourth
很显然,这里的return true语句并不能退出foreach循环,它只能保证本次循环中余下的语句不被执行,而接下来其它的元素还是会被遍历到。
在我们的subset()方法中,如果使用foreach语句,每一次都会遍历集合中的所有元素,如果遇到其中的元素没有在集合b中出现,就将issubset变量的值设置为false,但并不能退出foreach,issubset变量的值可能会被多次覆盖。为了提高执行效率,推荐使用every()函数,它会遍历集合中的元素,直到其中一个返回结果为false,就终止遍历,并返回false,否则就遍历所有的元素并返回true。有关every()函数的详细介绍可以看这里。与every()函数功能相似还有一个some()函数,它在遍历集合的过程中,遇到返回结果为true时就终止遍历,具体内容可以看这里。
差集的测试用例及结果:
let seta = new set(); seta.add("first"); seta.add("second"); let setb = new set(); setb.add("first"); setb.add("second"); setb.add("third"); let setc = new set(); setc.add("second"); setc.add("third"); setc.add("fourth"); console.log(seta.subset(setb)); // true console.log(seta.subset(setc)); // false
文章的开头说过,es6提供了原生的set类,让我们来看看它的一些使用方法:
let set = new set(); set.add(1); set.add(2); set.add(3); console.log(set.values()); // [set iterator] { 1, 2, 3 } console.log(set.has(1)); // true console.log(set.size); // 2 set.delete(1); console.log(set.values()); // [set iterator] { 2, 3 } set.clear(); console.log(set.values()); // [set iterator] { }
和前面我们自定义的set类稍微有一点不同,values()方法返回的不是一个数组,而是iterator迭代器。另一个就是这里的size是一个属性而不是方法,其它部分都和我们前面定义的set类相同。由于es6的set类不包含对集合的数学运算,我们可以按照前面我们提供的方法来对其进行扩充。
下一章我们将介绍如何用javascript来实现字典和散列表。
上一篇: 曾经的学生长大了,做了设计师
下一篇: 使用PixiJS做一个小游戏