集合数据结构的概念
集合(Set,或简称集),是数学中的概念,是指具有某个特定性质的事物的总体,里面的每个事物称为集合的一个元素。如所有的自然数就是一个集合,在数学中的表示为:N = {0, 1, 2, 3, 4, 5, 6, …}。不仅仅是数字,集合的元素可以是任何事物,可以是人,可以是物,也可以是字母或数字等。如下图所示,一些多边形凑在一起也组成了一个集合。创建一个集合其实就是把事物进行归类,一类东西就是一个集合。
一个包括一些多边形的集合
集合具有的特性包括:
- 无序性:集合里的东西是没有属性的,这也是其作为一种数据结构与列表的不同。如动物(集合)包括:狗、猫、鱼……,这里各种动物的罗列是没有先后关系的。
- 互异性:同一个集合里的元素是不相同的,即每个只出现一次。如动物(集合)包括:狗、狗、猫。鱼……,这里说了两遍狗,是不行的。
- 确定性:对于一个集合,某个元素那么在这个集合里,要么不在这个集合里,在不在的结果是确定的。如“大象”在不在动物集合里?“杯子”在不在动物集合里。
数据结构中的集和数学中的集是一样的,在集中,数据项是无序的,也不允许存在相同数据项。集支持添加、删除和查找项目。一些语言内建对集的支持,而在其它语言中,可以利用散列表实现集,JavaScript中即可以用对象来实现。
对象实现集合
JavaScript中有原生就有Set类,但是这也是语言给我们封装好的,虽然可以直接拿来用,但是作为学习,有必要自己来实现Set,而不是仅仅会使用而已。上面我们已经对集合的概念有了认识,要实现集合,那首先要确定集合应该具有哪些操作。
add(element):向集合中添加一个新元素delete(element):删除一个元素clear():清空整个集合has(element):集合中有没有某个元素,返回Booleansize():集合中有多少元素,返回数量,同数组的lengthvalues:将集合返回成一个数组,数组包含结合所有的元素
class Set {
constructor() {
this.items = {};
}
has(element) {
//return element in this.items;
// return this.items.hasOwnProperty(element);
return Object.prototype.hasOwnProperty.call(this.items, element)
}
add(element) {
if (!this.has(element)) {
this.items[element] = element;
return true;
}
return false;
}
delete(element) {
if (!this.has(element)) {
return false;
} else {
delete this.items[element];
return true;
}
}
clear() {
this.items = {}
}
size() {
let counter = 0;
for (let key in this.items) {
//这里还要验证集合(对象)中自己有该属性,而不是原型上的属性
if (this.items.hasOwnProperty(key)) {
counter++;
}
}
return counter;
}
values() {
// return Object.values(this.items)
let result = [];
for (let key in this.items) {
if (this.items.hasOwnProperty(key)) {
result.push(key)
}
}
return result;
}
}
在JavaScript中,利用对象来自己实现集合非常容易,因为JavaScript中已经内置了大量好用的对象的方法。
obj.hasOwnProperty(property)/property in obj:对象中是否有某个元素(属性),返回BooleanObject.values(obj),Object.keys(obj):对象变成数组for in:遍历的是对象的属性
for (x in object) {
statement
}
//里面的x表示属性
对象的方法有很多,之前的文章对数组的常用方法已经有了一个梳理,并且自己去实现这些方法。但对于对象的很多方法,有时间再认真捋一遍,以及对象和数组之间的关系,虽然数组也是对象,但在使用中(API boy),两者还是有些区别。
集合的运算
数学中的集合运算
集合源于数学中的概念,这里数据结构集合的元素也是和数学是一模一样的。包括并集、交集、差集、子集。
集合的运算
- 并集(unionSet):对于给定的两个集合,返回一个包含两个集合中所有元素的新集合。
- 交集():对于给定的两个集合,返回一个包含两个集合中共有元素的新集合。
- 差集:对于给定的两个集合,返回一个包含所有存在于第一个集合且不存在于第二个集 合的元素的新集合。
- 子集:验证一个给定集合是否是另一集合的子集。
集合运算JavaScript的实现
//并集:创建一个新集合,把要运算的两个集合都add到新集合里去
union(otherSet) {
const unionSet = new Set();
this.values().forEach(x => unionSet.add(x));
otherSet.values().forEach(x => unionSet.add(x));
return unionSet;
}
//交集:创建一个新集合,两重循环遍历(短的在外侧,为性能优化),将重复的add到新集合中
intersection(otherSet) {
const intersectionSet = new Set();
const values = this.values();
const otherValues = otherSet.values();
let bigSet = values;
let smallSet = otherValues;
if (values.length < otherValues.length) {
bigSet = otherValues;
smallSet = values;
}
smallSet.forEach(x => {
if (bigSet.includes(x)) {
intersectionSet.add(x)
}
})
return intersectionSet;
}
//差集:创建一个新集合,把在集合1中而不在集合2中的元素add到新集合中
difference(otherSet) {
const differenceSet = new Set();
let values = this.values();
values.forEach(x => {
if (!otherSet.has(x)) {
differenceSet.add(x)
}
})
return differenceSet;
}
//子集:返回的是Boolean
isChildOf(otherSet) {
//这里的if判断不要也可以,加上是为了性能优化,更快的判断为错
if(this.size > otherSet.size) {
return false;
}
const values = this.values();
return values.every(x => otherSet.has(x))
}
实现结合的这些运算方法并不难,并且有很多种方法都可以,但逻辑都是一样的。在JavaScript中,实现这些逻辑,尤其是数组的遍历操作上,上面的实现过程中反复用到了数组的迭代方法,如forEach。这些方法是函数式编程的基础。至于函数式编程与OOP编程,这是一个庞大的话题,没有大量的积累是无法去体会的,这里暂且有函数式编程这个概念即可。同时,不管是OOP还是functional Programming,只是不同的编程风格,我始终觉得代码之后的想法和逻辑才是最重要的,不同编程风格只是不容工具而已。
JavaScript原生Set类
ECMAScript 2015 新增了 Set 类作为 JavaScript API 的一部分。Set类可以把数组转换成集合,利用new Set()实例化即可即可轻松创建一个集合。
//实例化传入的参数为数组
const arr = [1, 2, 3, 4, 4];
const set = new Set(arr)
//set 为集合 {1, 2, 3, 4},会自动去除重复的4;
Set 对象允许你存储任何类型的唯一值,无论是原始值或者是对象引用,包括NaN、undefined都可以。判断里面的元素是否重复,Set判断用的是===。
const set = new Set([1, 2, 3, 4, "4"]);
//{1, 2, 3, 4, "4"} 数字4和字符4是不同的元素,不是重复的。
之前我们自己实现的Set 类实现了并集、交集、差集、子集等数学运算,然而 ES2015 原生的 Set 并没有 这些功能。但是可以通过扩展运算符来快速进行集合的数学运算。具体过程为:
- 将集转化为数组
- 执行并集、交集、差集,子集运算
- 将结果转化为集合(并集、交集、差集返回一个新集合,子集运算返回Boolean)
扩展运算符可以将集合转化成数组。
let set = new Set(1, 2, 3, 4, 5);
console.log([...set]); //[1, 2, 3, 4, 5]
假如现在有两个集合setA和setB,现在利用原生的Set和扩展运算符来对二者进行集合运算。
let setA = new Set([1, 2, 3])
let setB = new Set([1, 2, 3, 4, 5])
//并集
let unionSet = new Set([...setA, ...setB])
//交集
let intersectionSet = new Set([...setA].filter(x => setB.has(x)))
//差集
let differenceSet = new Set([...setA].filter(x => !setB.has(x)))
//子集(返回Boolean)
let AisChildOfB = [...setA].every(x => setB.has(x))
参考资料
- 书籍:学习JavaScript数据结构与算法(第3版),第7章
- 文章:Map和Set-廖雪峰