JS `Set` 的集合运算:`union`, `intersection`, `difference` (通过手动实现或未来提案)

集合运算: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 }

解释:

  1. union(setA, setB) 函数接收两个 Set 对象 setAsetB 作为参数。
  2. const _union = new Set(setA); 创建一个新的 Set 对象 _union,并将 setA 的所有元素复制到 _union 中。 这样做是为了避免直接修改 setA
  3. for (const elem of setB) { _union.add(elem); } 遍历 setB 的所有元素,并将每个元素添加到 _union 中。 由于 Set 会自动去重,所以即使 setB 中包含与 setA 相同的元素,最终 _union 中也只会保留一个。
  4. return _union; 返回包含 setAsetB 所有元素的并集的新 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 }

解释:

  1. intersection(setA, setB) 函数接收两个 Set 对象 setAsetB 作为参数。
  2. const _intersection = new Set(); 创建一个新的空 Set 对象 _intersection,用于存储交集元素。
  3. for (const elem of setB) { ... } 遍历 setB 的所有元素。
  4. if (setA.has(elem)) { ... } 对于 setB 中的每个元素,检查它是否也存在于 setA 中。 setA.has(elem) 方法用于判断 setA 是否包含 elem
  5. _intersection.add(elem); 如果 setA 也包含该元素,则将其添加到 _intersection 中。
  6. return _intersection; 返回包含 setAsetB 共同元素的交集的新 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 }

解释:

  1. difference(setA, setB) 函数接收两个 Set 对象 setAsetB 作为参数。
  2. const _difference = new Set(setA); 创建一个新的 Set 对象 _difference,并将 setA 的所有元素复制到 _difference 中。
  3. for (const elem of setB) { ... } 遍历 setB 的所有元素。
  4. if (_difference.has(elem)) { ... } 对于 setB 中的每个元素,检查它是否也存在于 _difference 中。
  5. _difference.delete(elem); 如果 _difference 包含该元素(说明 setAsetB 都有该元素),则将其从 _difference 中删除。
  6. 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 }

解释:

  1. symmetricDifference(setA, setB) 函数接收两个 Set 对象 setAsetB 作为参数。
  2. const _symmetricDifference = new Set(); 创建一个新的空 Set 对象 _symmetricDifference,用于存储对称差集元素。
  3. 第一个 for 循环遍历 setA 的所有元素。
  4. if (!setB.has(elem)) { ... } 对于 setA 中的每个元素,检查它是否不存在于 setB 中。
  5. _symmetricDifference.add(elem); 如果 setB 不包含该元素,则将其添加到 _symmetricDifference 中。
  6. 第二个 for 循环遍历 setB 的所有元素。
  7. if (!setA.has(elem)) { ... } 对于 setB 中的每个元素,检查它是否不存在于 setA 中。
  8. _symmetricDifference.add(elem); 如果 setA 不包含该元素,则将其添加到 _symmetricDifference 中。
  9. return _symmetricDifference; 返回包含只存在于 setAsetB 中的元素的对称差集的新 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: 使用扩展运算符 ...setAsetB 转换为数组,然后合并成一个新数组,最后用 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. 性能考量:精打细算

虽然我们自己实现的集合运算已经能满足基本需求,但在处理大型数据集时,性能也是一个需要考虑的问题。 原生 Sethas 方法通常比数组的 includes 方法更快,因为 Set 使用哈希表来存储元素。 因此,在实现集合运算时,尽量利用 Sethas 方法来提高效率。

另外,如果需要频繁进行集合运算,可以考虑使用一些专门的 JavaScript 库,比如 js-setimmutable.js,这些库通常会对性能进行优化。

7. 总结:成为 Set 的驯兽师

今天我们一起探索了 JavaScript Set 的狂野西部,学习了如何手动实现 unionintersectiondifference 等集合运算。 虽然原生 Set 目前还没有这些方法,但我们已经掌握了足够的技能,可以自己驯服 Set,让它为我们所用。

记住,编程的乐趣就在于不断学习和探索,即使没有现成的工具,我们也可以自己创造! 希望今天的讲座对大家有所帮助,咱们下次再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注