【问题标题】:Clojure - counting unique values from vectors in a seqClojure - 计算序列中向量的唯一值
【发布时间】:2012-07-11 12:33:11
【问题描述】:

对 Clojure 有点陌生,我似乎无法弄清楚如何做一些看起来应该很简单的事情。我只是看不到它。我有一个向量序列。假设每个向量都有两个值,分别代表客户编号和发票编号,每个向量代表一件商品的销售。所以它看起来像这样:

([ 100 2000 ] [ 100 2000 ] [ 101 2001 ] [ 100 2002 ])

我想计算唯一客户和唯一发票的数量。所以这个例子应该产生向量

[ 2 3 ]

在 Java 或其他命令式语言中,我将遍历 seq 中的每个向量,将客户编号和发票编号添加到一组中,然后计算每组中值的数量并返回它。我看不到执行此操作的实用方法。

感谢您的帮助。

编辑:我应该在我原来的问题中指定向量的 seq 是 10 的数百万并且实际上不止两个值。因此,我只想遍历 seq 一次,并在遍历 seq 的那一次上计算这些唯一计数(以及一些总和)。

【问题讨论】:

    标签: clojure


    【解决方案1】:

    在 Clojure 中,您可以使用几乎相同的方式进行操作 - 首先调用 distinct 获取唯一值,然后使用 count 计算结果:

    (def vectors (list [ 100 2000 ] [ 100 2000 ] [ 101 2001 ] [ 100 2002 ]))
    (defn count-unique [coll] 
       (count (distinct coll)))
    (def result [(count-unique (map first vectors)) (count-unique (map second vectors))])
    

    请注意,这里首先获取向量的第一个和第二个元素的列表(映射第一个/第二个向量),然后分别对每个元素进行操作,从而对集合进行两次迭代。如果性能确实很重要,您可以对迭代(参见loop 形式或尾递归)和集合做同样的事情,就像在 Java 中一样。要进一步提高性能,您还可以使用transients。虽然对于像你这样的初学者,我会推荐使用distinct 的第一种方法。

    UPD。这是带循环的版本:

    (defn count-unique-vec [coll]
      (loop [coll coll, e1 (transient #{}), e2 (transient #{})]
        (cond (empty? coll) [(count (persistent! e1)) (count (persistent! e2))]
              :else (recur (rest coll)
                           (conj! e1 (first (first coll)))
                           (conj! e2 (second (first coll)))))))
    (count-unique-vec vectors)    ==> [2 3]
    

    如您所见,不需要原子或类似的东西。首先,您将状态传递给每次下一次迭代(重复调用)。其次,您使用瞬态来使用 临时可变 集合(详细了解瞬态),从而避免每次都创建新对象。

    UPD2. 这是带有reduce 的扩展问题版本(含价格):

    (defn count-with-price
      "Takes input of form ([customer invoice price] [customer invoice price] ...)  
       and produces vector of 3 elements, where 1st and 2nd are counts of unique    
       customers and invoices and 3rd is total sum of all prices"
      [coll]
      (let [[custs invs total]
            (reduce (fn [[custs invs total] [cust inv price]]
                      [(conj! custs cust) (conj! invs inv) (+ total price)])
                [(transient #{}) (transient #{}) 0]
                coll)]
        [(count (persistent! custs)) (count (persistent! invs)) total]))
    

    在这里,我们将中间结果保存在向量[custs invs total] 中,每次解包、处理并将它们打包回向量中。如您所见,用reduce 实现这种非平凡的逻辑更难(编写和读取)并且需要更多代码(在looped 版本中,为价格添加一个更多参数就足够了循环args)。所以我同意@ammaloy 的观点,对于更简单的情况reduce 更好,但更复杂的事情需要更多的低级构造,例如loop/recur 对。

    【讨论】:

    • 谢谢。性能确实非常重要,因为交易的收集量达到数百万。那么使用循环形式是否需要使用原子或类似的东西来维持循环的每次迭代之间的状态?我认为这就是让我绊倒的部分。
    • @DaveKincaid:查看我的更新。但是请注意,所有解决方案的时间复杂度都是相同的,因此它们的运行时间将仅相差(可能非常小)常数乘数。
    • 这太棒了!谢谢。在发布我的问题并看到您的第一个回复后。我去尝试了一点。这是我想出的。我想知道您是否可以帮助我了解您的方法与此方法之间可能存在的差异。 (let [ customer-set (atom #{}) invoice-set (atom #{})] (doseq [ [customer invoice] txn ] (swap! customer-set conj customer) (swap! invoice-set conj invoice)) [ (count (deref customer-set)) (count (deref invoice-set)) ])
    • 首先,您的方法是必要的,而且由于 Clojure 主要是函数式语言,在更复杂的情况下,您可能会遇到一些小问题。使用语言主范式总是更好,因为它的风格有更多的编程工具。其次,您使用同步原语,这在此处完全没有必要:在函数式语言中,您使用递归而不是显式循环(如 Java 中的 while)并在将 vars 传递到下一个递归步骤时更改状态(请参阅我的示例中的 args to recur )。同步对于系统来说可能非常昂贵。其余的大致相同。
    • 感谢您的解释。这正是我想要理解的。
    【解决方案2】:

    在使用序列时,reduce 比loop 更好。你可以这样做:

    (map count (reduce (partial map conj) 
                       [#{} #{}]
                       txn))
    

    或者,如果你真的很喜欢瞬变:

    (map (comp count persistent!)
         (reduce (partial map conj!) 
                 (repeatedly 2 #(transient #{}))
                 txn))
    

    这两种解决方案都只遍历输入一次,而且它们所用的代码比循环/递归解决方案少得多。

    【讨论】:

    • 那太好了!不过,让我对此表示不满。向每个向量添加第三个元素,即价格。现在生成一个向量,该向量包括之前的计数,但也增加了价格的总和。可以以类似的干净方式完成吗?
    • 当然可以,reduce 仍然是最好的方法,但我不会自己写:P。
    • 我对此进行了尝试。告诉我这有多疯狂。我创建了一个函数映射(def f-map {0 count 1 count 2 (partial reduce +)}),然后使用 map-indexed 在 f-map 的相应函数上运行每个函数。像这样: (map-indexed #((get f-map %1) %2) (reduce (partial map conj) [#[] #[] []] txn))
    • 事实证明,这个解决方案用太多的数据破坏了堆栈。而“太多”结果并没有那么多。我在数千行中使用 StackOverflowError。
    • 哦,好点。你可以用(comp doall (partial map conj))来解决这个问题。
    【解决方案3】:

    或者您可以使用集合来为您处理重复数据删除,因为集合的最大值可以是任何特定值之一。

    (def vectors '([100 2000] [100 2000] [101 2001] [100 2002]))    
    [(count (into #{} (map first vectors)))  (count (into #{} (map second vectors)))]
    

    【讨论】:

      【解决方案4】:

      这是使用地图和高阶函数执行此操作的好方法:

      (apply 
        map 
        (comp count set list) 
        [[ 100 2000 ] [ 100 2000 ] [ 101 2001 ] [ 100 2002 ]])
      
      => (2 3)
      

      【讨论】:

        【解决方案5】:

        还有其他解决上述问题的方法:

        (map (comp count distinct vector) [ 100 2000 ] [ 100 2000 ] [ 101 2001 ] [ 100 2002 ])

        其他用thread-last宏写的:

        (->> '([100 2000] [100 2000] [101 2001] [100 2002]) (apply map vector) (map distinct) (map count))

        都返回 (2 3)。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2014-02-27
          • 2014-11-19
          • 1970-01-01
          • 1970-01-01
          • 2014-04-07
          • 1970-01-01
          相关资源
          最近更新 更多