Clojure-从序列中的向量计算唯一值

Clojure-从序列中的向量计算唯一值,clojure,Clojure,作为Clojure的新手,我似乎不知道如何做一些看似简单的事情。我就是看不见。我有一系列向量。假设每个向量有两个值,分别表示客户编号和发票编号,每个向量表示一个项目的销售。所以它看起来像这样: ([ 100 2000 ] [ 100 2000 ] [ 101 2001 ] [ 100 2002 ]) 我要计算唯一客户和唯一发票的数量。所以这个例子应该产生向量 [ 2 3 ] 在Java或另一种命令式语言中,我会循环遍历seq中的每一个向量,将客户编号和发票编号添加到一个集合中,然后计算每个集

作为Clojure的新手,我似乎不知道如何做一些看似简单的事情。我就是看不见。我有一系列向量。假设每个向量有两个值,分别表示客户编号和发票编号,每个向量表示一个项目的销售。所以它看起来像这样:

([ 100 2000 ] [ 100 2000 ] [ 101 2001 ] [ 100 2002 ])
我要计算唯一客户和唯一发票的数量。所以这个例子应该产生向量

[ 2 3 ]
在Java或另一种命令式语言中,我会循环遍历seq中的每一个向量,将客户编号和发票编号添加到一个集合中,然后计算每个集合中的值的数量并返回它。我看不出这样做的实用方法

谢谢你的帮助


编辑:我应该在我的原始问题中指定,向量的seq是百万分之十,实际上不止两个值。因此,我只想对seq进行一次检查,然后计算这些唯一的计数(以及一些总和)。在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
form或tail递归)和集合执行相同的操作,就像在Java中一样。为了进一步提高性能,您还可以使用。不过对于像您这样的初学者,我建议使用
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]
正如你所看到的,不需要原子之类的东西。首先,将状态传递给下一次迭代(recur调用)。其次,您可以使用瞬态来使用临时可变集合(详细信息请阅读瞬态的更多内容),从而避免每次创建新对象

UPD2。以下是扩展问题的
reduce
版本(含价格):


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

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

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

在使用序列时,通常情况下,
reduce
比这里的
loop
更好。您只需执行以下操作:

(map count (reduce (partial map conj) 
                   [#{} #{}]
                   txn))
或者,如果你真的对瞬变感兴趣:

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

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

使用映射和高阶函数可以很好地做到这一点:

(apply 
  map 
  (comp count set list) 
  [[ 100 2000 ] [ 100 2000 ] [ 101 2001 ] [ 100 2002 ]])

=> (2 3)

还有上述nice的其他解决方案:

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

使用线程最后一个宏写入的其他命令:

(->'([100 2000][100 2000][101 2001][100 2002])(应用地图矢量)(地图区分)(地图计数))


两人都回来了(23)。

谢谢。性能确实非常重要,因为事务的收集量达到了1000万。使用循环形式是否需要使用原子或类似的东西来维持循环每次迭代之间的状态?我想这正是让我绊倒的地方。@Davekingaid:请看我的更新。然而,请注意,所有解决方案的时间复杂度都是相同的,所以它们的运行时间只会相差(可能很小)常量乘数。这太棒了!谢谢在发布我的问题并看到您的第一个回复后。我去做了一点实验。这是我想到的。我想知道你是否能帮助我理解你的方法和这个方法之间的区别<代码>(让[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
),并在将变量传递到下一个递归步骤时更改状态(请参见我的示例中的args to recur)。此外,同步对于系统来说可能相当昂贵。其余的差不多。谢谢你的解释。这正是我想要理解的。那太好了!不过,让我给它添一条皱纹吧。向每个向量添加第三个元素,即价格。现在生成一个向量,该向量和以前一样包含计数,但也添加到价格的总和上。这可以用一种类似的干净的方式来完成吗?这当然是可能的,而reduce仍然是最好的方法,但我不打算自己写:P.I尝试了一下。告诉我这有多疯狂。我创建一个函数映射(def-map{0 count 1 count 2(partial reduce+)}),然后使用映射索引在f-map的相应函数上运行每个函数。像这样:(映射索引的#)((获取f-map%1)%2)(reduce(partial map conj)[#[].[]]txn])结果是这个解决方案用太多的数据破坏了堆栈。而“太多”并不是那么多。我要找斯塔科夫
(apply 
  map 
  (comp count set list) 
  [[ 100 2000 ] [ 100 2000 ] [ 101 2001 ] [ 100 2002 ]])

=> (2 3)