【问题标题】:The fastest way to do a collection subtraction做集合减法的最快方法
【发布时间】:2011-01-24 23:03:32
【问题描述】:

我有两套。 Set bSet a 的子集。他们都是非常巨大的集合。 我想从 a 中减去 b,执行此常见操作的最佳做​​法是什么? 我写过很多这样的代码,但我认为它效率不高。你有什么想法?

伪代码:(这不是 Java API)。

for(int i = 0 ; i < a.size(); i++) {
          for (int j=0 ; j < b.size() ;j++) {
              // do comparison , if found equals ,remove from a
              break;
          }
 }

我想找一个算法,不仅适用于Sets,也适用于Array。

编辑:这里的 Set 不是 JAVA API,它是一个数据结构。所以我不在乎Java API是否有removeAll()方法,我想为这个问题找到一个通用的解决方案,我在使用Javascript和Actionscript时遇到了很多这样的问题。

【问题讨论】:

  • 我更改了标签列表,因为 OP 对 Java 解决方案不感兴趣。
  • 不,不是。我想找到一个通用算法,而不是 Java API。
  • 对,所以我把java标签去掉了。

标签: javascript actionscript-3 performance


【解决方案1】:

我不认为你会更快,但你的代码看起来会更简单,并且不会因为a.removeAll(b); 而变慢。 removeAll() 是 Java-API 的一部分。

对于效率分析:您给定的代码示例是 O(n^2),它的扩展性不是很好,但也不是地球上最可怕的事情(指数复杂度是您不想要的东西)。只要不了解 Collection 中数据的内部组织方式,就不会获得更好的性能。 removeAll() 由类本身实现并了解内部组织。所以如果将数据组织成一个Hash,你可能会得到更好的结果,如果数据组织成一个未排序的数组,复杂度是一样的。如果一个新项目已经在集合中,一个集合必须有效地查找,所以我怀疑某种哈希作为内部表示,特别是如果实现被称为哈希集。 :-)

编辑: OP 改变了它的问题,提到它不仅适用于 Java。 removeAll() 是一个 Java-API,所以这个(或类似的东西)可能在其他语言中不可用。如前所述,如果集合是没有其他限制的未排序数组,则两个 for 循环已经是最快的解决方案。但是,如果数据的组织方式不同,您就有更快的选择。如果这两个集合是排序数据(在我的示例中,最小元素在前),您可以执行以下操作(将复杂度降低到 O(n)):

int bIndex = 0;
for(int i = 0 ; i < a.size(); i++) {
          while (a[i] < b[bIndex]) {bIndex++;}
          if (a[i] == b[bIndex]) {markForRemoval(a[i]);} // I mark this only for removal, as the actual removal would make your index incorrect
}

如果数据在两个集合中都组织为散列,您还只需要一个 for 循环,直接访问 b 中的元素。其他可能的数据组织也是可能的。

【讨论】:

    【解决方案2】:

    最后,除了一个一个比较元素并删除两者中的元素之外,别无选择。

    换一种方式,你必须做一些花哨的事情,比如给所有集合成员一个唯一的值索引,并构造一个代表每个集合的巨大布尔数组,然后你可以做位操作从 A 中减去 B . 考虑到创建唯一值索引和操作非常大的位掩码的开销,我不知道这是否会更快。

    我知道您并不关心 Java 解决方案,但由于其他人推荐了 removeAll(),我想指出它在幕后仍然在做同样的事情。检查 HashSet 的来源。

    【讨论】:

    • 但是我没有看到任何快速排序算法迭代这样的集合,只有冒泡排序,它不够快,有人说它应该被弃用。
    • 正确,大部分 removeAll() 应该做同样的事情。但它更简单,更容易在代码中阅读,并且某些 removeAll 实现可以使用更好的内部数据组织,尤其是在 Set 中。 Set 应该使用某种快速随机访问,以快速确定元素是否已经存在。最简单的方法是对条目进行排序,即使这样也会将操作的复杂度降低到 O(n)(只需要对两个集合进行一次迭代)。
    • @Mnementh:是否有可能将两个 int[] 数组的复杂性降低到 O(n) 比较?
    • @Tony:如果数组中的元素已排序,则可以在一个循环中遍历两者。
    • @Tony 和@Mnementh - 我不是在提议排序数组,而是一对使用数组的非常大的位掩码 - 如果你关注我,这将使它们更具位置性而不是排序。但是,是的,你在一个循环中遍历这两个,对每个块进行位操作。
    【解决方案3】:

    如果保持集合以使元素在任何给定时间按排序顺序可用,那么您可以对两个集合执行单个线性传递并创建 O(n) 时间的差异。现在,再次,如果您可以免费获得元素的有序列表 - 也就是说,维护(即添加元素和删除元素操作) 的集合支付保持元素按排序顺序可用的成本。

    任何依赖于执行查找的“removeAll”操作必然会比 O(n) 更糟。

    (我突然想到,如果你不是特别小心的话,差异集的构造——也就是说,通过两个列表的线性传递构造的答案——可能是 O(n log n)。)

    【讨论】:

      【解决方案4】:

      鉴于 b 是 a 的子集,我不确定您的伪代码为什么有 2 个循环。我的只是:

      foreach b in B
          remove b from A
      

      实际上,它的运行时间与您的运行时间的比较取决于您如何将集合实现为数据结构。

      【讨论】:

        【解决方案5】:

        好吧,已经指出了正确的想法:集合应该使用散列来实现。理想情况下,哈希具有O(1) 访问成本,因此假设您可以确定哪个集合更大(例如在插入/删除操作期间维护一个计数器),您可以获得整个操作的O(min(m,n)) 成本。

        在 actionscript 3 中,您将使用 Dictionary。只需将元素用作键和值。

        删除看起来像这样:

        for each (var key:* in set2) {//a simple for-in loop will also do the trick, since keys and values are equal, but for-each-in loops perform faster
            delete set1[key];
        }
        

        在 JavaScript 中,您需要在插入时提供条目 id,因此您可以使用这些 id 作为映射中的键。只需将 id 映射到原始值即可。

        删除看起来像这样:

        for (var key in set2) {
            delete set1[key];
        }
        

        【讨论】:

          【解决方案6】:

          您正在编写的操作是 O(N^2),但如果集合很大,您可能需要使用哈希。

          // A is some kind of array, O(1) iteration
          // B is a hash containing elements to remove, O(1) contains(elt)
          List<T> removeAll(List<T> A, Set<T> B) {
            List<T> result; // empty, could preallocate at |A|
            for (elt : A) { // for each 'elt' belonging to A, hence O(|A|)
              if (! B.contains(elt) ) { // O(1) thanks to hash
                C.add(elt) ; // ensure this is O(1) with preallocation or linked list
              }
            }
            return result;
          }
          

          这需要对集合 B 进行索引,因此您需要一个哈希函数。 在 Java 中,您可以使用 Set&lt;T&gt; Bh = new HashSet&lt;T&gt;(B);,它在时间和内存上都是 O(|B|)。 所以总的来说,我们在时间上得到 O(|A|+|B|),在内存中大约得到 O(2|A|+2|B|))。 肯定胜过 removeAll 的二次方,你会感觉到不同 (TM)。

          最好将元素复制到一个新数组中(如伪代码中所做的那样),因为如果您保持元素有序,直接从 A 中删除元素可能会导致开销(A 中的左移元素代价高昂)。

          【讨论】:

            【解决方案7】:

            你见过Set接口中的removeAll方法吗?

            还可以查看this stack overflow question

            【讨论】:

              【解决方案8】:

              我相信您会发现java.util.HashSet.removeAll(Collection toRemove) 表现出色。 另一方面,如果您没有 sets 而是排序集合,则可能会做得更好。

              【讨论】:

              • 确实,哈希表、BST 或其他针对随机访问优化的集合类型的性能应该更好。
              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2020-07-09
              • 2014-10-03
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              相关资源
              最近更新 更多