两个地图之间的区别

18

我需要在Clojure/Java中高效地比较两个Map,并使用Java的.equals(..)方法返回差异,其中nil/null等效于“不出现”。

也就是说,我正在寻找编写此类函数的最有效方法:

(map-difference
  {:a 1, :b nil, :c 2, :d 3}
  {:a 1, :b "Hidden", :c 3, :e 5})

=> {:b nil, :c 2, :d 3, :e nil}

我希望输出一个不可变的Clojure映射,如果性能有显著提升,Java映射也可以接受。

值得一提的是,对于任何两个映射a和b,我的基本测试用例/行为期望是它们将相等(在null =“不存在”等价的情况下):

a 
(merge b (difference a b))

最佳的实现方式是什么?


1
老生常谈,但我想知道Clojure 1.3中的clojure.data.diff在你的问题上表现如何? - Marko Topolnik
7个回答

12

我不确定最高效的方法是什么,但这里有几个可能有用的建议:

  1. 问题文本的基本预期行为是不可能的:如果 ab 是地图,使得 b 包含至少一个在 a 中不存在的键,则 (merge b <sth>) 不能等于 a

  2. 如果您最终选择使用interop解决方案,但随后需要返回到 PersistentHashMap,则始终可以使用

(clojure.lang.PersistentHashMap/create
 (doto (java.util.HashMap.)
   (.put :foo 1)
   (.put :bar 2)))
; => {:foo 1 :bar 2}
  • 如果你需要将Clojure map的键集传递给Java方法,可以使用

  • (.keySet {:foo 1 :bar 2})
    ; => #< [:foo, :bar]>
    
  • 如果涉及的所有键都保证是 Comparable,这可以被利用来高效计算具有许多键的映射的 difference(排序和合并扫描)。对于无限制的键,这当然是不可行的,并且对于小型映射,实际上可能会降低性能。

  • 有一个用Clojure编写的版本是很好的,即使只是为了设置基准性能期望。 这是一个: (已更新)

  • (defn map-difference [m1 m2]
            (loop [m (transient {})
                   ks (concat (keys m1) (keys m2))]
              (if-let [k (first ks)]
                (let [e1 (find m1 k)
                      e2 (find m2 k)]
                  (cond (and e1 e2 (not= (e1 1) (e2 1))) (recur (assoc! m k (e1 1)) (next ks))
                        (not e1) (recur (assoc! m k (e2 1)) (next ks))
                        (not e2) (recur (assoc! m k (e1 1)) (next ks))
                        :else    (recur m (next ks))))
                (persistent! m))))
    
    我认为只需执行(concat (keys m1) (keys m2)),即使可能会重复一些工作,但大多数情况下比在每一步都检查给定的键是否也存在于“另一个映射”中更有效率。
    为了总结答案,这里是一个基于集合的非常简单的版本,它有一个不错的特性,即它可以表达它的意思 - 如果我误解了规范,在这里应该很容易看出来。 :-)
    (defn map-difference [m1 m2]
      (let [ks1 (set (keys m1))
            ks2 (set (keys m2))
            ks1-ks2 (set/difference ks1 ks2)
            ks2-ks1 (set/difference ks2 ks1)
            ks1*ks2 (set/intersection ks1 ks2)]
        (merge (select-keys m1 ks1-ks2)
               (select-keys m2 ks2-ks1)
               (select-keys m1
                            (remove (fn [k] (= (m1 k) (m2 k)))
                                    ks1*ks2)))))
    

    太棒了Michal,非常感谢!关于1)只有一个要点,如果您指定nil等同于不存在(根据问题),我认为通过将map-difference分配nil给需要“删除”的键可以实现要求。 - mikera
    希望你知道defrecords?如果你不需要地图是通用的,那么它们可能是一个解决方案。 - nickik
    @mikera:谢谢,很高兴听到这个消息。 :-) 至于1),这是一个好观点,对任何解决方案来说都应该是一个小改进 - 感谢更正! @nickik:我不确定你有什么想法。你可以详细说明一下吗? - Michał Marczyk
    @Michal Marczyk - 你那个优雅的第一基线方案怎么了?我想看看它,但现在它不见了。 - Adam Schmideg
    @Adam Schmideg:我发现它不符合规范并将其编辑删除。您可以在此答案的修订历史记录中查看(从回答文本下方的“编辑于xxx时间”通知中链接到)。 - Michał Marczyk

    6

    1
    谢谢!虽然这很有用,但比我想要的更加通用(它生成了完整的地图比较,而不是我要找的简单差异地图)。 - mikera

    3

    使用内置的集合API:

    Set<Map.Entry<K,V>> difference = a.entrySet().removeAll(b.entrySet());
    

    如果你需要将其转换回地图,则必须进行迭代。在这种情况下,我建议:

    Map<K,V> result = new HashMap<K,V>(Math.max(a.size()), b.size()));
    Set<Map.Entry<K,V>> filter = b.entrySet();
    for( Map.Entry<K,V> entry : a.entrySet ) {
        if( !filter.contains( entry ) {
            result.put(entry.getKey(), entry.getValue());
        }
    }
    

    3

    我不确定它的性能如何。

    (defn map-difference
      [orig other]
      (let [changed (set/difference (set orig) (set other))
            added (set/difference (set (keys other)) (set (keys orig)))]
        (reduce (fn [acc key]
                  (assoc acc key :missing))
          (into {} changed)
          added)))
    

    我使用了:missing键来避免原始映射中的nil值和缺失键之间的歧义 -- 当然你也可以将其更改为nil


    3
    1. 使用Clojure maps 是可以的,因为读取 Clojure maps 非常快。

    2. 我无法回答你,但我可以给你一些东西供你参考。Brenton Ashworth 制作了一个测试工具,在那里他解决了与 map 比较相关的问题。也许你可以查看他的代码以获取实现提示。 http://formpluslogic.blogspot.com/2010/07/better-clojure-test-results-with-deview.htmlhttp://github.com/brentonashworth/deview

    3. 我认为没有比比较键并查找是否不同的更好的实现方法。


    请修正您第三点的语法,它没有意义。 - Svante

    2

    0

    关于这个问题...

    (defn map-diff [m1 m2]
      ;; m1: hashmap
      ;; m2: hashmap
      ;; => the difference between them
      (reduce merge
              (map #(hash-map % (- (or (% m1) 0) (or (% m2) 0)))
                   (keys (merge m1 m2)))))
    

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接