【问题标题】:Algorithm for determining equality of circular data structures in JavaScriptJavaScript 中判断循环数据结构是否相等的算法
【发布时间】:2017-03-30 01:03:53
【问题描述】:
var ones = [1];
ones[1] = ones;
ones;
// => [1, [1, [1, [1, [1, ...]]]]]

var ones_ = [1];
ones_[1] = ones_;
ones_;
// => [1, [1, [1, [1, [1, ...]]]]]

如何确定onesones_ 相等?是否有处理上述循环结构的算法?

【问题讨论】:

  • 澄清一下,您是否也希望将对象var foo = [1]; foo[1] = [1, foo]; 视为与上述对象相同?
  • 正确。恰好与值关联的标识符是不相关的。
  • 特别是我有兴趣更新 Z.equals (source) 以将等效的圆形结构视为平等。当前算法检测循环引用以避免无限期递归,但只要检测到循环引用,它就会简单地返回false。我希望有人能指点我一篇描述适用于 JavaScript 上下文的通用算法的论文。
  • 我误读了您的评论,@IlmariKaronen。我现在明白了你问的问题,即递归开始的深度是否重要。我不认为这很重要。表示值[1, [1, [1, [1, [1, ...]]]]] 的两个对象应该被认为是相等的,即使它们的结构不相同。
  • 我想说递归类型和包装值的类型都必须是 setoids,此外,递归类型必须是可折叠的。 “圆形”到底是什么意思?您只需在示例中显示一个链接列表。

标签: javascript functional-programming equality


【解决方案1】:

解决这个问题的一个基本方法是注意,如果在递归比较期间,我们最终再次比较了我们已经在比较的同一对对象,那么 我们可以简单地假设它们'重新相等。这是可行的,因为如果它们毕竟相等,那么已经在进行中的比较最终会发现它们之间存在一些差异。

因此,我们可以简单地从一个基本的递归比较函数开始,然后添加一堆当前正在比较的对象:

function isEqual (a, b) {
    var stack = [];
    function _isEqual (a, b) {
        // console.log("->", stack.length);
        // handle some simple cases first
        if (a === b) return true;
        if (typeof(a) !== "object" || typeof(b) !== "object") return false;
        // XXX: typeof(null) === "object", but Object.getPrototypeOf(null) throws!
        if (a === null || b === null) return false;
        var proto = Object.getPrototypeOf(a);
        if (proto !== Object.getPrototypeOf(b)) return false;
        // assume that non-identical objects of unrecognized type are not equal
        // XXX: could add code here to properly compare e.g. Date objects
        if (proto !== Object.prototype && proto !== Array.prototype) return false;

        // check the stack before doing a recursive comparison
        for (var i = 0; i < stack.length; i++) {
            if (a === stack[i][0] && b === stack[i][1]) return true;
            // if (b === stack[i][0] && a === stack[i][1]) return true;
        }

        // do the objects even have the same keys?
        for (var prop in a) if (!(prop in b)) return false;
        for (var prop in b) if (!(prop in a)) return false;

        // nothing to do but recurse!
        stack.push([a, b]);
        for (var prop in a) {
            if (!(_isEqual(a[prop], b[prop]))) {
                stack.pop();
                return false;
            }
        }
        stack.pop();
        return true;
    }
    return _isEqual(a, b);
}

// TEST CASES:

var ones = [1]; ones[1] = ones;
var foo = [1]; foo[1] = [1, foo];
var bar = [1]; bar[1] = [1, ones];
console.log("ones == foo:", isEqual(ones, foo));
console.log("ones == bar:", isEqual(ones, bar));
console.log("foo == bar:", isEqual(foo, bar));

var obj = {}; obj["x"] = obj; obj["y"] = {obj};
console.log("obj == obj[x]:", isEqual(obj, obj["x"]));
console.log("obj != obj[y]:", !isEqual(obj, obj["y"]));

var seven = []; seven[0] = [[[[[[seven]]]]]];
var eleven = []; eleven[0] = [[[[[[[[[[eleven]]]]]]]]]];
console.log("seven == eleven:", isEqual(seven, eleven));
console.log("[seven] == [eleven]:", isEqual([seven], [eleven]));
console.log("[seven] == seven:", isEqual([seven], seven));
console.log("[seven] == [[[eleven]]]:", isEqual([seven], [[[eleven]]]));

请注意,上面代码中的许多复杂性是由于它试图接受并(或多或少)优雅地处理不同类型的 JavaScript 值的任何混合,包括原语、空值、数组、普通对象和所有JS 变量可以包含的其他杂项。如果您知道您的输入只能包含有限范围的数据类型,那么可以大大简化此代码。

附言。由于堆栈比较,这段代码的运行时间可以达到 O(nd),其中 n 是树中需要比较的节点数 (这可能超出预期;例如,比较上面 sn-p 中的对象 seveneleven 需要 77 次递归调用)并且 d 是堆栈的深度(其中,在这种情况下,也高达77)。在 ES2015 中,一个潜在有用的优化可能是使用先前看到的对象的 Map 将堆栈查找从 O(d) 减少到有效的 O(1)。如果我们期望被比较的数据结构通常有很多包含对相同对象的引用的重复分支,那么将其扩展为我们已经发现相等的先前看到的对象的通用缓存甚至可能是有用的。

【讨论】:

    【解决方案2】:

    您可以通过迭代对象并用“指针”(=某些数组中的索引)替换已经看到的对象来“回收”对象。一旦你得到 decycled 结构,只需将它们序列化并作为字符串进行比较:

    let encode = function (x) {
    
        function _enc(x, lst) {
            if (typeof x !== 'object')
                return x;
    
            let i = lst.indexOf(x);
            if (i >= 0)
                return {'->': i};
            lst.push(x);
    
            let y = {};
            for (let k of Object.keys(x))
                y[k] = _enc(x[k], lst)
    
            return y;
        }
    
        return JSON.stringify(_enc(x, []));
    };
    
    //////
    
    let ones = [1];
    ones[1] = ones;
    
    let ones_ = [1];
    ones_[1] = ones_;
    
    console.log(encode(ones) === encode(ones_))
    
    // more interesting example
    
    a = {
        b: {
            c: 123
        }
    };
    
    a.b.d = a;
    a.x = [9, a.b];
    
    
    a2 = {
        b: {
            c: 123
        }
    };
    
    a2.b.d = a2;
    a2.x = [9, a2.b];
    
    console.log(encode(a) === encode(a2))

    【讨论】:

    • 一个聪明的主意。唉,这段代码不会正确地将我的var foo = [1]; foo[1] = [1, foo] 对象识别为与 OP 的var ones = [1]; ones[1] = ones 相同。 (另外,另一个较小的问题是,如果数据结构包含具有单个键 "-&gt;" 和整数值的对象,它可能会给出误报。)
    • 尽管[1, {'-&gt;': 0}] 不等于ones,但encode([1, {'-&gt;': 0}])encode(ones) 是等价的这一事实是一个问题。我正在寻找一种适用于任何数组对的通用算法。
    • @IlmariKaronen:一个有趣的问题是如何正式定义此类结构的“平等”,例如1-&gt;2-&gt;3-&gt;1...1-&gt;2-&gt;3-&gt;1-&gt;2-&gt;3-&gt;1...。就像“如果......两个循环图被认为是'相等'”什么?
    • @georg:我认为此类图等价的自然定义是bisimulation,我相信我的答案中的算法可以有效计算。基本上,应用于嵌套数据结构,它有效地表示两个数据结构是等价的,如果任何一个结构的有效索引链(如 struct1[key1][key2]...[keyN])对另一个结构也有效,并产生一个等价的值。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-07-25
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多