将数组比较为(多)集

时间:2013-06-04 09:00:47

标签: javascript arrays

我正在寻找一种有效的方法来查明两个数组是否包含相同数量的相等元素(在==意义上),以任何顺序:

foo = {/*some object*/}
bar = {/*some other object*/}

a = [1,2,foo,2,bar,2]
b = [bar,2,2,2,foo,1]

sameElements(a, b) --> true

PS。请注意,线程中的每个解决方案都使用===而不是==进行比较。这对我的需求来说很好。

8 个答案:

答案 0 :(得分:7)

更新5 我发布了一个新的answer,采用了不同的方法。

更新

我将code扩展为可以referenceequality

进行检查

只需将true作为第二个参数进行参考检查。

我还将示例添加到 Brunos JSPerf

  • 以约11 ops / s的速度运行参考检查

我会尽快评论代码(!),因为我有一些空余时间来解释它,但目前没有时间,sry 。完成

更新2。

就像布鲁诺在评论sameElements([NaN],[NaN])中指出的那样产生false

在我看来,这是正确的行为,因为NaN是暧昧的,并且应该始终导致false结果,至少在比较NaN.equals(NaN)时。但他有一个很好的观点。

是否

[1,2,foo,bar,NaN,3]应该等于[1,3,foo,NaN,bar,2]

好吧......说实话,我是否有点不知所措,所以我添加了两个标志。

  • Number.prototype.equal.NaN
    • 如果是的话
      • NaN.equals(NaN) //true
  • Array.prototype.equal.NaN
    • 如果是的话
      • [NaN].equals([NaN],true) //true
      • 请注意,这仅供参考检查。深入检查无论如何都会调用Number.prototype.equals

更新3:

Dang我完全错过了排序函数中的2行。

已添加

 r[0] = a._srt; //DANG i totally missed this line
 r[1] = b._srt; //And this.

Fiddle

中的第105行

这很重要,因为它决定了元素的一致顺序。

更新4
我试图稍微优化排序功能,并设法将其提高到大约20 ops / s。

下面是更新的代码,以及更新的小提琴=)

此外,我选择在sort函数之外标记对象,它似乎不再产生性能差异,并且更具可读性


以下是使用Object.definePropertyequals函数添加到

的方法

Array,Object,Number,String,Boolean's prototype以避免在一个函数中进行类型检查 表现原因。因为我们可以递归地在任何元素上调用.equals

但是当然检查Objects是否相等可能会导致大对象出现性能问题。

因此,如果有人感到不愉快地操纵原生原型,只需进行类型检查并将其放入一个功能

Object.defineProperty(Boolean.prototype, "equals", {
        enumerable: false,
        configurable: true,
        value: function (c) {
            return this == c; //For booleans simply return the equality
        }
    });

Object.defineProperty(Number.prototype, "equals", {
        enumerable: false,
        configurable: true,
        value: function (c) {
            if (Number.prototype.equals.NaN == true && isNaN(this) && c != c) return true; //let NaN equals NaN if flag set
            return this == c; // else do a normal compare
        }
    });

Number.prototype.equals.NaN = false; //Set to true to return true for NaN == NaN

Object.defineProperty(String.prototype, "equals", {
        enumerable: false,
        configurable: true,
        value: Boolean.prototype.equals //the same (now we covered the primitives)
    });

Object.defineProperty(Object.prototype, "equals", {
        enumerable: false,
        configurable: true,
        value: function (c, reference) {
            if (true === reference) //If its a check by reference
                return this === c; //return the result of comparing the reference
            if (typeof this != typeof c) { 
                return false; //if the types don't match (Object equals primitive) immediately return
            }
            var d = [Object.keys(this), Object.keys(c)],//create an array with the keys of the objects, which get compared
                f = d[0].length; //store length of keys of the first obj (we need it later)
            if (f !== d[1].length) {//If the Objects differ in the length of their keys
                return false; //immediately return
            }
            for (var e = 0; e < f; e++) { //iterate over the keys of the first object
                if (d[0][e] != d[1][e] || !this[d[0][e]].equals(c[d[1][e]])) {
                    return false; //if either the key name does not match or the value does not match, return false. a call of .equal on 2 primitives simply compares them as e.g Number.prototype.equal gets called
                }
            }
            return true; //everything is equal, return true
        }
    });
Object.defineProperty(Array.prototype, "equals", {
        enumerable: false,
        configurable: true,
        value: function (c,reference) {

            var d = this.length;
            if (d != c.length) {
                return false;
            }
            var f = Array.prototype.equals.sort(this.concat());
            c = Array.prototype.equals.sort(c.concat(),f)

            if (reference){
                for (var e = 0; e < d; e++) {
                    if (f[e] != c[e] && !(Array.prototype.equals.NaN && f[e] != f[e] && c[e] != c[e])) {
                        return false;
                    }
                }                
            } else {
                for (var e = 0; e < d; e++) {
                    if (!f[e].equals(c[e])) {
                        return false;
                    }
                }
            }
            return true;

        }
    });

Array.prototype.equals.NaN = false; //Set to true to allow [NaN].equals([NaN]) //true

Object.defineProperty(Array.prototype.equals,"sort",{
  enumerable:false,
  value:function sort (curr,prev) {
         var weight = {
            "[object Undefined]":6,         
            "[object Object]":5,
            "[object Null]":4,
            "[object String]":3,
            "[object Number]":2,
            "[object Boolean]":1
        }
        if (prev) { //mark the objects
            for (var i = prev.length,j,t;i>0;i--) {
                t = typeof (j = prev[i]);
                if (j != null && t === "object") {
                     j._pos = i;   
                } else if (t !== "object" && t != "undefined" ) break;
            }
        }

        curr.sort (sorter);

        if (prev) {
            for (var k = prev.length,l,t;k>0;k--) {
                t = typeof (l = prev[k]);
                if (t === "object" && l != null) {
                    delete l._pos;
                } else if (t !== "object" && t != "undefined" ) break;
            }
        }
        return curr;

        function sorter (a,b) {

             var tStr = Object.prototype.toString
             var types = [tStr.call(a),tStr.call(b)]
             var ret = [0,0];
             if (types[0] === types[1] && types[0] === "[object Object]") {
                 if (prev) return a._pos - b._pos
                 else {
                     return a === b ? 0 : 1;
                 }
             } else if (types [0] !== types [1]){
                     return weight[types[0]] - weight[types[1]]
             }



            return a>b?1:a<b?-1:0;
        }

    }

});

有了这个,我们可以将sameElements函数减少到

function sameElements(c, d,referenceCheck) {
     return c.equals(d,referenceCheck);  //call .equals of Array.prototype.
}

注意。当然,您可以将所有相同的函数放入sameElements函数中,以获得类型检查的成本。

现在这里有3个例子:1个深度检查,2个参考检查。

var foo = {
    a: 1,
    obj: {
        number: 2,
        bool: true,
        string: "asd"
    },
    arr: [1, 2, 3]
};

var bar = {
    a: 1,
    obj: {
        number: 2,
        bool: true,
        string: "asd"
    },
    arr: [1, 2, 3]
};

var foobar = {
    a: 1,
    obj: {
        number: 2,
        bool: true,
        string: "asd"
    },
    arr: [1, 2, 3, 4]
};

var a = [1, 2, foo, 2, bar, 2];
var b = [foo, 2, 2, 2, bar, 1];
var c = [bar, 2, 2, 2, bar, 1];

所以这些是我们比较的数组。输出是

  1. 仅使用参考文献检查ab

    console.log (sameElements ( a,b,true)) //true As they contain the same elements

  2. 仅使用参考

    检查bc

    console.log (sameElements (b,c,true)) //false as c contains bar twice.

  3. 深入检查bc

    console.log (sameElements (b,c,false)) //true as bar and foo are equal but not the same

  4. 检查2个包含NaN的阵列

    Array.prototype.equals.NaN = true;
    console.log(sameElements([NaN],[NaN],true)); //true.
    Array.prototype.equals.NaN = false;

  5. 演示JSFiddle

答案 1 :(得分:2)

或许这样吗?

var foo = {}; var bar=[];
var a = [3,2,1,foo]; var b = [foo,1,2,3];

function comp(a,b)
{
    // immediately discard if they are of different sizes
    if (a.length != b.length) return false;

    b = b.slice(0); // clone to keep original values after the function

    a.forEach(function(e) {
        var i;
        if ((i = b.indexOf(e)) != -1)
            b.splice(i, 1);
    });

    return !b.length;
}

comp(a,b);

答案 2 :(得分:2)

您可以实施以下算法:

  • 如果ab的长度不同:
    • 返回false
  • 否则:
    • 克隆b
    • a中的每个项目:
      • 如果该项目存在于b的克隆中:
        • 从我们的b
        • 克隆中删除该项目
      • 否则:
        • 返回false
    • 返回true

使用Javascript 1.6,您可以使用every()indexOf()来编写:

function sameElements(a, b)
{
    if (a.length != b.length) {
        return false;
    }
    var ourB = b.concat();
    return a.every(function(item) {
        var index = ourB.indexOf(item);
        if (index < 0) {
            return false;
        } else {
            ourB.splice(index, 1);
            return true;
        }
    });
}

请注意,此实现并未完全满足您的要求,因为indexOf()在内部使用严格相等(===)。如果你真的想要非严格的相等(==),你将不得不写一个内部循环。

答案 3 :(得分:2)

<强>更新

正如@Bergi和@ thg435指出我之前的实现存在缺陷所以这是另一个实现:

function sameElements(a, b) {
    var objs = [];
    // if length is not the same then must not be equal
    if (a.length != b.length) return false;

    // do an initial sort which will group types
    a.sort();
    b.sort();

    for ( var i = 0; i < a.length; i++ ) {

        var aIsPrimitive = isPrimitive(a[i]);
        var bIsPrimitive = isPrimitive(b[i]);

        // NaN will not equal itself
        if( a[i] !== a[i] ) {
            if( b[i] === b[i] ) {
                return false;
            }
        }
        else if (aIsPrimitive && bIsPrimitive) {

            if( a[i] != b[i] ) return false;
        }
        // if not primitive increment the __count property
        else if (!aIsPrimitive && !bIsPrimitive) {
            incrementCountA(a[i]);
            incrementCountB(b[i]);
            // keep track on non-primitive objects
            objs.push(i);
        }
        // if both types are not the same then this array
        // contains different number of primitives
        else {
            return false;
        }

    }

    var result = true;

    for (var i = 0; i < objs.length; i++) {
        var ind = objs[i];
        // if __aCount and __bCount match then object exists same
        // number of times in both arrays
        if( a[ind].__aCount !== a[ind].__bCount ) result = false;
        if( b[ind].__aCount !== b[ind].__bCount ) result = false;

        // revert object to what it was 
        // before entering this function
        delete a[ind].__aCount;
        delete a[ind].__bCount;
        delete b[ind].__aCount;
        delete b[ind].__bCount;
    }

    return result;
}

// inspired by @Bergi's code
function isPrimitive(arg) {
    return Object(arg) !== arg;
}

function incrementCountA(arg) {
    if (arg.hasOwnProperty("__aCount")) {
        arg.__aCount = arg.__aCount + 1;
    } else {
        Object.defineProperty(arg, "__aCount", {
            enumerable: false,
            value: 1,
            writable: true,
            configurable: true
        });
    }
}
function incrementCountB(arg) {
    if (arg.hasOwnProperty("__bCount")) {
        arg.__bCount = arg.__bCount + 1;
    } else {
        Object.defineProperty(arg, "__bCount", {
            enumerable: false,
            value: 1,
            writable: true,
            configurable: true
        });
    }
}

然后只需调用函数

sameElements( ["NaN"], [NaN] ); // false

// As "1" == 1 returns true
sameElements( [1],["1"] ); // true

sameElements( [1,2], [1,2,3] ); //false

上述工具实际上定义了一个名为“__count”的新属性,用于跟踪两个数组中的非原始元素。在函数返回之前删除它们,以便像以前一样保留数组元素。

小提琴here

jsperf here

我更改jsperf测试用例的原因是@Bergi声明了测试数组,特别是整个数组中只有2个唯一对象的事实并不代表我们正在测试的内容。

此实现的另一个优点是,如果您需要使其与IE9之前的浏览器兼容,而不是使用defineProperty来创建不可枚举的属性,则可以使用普通属性。

答案 4 :(得分:1)

感谢大家分享想法!我想出了以下

function sameElements(a, b) {
    var hash = function(x) {
        return typeof x + (typeof x == "object" ? a.indexOf(x) : x);
    }
    return a.map(hash).sort().join() == b.map(hash).sort().join();
}

这不是the fastest解决方案,而是IMO,目前为止最具可读性。

答案 5 :(得分:1)

我不确定“===”是否还可以,这个问题有点笨拙...... 如果是这样,这比其他一些可能的方法更快更简单:

function isSame(a,b){
  return a.length==b.length && 
      a.filter(function(a){ return b.indexOf(a)!==-1 }).length == b.length;
}

答案 6 :(得分:1)

编辑2
1)感谢user2357112指出Object.prototype.toString.call问题    这也表明,它是快速的原因,它没有考虑阵列......

我修复了代码,它现在应该正常工作:),不幸的是它现在在chrome上约为59ops / s,在ff上为45ops / s。

小提琴和JSPerf已更新

修改
1)  我修复了代码,它支持引用相同Object的mutliple变量。 比之前慢一点,但在chrome上仍然超过100ops / s。

2) 我尝试使用位掩码而不是数组来保持对象的多个位置,但它的速度接近15ops / s

3)正如评论中指出的那样,我忘记在调用tmp后重置[[get]],修复了代码,小提琴和演出。


所以感谢user2357112和他的Answer继承人另一种使用计数的方法

var sameElements = (function () {
    var f, of, objectFlagName;
    of = objectFlagName = "__pos";
    var tstr = function (o) {
        var t = typeof o;
        if (o === null)
            t = "null";
        return t
    };
    var types = {};
    (function () {
        var tmp = {};
        Object.defineProperty(types, tstr(1), {
            set: function (v) {
                if (f)
                    tmp[v] = -~tmp[v];
                else
                    tmp[v] = ~-tmp[v];
            },
            get: function () {
                var ret = 1;
                for (var k in tmp) {
                    ret &= !tmp[k];
                }
                tmp = {};
                return ret;
            }
        });
    })();
    (function () {
        var tmp = {};
        Object.defineProperty(types, tstr(""), {

            set: function (v) {
                if (f) {
                    tmp[v] = -~tmp[v];
                } else {

                    tmp[v] = ~-tmp[v];
                }
            },
            get: function () {
                var ret = 1;
                for (var k in tmp) {
                    ret &= !tmp[k];
                }
                tmp = {};                
                return ret;
            }
        });
    })();

    (function () {
        var tmp = [];
        function add (v) {
            tmp.push(v);
            if (v[of]===undefined) {
                v[of] = [tmp.length -1];
            } else {
                v[of].push(tmp.length -1)
            }            

        }
        Object.defineProperty(types, tstr({}), {
            get: function () {
                var ret = true;
                for (var i = tmp.length - 1; i >= 0; i--) {
                    var c = tmp[i]
                    if (typeof c !== "undefined") {
                        ret = false
                        delete c[of]
                    }
                }
                tmp = [];
                return ret;
            },
            set: function (v) {
                var pos;
                if (f) {
                    add (v);
                } else if (!f && (pos = v[of]) !== void 0) {
                       tmp[pos.pop()] = undefined;
                       if (pos.length === 0)
                            delete v[of];
                } else {
                        add (v);
                }
            }
        });
    }());
    (function () {
        var tmp = 0;
        Object.defineProperty(types, tstr(undefined), {
            get: function () {
                var ret = !tmp;
                tmp = 0;
                return ret;

            },
            set: function () {
                tmp += f ? 1 : -1;
            }
        });
    })();
    (function () {
        var tmp = 0;
        Object.defineProperty(types, tstr(null), {
            get: function () {
                var ret = !tmp;
                tmp = 0;
                return ret;
            },
            set: function () {
                tmp += f ? 1 : -1;
            }
        });
    })();

    var tIt = [tstr(1), tstr(""), tstr({}), tstr(undefined), tstr(null)];

    return function eq(a, b) {

        f = true;
        for (var i = a.length - 1; i >= 0; i--) {
            var v = a[i];
            types[tstr(v)] = v;
        }
        f = false;
        for (var k = b.length - 1; k >= 0; k--) {
            var w = b[k];
            types[tstr(w)] = w;
        }
        var r = 1;
        for (var l = 0, j; j = tIt[l]; l++) {
            r &= types [j]
        }

        return !!r;
    }
    })()

以下是 JSFiddle JSPerf (它使用与先前答案中相同的阵列a和b )使用此代码与Closure compiled

继承人的输出。 注意:它不再支持深度比较,因为

var foo = {a:2}    
var bar = {a:1};
var a = [1, 2, foo, 2, bar, 2];
var b = [foo, 2, 2, 2, bar, 1];
var c = [bar, 2, 2, 2, bar, 1];
console.log(sameElements([NaN],[NaN])); //true
console.log (sameElements ( a,b))  //true
console.log (sameElements (b,c))   //false

答案 7 :(得分:0)

使用高效的查找表来计算元素的数量:

function sameElements(a) { // can compare any number of arrays
    var map, maps = [], // counting booleans, numbers and strings
        nulls = [], // counting undefined and null
        nans = [], // counting nans
        objs, counts, objects = [],
        al = arguments.length;

    // quick escapes:
    if (al < 2)
        return true;
    var l0 = a.length;
    if ([].slice.call(arguments).some(function(s) { return s.length != l0; }))
        return false;

    for (var i=0; i<al; i++) {
        var multiset = arguments[i];
        maps.push(map = {}); // better: Object.create(null);
        objects.push({vals: objs=[], count: counts=[]});
        nulls[i] = 0;
        nans[i] = 0;
        for (var j=0; j<l0; j++) {
            var val = multiset[j];
            if (val !== val)
                nans[i]++;
            else if (val === null)
                nulls[i]++;
            else if (Object(val) === val) { // non-primitive
                var ind = objs.indexOf(val);
                if (ind > -1)
                    counts[ind]++;
                else
                    objs.push(val), counts.push(1);
            } else { // booleans, strings and numbers do compare together
                if (typeof val == "boolean")
                    val = +val;
                if (val in map)
                    map[val]++;
                else
                    map[val] = 1;
            }
        }
    }

    // testing if nulls and nans are the same everywhere
    for (var i=1; i<al; i++)
        if (nulls[i] != nulls[0] || nans[i] != nans[0])
            return false;

    // testing if primitives were the same everywhere
    var map0 = maps[0];
    for (var el in map0)
        for (var i=1; i<al; i++) {
            if (map0[el] !== maps[i][el])
                return false;
            delete maps[i][el];
        }
    for (var i=1; i<al; i++)
        for (var el in maps[i])
            return false;

    // testing if objects were the same everywhere
    var objs0 = objects[0].vals,
        ol = objs0.length;
        counts0 = objects[0].count;
    for (var i=1; i<al; i++)
        if (objects[i].count.length != ol)
            return false;
    for (var i=0; i<ol; i++)
        for (var j=1; j<al; j++)
            if (objects[j].count[ objects[j].vals.indexOf(objs0[i]) ] != counts0[i])
                return false; 

    // else, the multisets are equal:
    return true;
}

它仍在所有对象中使用indexOf搜索,因此如果您有多个具有许多不同对象的对象,您可能也希望优化该部分。查看Unique ID or object signature(以及它是重复的问题),了解如何为它们获取查找表键。如果你在多集合中没有很多原始值,你可能只是将它们存储在数组中并在逐个比较每个项目之前对它们进行排序(如@Bruno所做的那样)。

免责声明:此解决方案不会尝试获取[[PrimitiveValue]]个对象,它们永远不会被视为等于原语(而==会这样做。)

Here是对@ Bruno的jsperf测试答案的更新,但我想只有两个对象(每个对象在10k数组中出现500次)并且没有重复的原始值不具代表性。