【问题标题】:What is the fastest or most elegant way to compute a set difference using Javascript arrays?使用 Javascript 数组计算集合差异的最快或最优雅的方法是什么?
【发布时间】:2010-12-15 22:36:05
【问题描述】:

AB 为两组。我正在寻找真正快速或优雅的方法来计算它们之间的集合差异(A - BA \B,取决于您的偏好)。正如标题所说,这两个集合作为 Javascript 数组存储和操作。

注意事项:

  • Gecko 特有的技巧还可以
  • 我更喜欢坚持使用本机函数(但如果轻量级库更快,我愿意使用它)
  • 我见过,但未经测试,JS.Set(见上一点)

编辑:我注意到关于包含重复元素的集合的评论。当我说“集合”时,我指的是数学定义,这意味着(除其他外)它们不包含重复元素。

【问题讨论】:

  • 您使用的“设置差异”术语是什么?是来自 C++ 还是什么?
  • 你的套装里有什么?根据您的目标类型(例如数字),计算一组差异可以真正快速而优雅地完成。如果您的集合包含(比如说)DOM 元素,您将陷入缓慢的indexOf 实现。
  • @Crescent:我的集合包含数字 - 抱歉没有指定。 @Josh:这是数学中的标准集合运算 (en.wikipedia.org/wiki/Set_%28mathematics%29#Complements)
  • @MattBall 不,我看到了。但是乔希的问题是有效的,没有答案,所以我回答了:)

标签: javascript arrays set-difference


【解决方案1】:

如果不知道这是否最有效,但可能是最短的

A = [1, 2, 3, 4];
B = [1, 3, 4, 7];

diff = A.filter(function(x) { return B.indexOf(x) < 0 })

console.log(diff);

更新到 ES6:

A = [1, 2, 3, 4];
B = [1, 3, 4, 7];

diff = A.filter(x => !B.includes(x) );

console.log(diff);

【讨论】:

  • +1:不是最有效的解决方案,但绝对简短易读
  • 注意:array.filter 不支持跨浏览器(例如不在 IE 中)。这对@Matt 来说似乎并不重要,因为他说“Gecko 特有的技巧还可以”,但我认为值得一提。
  • 这很慢。 O(|A| * |B|)
  • @EricBréchemier 现在支持此功能(从 IE 9 开始)。 Array.prototype.filter 是标准的 ECMAScript 功能。
  • 在 ES6 中,你可以使用 !B.includes(x) 代替 B.indexOf(x) &lt; 0 :)
【解决方案2】:

嗯,7 年后,使用 ES6's Set 对象非常简单(但仍然不如 python's A - B 紧凑),并且据报道对于大型数组比 indexOf 更快:

console.clear();
let a = new Set([1, 2, 3, 4]);
let b = new Set([5, 4, 3, 2]);


let a_minus_b = new Set([...a].filter(x => !b.has(x)));
let b_minus_a = new Set([...b].filter(x => !a.has(x)));
let a_intersect_b = new Set([...a].filter(x => b.has(x))); 

console.log([...a_minus_b]) // {1}
console.log([...b_minus_a]) // {5}
console.log([...a_intersect_b]) // {2,3,4}

【讨论】:

  • 对于大型数组也比 indexOf 快得多。
  • 为什么 JavaScript 集合没有内置 union/intersect/difference 超出了我的理解...
  • 我完全同意;这些应该是在 js 引擎中实现的较低级别的原语。这也超出了我的范围......
  • @SwiftsNamesake 有一个关于 set 内置方法的提案,有望在 2018 年 1 月 github.com/tc39/agendas/blob/master/2018/01.md 讨论。
【解决方案3】:

您可以将对象用作映射以避免像user187291's answer 中那样线性扫描B 的每个A 元素:

function setMinus(A, B) {
    var map = {}, C = [];

    for(var i = B.length; i--; )
        map[B[i].toSource()] = null; // any other value would do

    for(var i = A.length; i--; ) {
        if(!map.hasOwnProperty(A[i].toSource()))
            C.push(A[i]);
    }

    return C;
}

非标准的toSource() method用于获取唯一的属性名;如果所有元素都已经具有唯一的字符串表示形式(就像数字的情况一样),您可以通过删除 toSource() 调用来加快代码速度。

【讨论】:

    【解决方案4】:

    看看这些解决方案中的许多,它们都适用于小案例。但是,当您将它们放大到一百万个项目时,时间复杂度开始变得愚蠢。

     A.filter(v => B.includes(v))
    

    这开始看起来像一个 O(N^2) 解决方案。既然有一个 O(N) 的解决方案,让我们使用它,如果您的 JS 运行时不是最新的,您可以轻松修改为不是生成器。

        function *setMinus(A, B) {
          const setA = new Set(A);
          const setB = new Set(B);
    
          for (const v of setB.values()) {
            if (!setA.delete(v)) {
                yield v;
            }
          }
    
          for (const v of setA.values()) {
            yield v;
          }
        }
    
        a = [1,2,3];
        b = [2,3,4];
    
        console.log(Array.from(setMinus(a, b)));

    虽然这比许多其他解决方案要复杂一些,但当您拥有大型列表时,这会快得多。

    让我们快速看一下性能差异,在 0...10,000 之间的 1,000,000 个随机整数上运行它,我们会看到以下性能结果。

    setMinus time =  181 ms
        diff time =  19099 ms
    

    function buildList(count, range) {
      result = [];
      for (i = 0; i < count; i++) {
        result.push(Math.floor(Math.random() * range))
      }
      return result;
    }
    
    function *setMinus(A, B) {
      const setA = new Set(A);
      const setB = new Set(B);
    
      for (const v of setB.values()) {
        if (!setA.delete(v)) {
            yield v;
        }
      }
    
      for (const v of setA.values()) {
        yield v;
      }
    }
    
    function doDiff(A, B) {
      return A.filter(function(x) { return B.indexOf(x) < 0 })
    }
    
    const listA = buildList(100_000, 100_000_000); 
    const listB = buildList(100_000, 100_000_000); 
    
    let t0 = process.hrtime.bigint()
    
    const _x = Array.from(setMinus(listA, listB))
    
    let t1 = process.hrtime.bigint()
    
    const _y = doDiff(listA, listB)
    
    let t2 = process.hrtime.bigint()
    
    console.log("setMinus time = ", (t1 - t0) / 1_000_000n, "ms");
    console.log("diff time = ", (t2 - t1) / 1_000_000n, "ms");

    【讨论】:

    • @RonKlein 公平点,将代码更新为两组
    【解决方案5】:

    使用jQuery,最短的是:

    var A = [1, 2, 3, 4];
    var B = [1, 3, 4, 7];
    
    var diff = $(A).not(B);
    
    console.log(diff.toArray());
    &lt;script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"&gt;&lt;/script&gt;

    【讨论】:

    • 这会返回一个不同的对象。
    • jQuery not 自 3.0.0-rc1 起不再适用于通用对象。见github.com/jquery/jquery/issues/3147
    • 添加对约 70k 第三方库的依赖并不是一个好主意只是这样做,因为只需几行代码即可完成相同的事情如此处的其他答案所示。然而,如果你已经在你的项目中使用了 jQuery,那么它就可以正常工作。
    • 虽然这种方法的代码较少,但它没有提供不同算法的空间和时间复杂度以及它用于执行该方法的数据结构的任何解释。当允许数据扩展或内存有限时,开发人员在不进行评估的情况下设计软件是黑盒的。如果您在大型数据集上使用这种方法,在进一步研究源代码之前,性能可能仍然未知。
    • 这只是返回 A 中不在 B 中的元素的数量(在这种情况下为 2)。将 2 转换为数组是没有意义的......
    【解决方案6】:

    如果您使用Sets,它可以非常简单且高效:

    function setDifference(a, b) {
      return new Set(Array.from(a).filter(item => !b.has(item)));
    }
    

    由于Sets 在后台使用哈希函数*,因此has 函数比indexOf 快得多(如果您有超过 100 个项目,这很重要)。

    【讨论】:

      【解决方案7】:

      我会对数组 B 进行哈希处理,然后将数组 A 中的值保留在 B 中:

      function getHash(array){
        // Hash an array into a set of properties
        //
        // params:
        //   array - (array) (!nil) the array to hash
        //
        // return: (object)
        //   hash object with one property set to true for each value in the array
      
        var hash = {};
        for (var i=0; i<array.length; i++){
          hash[ array[i] ] = true;
        }
        return hash;
      }
      
      function getDifference(a, b){
        // compute the difference a\b
        //
        // params:
        //   a - (array) (!nil) first array as a set of values (no duplicates)
        //   b - (array) (!nil) second array as a set of values (no duplicates)
        //
        // return: (array)
        //   the set of values (no duplicates) in array a and not in b, 
        //   listed in the same order as in array a.
      
        var hash = getHash(b);
        var diff = [];
        for (var i=0; i<a.length; i++){
          var value = a[i];
          if ( !hash[value]){
            diff.push(value);
          }
        }
        return diff;
      }
      

      【讨论】:

      • 这与我半小时前发布的算法完全相同
      • @Christoph:你是对的......我没有注意到这一点。我发现我的实现更容易理解:)
      • 我认为最好在 getDifference 之外计算差异,以便可以多次重复使用。也许像这样是可选的:getDifference(a, b, hashOfB),如果未通过,它将被计算,否则将按原样重用。
      【解决方案8】:

      结合 Christoph 的想法并假设数组和对象/哈希(each 和朋友)的几个非标准迭代方法,我们可以在大约 20 行的线性时间内得到集合的差异、联合和交集:

      var setOPs = {
        minusAB : function (a, b) {
          var h = {};
          b.each(function (v) { h[v] = true; });
          return a.filter(function (v) { return !h.hasOwnProperty(v); });
        },
        unionAB : function (a, b) {
          var h = {}, f = function (v) { h[v] = true; };
          a.each(f);
          b.each(f);
          return myUtils.keys(h);
        },
        intersectAB : function (a, b) {
          var h = {};
          a.each(function (v) { h[v] = 1; });
          b.each(function (v) { h[v] = (h[v] || 0) + 1; });
          var fnSel = function (v, count) { return count > 1; };
          var fnVal = function (v, c) { return v; };
          return myUtils.select(h, fnSel, fnVal);
        }
      };
      

      这假设 eachfilter 是为数组定义的,并且我们有两个实用方法:

      • myUtils.keys(hash):返回一个 带有哈希键的数组

      • myUtils.select(hash, fnSelector, fnEvaluator):返回一个数组 调用fnEvaluator的结果 在键/值对上 fnSelector 返回真。

      select() 大致受到 Common Lisp 的启发,只是将filter()map() 合二为一。 (最好在Object.prototype 上定义它们,但是这样做会破坏 jQuery,所以我选择了静态实用程序方法。)

      性能:测试

      var a = [], b = [];
      for (var i = 100000; i--; ) {
        if (i % 2 !== 0) a.push(i);
        if (i % 3 !== 0) b.push(i);
      }
      

      给出两个集合,分别包含 50,000 和 66,666 个元素。使用这些值 A-B 大约需要 75 毫秒,而联合和交集每个大约需要 150 毫秒。 (Mac Safari 4.0,使用 Javascript Date 进行计时。)

      我认为这对 20 行代码来说是不错的回报。

      【讨论】:

      • 即使元素是数字,你仍然应该检查hasOwnProperty():否则,Object.prototype[42] = true; 之类的东西意味着42 永远不会出现在结果集中
      • 当然可以以这种方式设置 42,但是是否存在一个半现实的用例,任何人都会这样做?但是对于一般的字符串,我明白了——它很容易与一些 Object.prototype 变量或函数发生冲突。
      【解决方案9】:

      使用Underscore.js(函数式 JS 库)

      >>> var foo = [1,2,3]
      >>> var bar = [1,2,4]
      >>> _.difference(foo, bar);
      [4]
      

      【讨论】:

        【解决方案10】:

        一些简单的函数,借鉴@milan 的回答:

        const setDifference = (a, b) => new Set([...a].filter(x => !b.has(x)));
        const setIntersection = (a, b) => new Set([...a].filter(x => b.has(x)));
        const setUnion = (a, b) => new Set([...a, ...b]);
        

        用法:

        const a = new Set([1, 2]);
        const b = new Set([2, 3]);
        
        setDifference(a, b); // Set { 1 }
        setIntersection(a, b); // Set { 2 }
        setUnion(a, b); // Set { 1, 2, 3 }
        

        【讨论】:

          【解决方案11】:

          至于禁食的方式,这不是那么优雅,但我已经运行了一些测试来确定。将一个数组作为对象加载在大量处理时要快得多:

          var t, a, b, c, objA;
          
              // Fill some arrays to compare
          a = Array(30000).fill(0).map(function(v,i) {
              return i.toFixed();
          });
          b = Array(20000).fill(0).map(function(v,i) {
              return (i*2).toFixed();
          });
          
              // Simple indexOf inside filter
          t = Date.now();
          c = b.filter(function(v) { return a.indexOf(v) < 0; });
          console.log('completed indexOf in %j ms with result %j length', Date.now() - t, c.length);
          
              // Load `a` as Object `A` first to avoid indexOf in filter
          t = Date.now();
          objA = {};
          a.forEach(function(v) { objA[v] = true; });
          c = b.filter(function(v) { return !objA[v]; });
          console.log('completed Object in %j ms with result %j length', Date.now() - t, c.length);
          

          结果:

          completed indexOf in 1219 ms with result 5000 length
          completed Object in 8 ms with result 5000 length
          

          但是,这仅适用于 字符串。如果您打算比较编号集,您需要使用 parseFloat 映射结果。

          【讨论】:

          • 第二个函数不应该是c = b.filter(function(v) { return !A[v]; });吗?
          • 你是对的。不知何故,它对我来说似乎更快
          【解决方案12】:

          这行得通,但我认为另一个更短,也更优雅

          A = [1, 'a', 'b', 12];
          B = ['a', 3, 4, 'b'];
          
          diff_set = {
              ar : {},
              diff : Array(),
              remove_set : function(a) { ar = a; return this; },
              remove: function (el) {
                  if(ar.indexOf(el)<0) this.diff.push(el);
              }
          }
          
          A.forEach(diff_set.remove_set(B).remove,diff_set);
          C = diff_set.diff;
          

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2011-11-15
            • 2013-09-29
            • 1970-01-01
            • 2016-12-20
            • 1970-01-01
            相关资源
            最近更新 更多