集合运算: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,让它为我们所用。
记住,编程的乐趣就在于不断学习和探索,即使没有现成的工具,我们也可以自己创造! 希望今天的讲座对大家有所帮助,咱们下次再见!