什么是集合
集合
集合是由一组无序且唯一 (即不能重复) 的项组成。
在数学中,集合是一组不同对象的集合。比如是说,一个又大于或等于 0 的整数组成的自然数集合: N = {0, 1, 2, 3,4,5, 6, ...}。集合中的对象列表用花括号 {} 包围。
空集
空集是不包含任何元素的集合。空集用 {} 表示。
实现集合
定义集合类
我们使用 ES6 的 class 语法来创建一个基于对象的 Set 类:
class Set { constructor() { this.items = {} } }复制代码
接下来,我们为集合声明一些可用的方法:
add(element) 向集合添加一个新元素
delete(element) 从集合中移除一个元素
has(element) 判断一个元素是否在集合中
clear() 移除集合中的所有元素
size() 返回集合中所包含元素的数量
values() 返回一个包含集合中所有元素的数组
isEmpty() 判断集合是否为空
size() 返回集合中元素个数
clear() 清空集合
toString() 将集合中的元素以字符串形式输出
下面,我们逐一实现这些方法:
add 向集合添加一个新元素
add(element) { if (!this.has(element)) { this.items[element] = element; return true; } return false; }复制代码
向集合中添加一个 element 的时候,首先检查它是否存在于集合中,如果不存在,就将 element 添加到集合中,并返回 true,表示添加了该元素;如果集合中已经有了这个element,则返回 false ,表示没有添加这个元素。
delete 从集合中移除一个元素
delete(element) { if (this.has(element)) { delete this.items[element]; return true; } return false; }复制代码
同样的,从集合中移除一个元素时,首先判断该元素是否存在于集合中,如果存在,就从集合中移除,并返回 true,表示元素被移除;如果不存在,则返回 false。
has 判断一个元素是否在集合中
has(element) { return Object.prototype.hasOwnProperty.call(this.items, element); }复制代码
我们使用 Object 的原型方法 hasOwnProperty 来判断元素是否在集合中。hasOwnProperty 方法返回一个表明对象是否具有特定属性布尔值。
clear 移除集合中的所有元素
clear() { this.items = {} }复制代码
size 返回集合中所包含元素的数量
方法一:使用 Object.keys()
size() { return Object.keys(this.items).length; }复制代码
在 size 方法中,我们使用 Object 类的 keys 方法来获取集合中所包含元素的数量。keys 方法返回一个包含给定对象所有属性的数组,然后我们可以使用数组的 length 属性来返回集合中的元素数量。
方法二:使用 for...in 循环
size() { let count = 0; for (let key in this.items) { if (this.items.hasOwnProperty(key)) { count++ } } return count; }复制代码
我们定义一个 count 变量来存储集合中元素的个数, 然后使用 for...in 循环迭代 items 对象的所有属性,检查它们是否是对象自身的属性,如果是,就递增 count 变量的值,最后在方法结束时返回 count 变量。
values 返回一个包含集合中所有元素的数组
方法一:使用 Object.values()
values() { return Object.values(this.items); }复制代码
我们使用 Object 类内置的 values 方法可以很轻松的获取集合中的所有元素,但Object.values 方法目前只在现代浏览器中可用。
方法二:使用 for...in 循环
values() { let values = []; for (let key in this.items) { if (this.items.hasOwnProperty(key)) { // 注意,我们保存的集合元素 key 与 value 相同,因此可以直接将 key push 进 values 中 values.push(key) } } }复制代码
isEmpty 判断集合是否为空
isEmpty() { return this.size() === 0; }复制代码
toString 将集合中的元素以字符串形式输出
toString() { if (this.isEmpty()) { return ''; } const values = this.values(); let objString = `${values[0]}`; for (let i = 1; i < values.length; i++) { objString = `${objString}, ${values[i].toString()}`; } return objString; }复制代码
集合运算
在数学中,集合有 并集、交集、差集、子集等运算,我们也可以使用我们定义的集合类进行这些运算。
并集:对于给定的两个集合,返回一个包含两个集合中所有元素的新集合
交集:对于给定的两个集合,返回一个包含两个集合中共有元素的集合
差集:对于给定的两个集合,返回一个包含所有存在于第一个集合且不存在于第二个集合的元素的新集合
子集:验证一个给定集合是否是另一个集合的子集
并集
在数学概念中,集合 A 和集合 B 的并集表示如下:
A ∪ B
该集合的定义如下:
A ∪ B = {x | x ∈ A ∨ x ∈ B}
意思是 x (元素) 存在于 A 中,或 x 存在于 B 中。下图展示了并集运算:
下面,我们实现 Set 类的 union 方法:
union(otherSet) { // 存放并集的结果 const unionSet = new Set(); // 迭代当前集合的所有值,将值添加到并集 集合中 this.values().forEach(value => unionSet.add(value)); // 迭代 otherSet 的所有值,将值添加到并集 集合中 otherSet.values().forEach(value => unionSet.add(value)); return unionSet; }复制代码
首先使用 ES6 提供的数据结构 Set 来创建一个新的集合 unionSet,代表两个集合的并集。接着获取第一个集合(当前的 Set 类实例)的所有值,迭代并全部添加到代表并集的集合中。然后对第二个集合做同样的是,最后将代表并集的集合返回。
交集
在数学概念中,集合 A 和集合 B 的交集表示如下:
A ∩ B
该集合的定义如下:
A ∩ B = {x | x ∈ A ∧ x ∈ B}
意思是 x (元素) 存在于 A 中,且 x 存在于 B 中,下图展示了交集运算:
下面,我们实现 Set 类的 intersection 方法:
intersection(otherSet) { // 存放交集的结果 const intersectionSet = new Set(); // 当前集合(当前 Set 类的实例)的所有值 const values = this.values(); // 第二个集合的所有值 const otherValues = otherSet.values(); // 假设当前的集合元素较多 let biggerSet = values; // 假设第二个集合元素较少 let smallerSet = otherValues; // 比较两个集合的元素个数,如果另外一个集合的元素多于当前集合,则交换 biggerSet 和 smallerSet if (otherValues.length - values.length > 0) { biggerSet = otherValues; smallerSet = values; } // 迭代较小的集合,计算出两个集合共有元素添加到 交集 集合中 smallerSet.forEach(value => { if (biggerSet.includes(value)) { intersectionSet.add(value); } }) // 将交集结果返回 return intersectionSet; }复制代码
intersection 方法会得到所有同时存在于两个集合中的元素。我们首先创建一个新的集合 intersectionSet 来存放交集的结果。然后分别获取当前 Set 实例中的所有元素和另一个集合中的所有元素。我们假定当前集合的元素较多,另一个集合的元素较少,比较两个集合的元素个数,如果另一个集合的元素个数多于当前集合中的元素个数,我们就交换 biggerSet 和 smallerSet 的值。最后迭代较小集合 smallerSet 来计算出两个集合的共有元素。
差集
在数学概念中,集合 A 和集合 B 的差集表示为:
A﹣B
该集合的定义如下:
A﹣B = {x | x ∈ A ∧ x ∉ B}
意思是 x (元素) 存在于 A 中,且 x 不存在于 B 中。下图展示了集合 A 和集合 B 的差集运算:
下面,我们来实现 Set 类的 subtract 方法:
subtract(otherSet) { // 存放差集结果 const subtractionSet = new Set(); // 迭代当前集合的所有值 this.values().forEach(value => { // 当前值不存在于 otherSet 中,将其添加到 差集 集合中 if (!otherSet.has(value)) { // 将元素添加到集合中 subtractionSet.add(value); } }) return subtractionSet; }复制代码
subtract 方法会得到所有存在于集合 A 但不存在于集合 B 的元素。我们首先创建一个新的集合变量 subtractionSet 来存放差集结果。通过 this.values() 拿到集合A 中的所有值,然后迭代集合 A 中的所有值,将存在于集合A 但不存在于集合B 中的元素放入 subtractionSet 集合中,迭代完集合 A 中的所有元素后,subtractionSet 集合中的元素就是集合A 与集合B 差集。
子集
在数学概念中,集合 A 是集合 B 的一个子集(或者 B 包含 A),表示如下:
A ⊆ B
该集合的定义如下:
{x|∀x∈A ⇒ x ∈ B}
意思是集合A 中的每一个 x (元素),也需要存在于集合B 中。下图展示了集合A 是集合B 的子集:
下面,我们来实现 Set 类的 isSubsetOf 方法:
isSubsetOf(otherSet) { // 验证当前 Set 实例的大小 // 当前 Set 实例的元素多于 otherSet,那么当前 Set 实例不是 otherSet 的子集 if (this.size() > otherSet.size()) { return false; } // 假定当前 Set 实例是给定集合的子集 let isSubset = true; // 迭代当前 Set 实例的所有元素 this.values().every(value => { // 有任何元素不存在于 otherSet 中,那么当前 Set 实例就不是 otherSet 的子集 if (!otherSet.has(value)) { isSubset = false; return false; } return true; }) return isSubset; }复制代码
isSubsetOf 方法验证 集合 A 是否是 集合 B 的子集。我们首先需要验证当前 Set 实例的大小(集合A),如果当前实例中的元素比 otherSet 实例(集合B)更多,说明当前 Set 实例不是 otherSet 的子集。(子集的元素个数需要小于等于要比较的集合)。
接下来,我们假定当前集合是给定集合的子集,然后迭代当前集合的所有元素,并验证这些元素是否存在于 otherSet 中。如果有任何一个元素不存在于 otherSet 中,那么当前集合就不是给定集合的子集,我们就返回 false。如果所有元素都存在于 otherSet 中,那么当前集合就是给定集合的子集。
完整代码
class Set { constructor() { this.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; } // 判断一个元素是否存在于集合中 has(element) { return Object.prototype.hasOwnProperty.call(this.items, element); } // 移除集合中的所有元素 clear() { this.items = {}; } // 返回集合中所包含元素的数量 size() { let count = 0; for(let key in this.items) { if (this.items.hasOwnProperty(key)) { count++; } } return count; } // 返回一个包含集合中所有元素的数组 values() { let values = []; for (let key in this.items) { if (this.items.hasOwnProperty(key)) { values.push(key); } } return values; } // 并集 union(otherSet) { const unionSet = new Set(); this.values().forEach(value => unionSet.add(value)); otherSet.values().forEach(value => unionSet.add(value)); return unionSet; } // 交集 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; } // 差集 subtract(otherSet) { const subtractionSet = new Set(); this.values().forEach(value => { if (!otherSet.has(value)) { subtractionSet.add(value) } }) return subtractionSet; } // 子集 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; } }复制代码
ES6 Set 类的集合运算
并集
// A 和 B 的并集 function union(setA, setB) { const unionSet = new Set([...setA, ...setB]); return unionSet; }复制代码
交集
// A 和 B 的交集 function intersection(setA, setB) { const intersect = new Set([...a].filter(x => b.has(x))); return intersect }复制代码
差集
// A 相对于 B 的差集 function subTract(setA, setB) { const subTractSet = new Set([...a].filter(x => !b.has(x))); return subTractSet; }复制代码
参考资料:
书籍:《JavaScript数据结构与算法》
作者:moozi