集合运算:Set 的狂野西部
大家好!今天咱们来聊聊 JavaScript Set
的集合运算。 别看 Set
这么“冷淡”,只管存唯一值,其实它内心也渴望搞事情,比如和其他 Set
合体、相爱相杀。
目前 JavaScript 原生 Set
并没有直接提供 union
(并集)、intersection
(交集)、difference
(差集)这些集合运算方法。但这怎么能难倒我们这些身经百战的程序员呢? 今天就带大家闯入 Set
的狂野西部,自己动手,丰衣足食!
1. 认识 Set
:一个独特的容器
首先,简单回顾一下 Set
。 它是一个存储唯一值的集合。 也就是说,你往里面放重复的东西,它会自动忽略,只保留一个。 这特性在很多场景下都非常有用,比如去重、判断元素是否存在等。
const mySet = new Set();
mySet.add(1);
mySet.add(2);
mySet.add(3);
mySet.add(1); // 再次添加1,会被忽略
console.log(mySet); // Set(3) { 1, 2, 3 }
console.log(mySet.has(2)); // true
console.log(mySet.size); // 3
2. 手动实现集合运算:牛仔们的生存之道
既然原生不支持,那就自己造轮子!咱们用 JavaScript 现有的方法,一步一步实现这些集合运算。
2.1 Union (并集): 合并同类项
并集,顾名思义,就是把两个集合的所有元素都加起来,形成一个新的集合。 就像把两堆金矿合并成更大的一堆,当然,重复的金子就没必要再要了。
function union(setA, setB) {
const _union = new Set(setA); // 先复制一份 setA,避免修改原 setA
for (const elem of setB) {
_union.add(elem); // 把 setB 的元素逐个添加到 _union 中
}
return _union;
}
const setA = new Set([1, 2, 3]);
const setB = new Set([3, 4, 5]);
const unionSet = union(setA, setB);
console.log(unionSet); // Set(5) { 1, 2, 3, 4, 5 }
解释:
union(setA, setB)
函数接收两个Set
对象setA
和setB
作为参数。const _union = new Set(setA);
创建一个新的Set
对象_union
,并将setA
的所有元素复制到_union
中。 这样做是为了避免直接修改setA
。for (const elem of setB) { _union.add(elem); }
遍历setB
的所有元素,并将每个元素添加到_union
中。 由于Set
会自动去重,所以即使setB
中包含与setA
相同的元素,最终_union
中也只会保留一个。return _union;
返回包含setA
和setB
所有元素的并集的新Set
对象。
2.2 Intersection (交集): 寻找共同点
交集,就是找出两个集合共有的元素,就像找出两个帮派共同的地盘。
function intersection(setA, setB) {
const _intersection = new Set();
for (const elem of setB) {
if (setA.has(elem)) { // 只有 setA 也包含这个元素,才添加到 _intersection
_intersection.add(elem);
}
}
return _intersection;
}
const setA = new Set([1, 2, 3]);
const setB = new Set([2, 3, 4]);
const intersectionSet = intersection(setA, setB);
console.log(intersectionSet); // Set(2) { 2, 3 }
解释:
intersection(setA, setB)
函数接收两个Set
对象setA
和setB
作为参数。const _intersection = new Set();
创建一个新的空Set
对象_intersection
,用于存储交集元素。for (const elem of setB) { ... }
遍历setB
的所有元素。if (setA.has(elem)) { ... }
对于setB
中的每个元素,检查它是否也存在于setA
中。setA.has(elem)
方法用于判断setA
是否包含elem
。_intersection.add(elem);
如果setA
也包含该元素,则将其添加到_intersection
中。return _intersection;
返回包含setA
和setB
共同元素的交集的新Set
对象。
2.3 Difference (差集): 你有我没有
差集,就是找出只存在于一个集合,而不存在于另一个集合的元素。 就像找出某个帮派独占的地盘,其他帮派想都别想。
function difference(setA, setB) {
const _difference = new Set(setA); // 先复制一份 setA
for (const elem of setB) {
if (_difference.has(elem)) {
_difference.delete(elem); // 如果 setB 也有这个元素,就从 _difference 中删除
}
}
return _difference;
}
const setA = new Set([1, 2, 3]);
const setB = new Set([2, 4]);
const differenceSet = difference(setA, setB);
console.log(differenceSet); // Set(2) { 1, 3 }
解释:
difference(setA, setB)
函数接收两个Set
对象setA
和setB
作为参数。const _difference = new Set(setA);
创建一个新的Set
对象_difference
,并将setA
的所有元素复制到_difference
中。for (const elem of setB) { ... }
遍历setB
的所有元素。if (_difference.has(elem)) { ... }
对于setB
中的每个元素,检查它是否也存在于_difference
中。_difference.delete(elem);
如果_difference
包含该元素(说明setA
和setB
都有该元素),则将其从_difference
中删除。return _difference;
返回包含只存在于setA
中,而不存在于setB
中的元素的差集的新Set
对象。
2.4 Symmetric Difference (对称差集): 你有我没有,我也有你没有
对称差集,就是找出只存在于其中一个集合,而不同时存在于两个集合的元素。 相当于两个帮派各自独占的地盘的总和。
function symmetricDifference(setA, setB) {
const _symmetricDifference = new Set();
for (const elem of setA) {
if (!setB.has(elem)) {
_symmetricDifference.add(elem);
}
}
for (const elem of setB) {
if (!setA.has(elem)) {
_symmetricDifference.add(elem);
}
}
return _symmetricDifference;
}
const setA = new Set([1, 2, 3]);
const setB = new Set([2, 4]);
const symmetricDifferenceSet = symmetricDifference(setA, setB);
console.log(symmetricDifferenceSet); // Set(3) { 1, 3, 4 }
解释:
symmetricDifference(setA, setB)
函数接收两个Set
对象setA
和setB
作为参数。const _symmetricDifference = new Set();
创建一个新的空Set
对象_symmetricDifference
,用于存储对称差集元素。- 第一个
for
循环遍历setA
的所有元素。 if (!setB.has(elem)) { ... }
对于setA
中的每个元素,检查它是否不存在于setB
中。_symmetricDifference.add(elem);
如果setB
不包含该元素,则将其添加到_symmetricDifference
中。- 第二个
for
循环遍历setB
的所有元素。 if (!setA.has(elem)) { ... }
对于setB
中的每个元素,检查它是否不存在于setA
中。_symmetricDifference.add(elem);
如果setA
不包含该元素,则将其添加到_symmetricDifference
中。return _symmetricDifference;
返回包含只存在于setA
或setB
中的元素的对称差集的新Set
对象。
3. 更简洁的实现:ES6 的魔法
有了 ES6 的箭头函数和扩展运算符,我们可以把上面的代码写得更简洁一些,更像一个优雅的现代牛仔。
const union = (setA, setB) => new Set([...setA, ...setB]);
const intersection = (setA, setB) => new Set([...setB].filter(x => setA.has(x)));
const difference = (setA, setB) => new Set([...setA].filter(x => !setB.has(x)));
const symmetricDifference = (setA, setB) => new Set([...difference(setA, setB), ...difference(setB, setA)]);
const setA = new Set([1, 2, 3]);
const setB = new Set([2, 3, 4]);
console.log(union(setA, setB)); // Set(4) { 1, 2, 3, 4 }
console.log(intersection(setA, setB)); // Set(2) { 2, 3 }
console.log(difference(setA, setB)); // Set(1) { 1 }
console.log(symmetricDifference(setA, setB)); // Set(2) { 1, 4 }
解释:
union
: 使用扩展运算符...
将setA
和setB
转换为数组,然后合并成一个新数组,最后用new Set()
转换为Set
对象。intersection
: 将setB
转换为数组,然后使用filter
方法筛选出setA
也包含的元素,最后用new Set()
转换为Set
对象。difference
: 将setA
转换为数组,然后使用filter
方法筛选出setB
不包含的元素,最后用new Set()
转换为Set
对象。symmetricDifference
: 利用前面定义的difference
函数,分别计算setA
相对于setB
的差集和setB
相对于setA
的差集,然后将这两个差集合并成一个新的Set
对象。
4. 未来提案:TC39 的远方
TC39 (负责 JavaScript 标准化的委员会) 曾经考虑过为 Set
增加这些集合运算方法,但提案一直处于讨论阶段,还没有最终定稿。 虽然现在没有原生支持,但我们自己实现的版本已经足够用了。如果未来真的有了原生方法,那我们就可以直接用原生的,岂不美哉?
方法名 | 功能描述 |
---|---|
Set.prototype.union(otherSet) |
返回一个包含两个集合所有元素的新集合。 |
Set.prototype.intersection(otherSet) |
返回一个包含两个集合共有元素的新集合。 |
Set.prototype.difference(otherSet) |
返回一个包含只存在于第一个集合,而不存在于第二个集合的元素的新集合。 |
Set.prototype.symmetricDifference(otherSet) |
返回一个包含只存在于其中一个集合,而不同时存在于两个集合的元素的新集合。 |
Set.prototype.isSubsetOf(otherSet) |
判断当前集合是否是另一个集合的子集。 |
Set.prototype.isSupersetOf(otherSet) |
判断当前集合是否是另一个集合的超集。 |
Set.prototype.isDisjointFrom(otherSet) |
判断当前集合是否与另一个集合不相交(没有共同元素)。 |
5. 应用场景:有用武之地
集合运算在实际开发中有很多应用场景,比如:
- 数据过滤: 从一个大的数据集中,筛选出符合特定条件的子集。
- 权限管理: 判断用户是否拥有某些权限(权限可以用 Set 表示)。
- 推荐系统: 根据用户的历史行为,推荐相似的商品或内容。
- 算法实现: 在一些算法中,需要用到集合运算来解决问题。
举个例子,假设我们有一个用户集合,和一个管理员集合,我们可以用交集来找出哪些用户同时也是管理员:
const users = new Set(['Alice', 'Bob', 'Charlie']);
const admins = new Set(['Bob', 'David']);
const adminUsers = intersection(users, admins);
console.log(adminUsers); // Set(1) { 'Bob' }
6. 性能考量:精打细算
虽然我们自己实现的集合运算已经能满足基本需求,但在处理大型数据集时,性能也是一个需要考虑的问题。 原生 Set
的 has
方法通常比数组的 includes
方法更快,因为 Set
使用哈希表来存储元素。 因此,在实现集合运算时,尽量利用 Set
的 has
方法来提高效率。
另外,如果需要频繁进行集合运算,可以考虑使用一些专门的 JavaScript 库,比如 js-set
或 immutable.js
,这些库通常会对性能进行优化。
7. 总结:成为 Set 的驯兽师
今天我们一起探索了 JavaScript Set
的狂野西部,学习了如何手动实现 union
、intersection
、difference
等集合运算。 虽然原生 Set
目前还没有这些方法,但我们已经掌握了足够的技能,可以自己驯服 Set
,让它为我们所用。
记住,编程的乐趣就在于不断学习和探索,即使没有现成的工具,我们也可以自己创造! 希望今天的讲座对大家有所帮助,咱们下次再见!