【问题标题】:Idiomatic/Efficient Clojure way to intersect two a priori sorted vectors?相交两个先验排序向量的惯用/高效Clojure方法?
【发布时间】:2012-12-19 02:37:21
【问题描述】:

我有一对向量 xy 的唯一项目,我知道每个项目都需要排序。我希望有两者的交集,保持排序顺序。理想情况下,结果将是另一个向量,用于快速随机访问。

下面的生成只是为了举例,我的xy 将预先排序和预先区分(它们实际上是时间样本)。

(defn gen-example [c] (-> (repeatedly c #(-> c rand int)) distinct sort vec))

user=> (def x (gen-example 100000)) (count x)
#'user/x
63161
user=> (def y (gen-example 100000)) (count y)
#'user/y
63224

我知道 Clojure 有 clojure.set/intersection 可以在 sorted-set 上工作。我的xy 具有相同的属性(已排序的不同元素),但类型不同。

问题 1:鉴于 xysorted-sets 已经不同且已排序,是否有更好/更快的方法将它们转换为 sorted-sets?

user=> (time (def ssx (apply sorted-set x)))
"Elapsed time: 607.642592 msecs"
user=> (time (def ssy (apply sorted-set y)))
"Elapsed time: 617.046022 msecs"

现在我已经准备好执行我的交叉路口了

user=> (time (count (clojure.set/intersection ssx ssy)))
"Elapsed time: 355.42534 msecs"
39992

这有点令人失望,粗略地看一下(source clojure.set/intersection) 似乎并没有对这些集合进行排序这一事实进行任何特殊处理。

问题 2:有没有比clojure.set/intersection 更好/更快的方法来执行sorted-sets 的交集?

(defn intersect-sorted-vector [x y] 
  (loop [x (seq x) y (seq y) acc []] 
    (if (and x y)
      (let [x1 (first x) 
            y1 (first y)] 
      (cond 
        ( < x1 y1) (recur (next x) y acc) 
        ( > x1 y1) (recur x (next y) acc) 
        :else (recur (next x) (next y) (conj acc x1))))
    acc)))

事实证明这是一个很好的交易(近 10 倍)。

user=> (time (count (intersect-sorted-vector x y)))
"Elapsed time: 40.142532 msecs"
39992

但是,我不禁觉得我的代码过于程序化/迭代。

问题 3:谁能提出一种更惯用的方法来处理 Clojure 中的一对向量?

【问题讨论】:

    标签: vector clojure


    【解决方案1】:

    通常情况下,快速的 Clojure 代码看起来有点势在必行。函数式代码通常很优雅,但会带来一些相关的性能成本,您必须为此付出代价(懒惰、来自丢弃的不可变对象的额外 GC 压力等)

    此外,转换成套装总是会更昂贵。构建一个集合本身就是一个O(n log n) 操作,但您可以利用向量已经支持在O(n) 时间实现交集操作这一事实。

    您的代码已经很不错了,但您还可以进行更多优化:

    • 使用transient 向量收集结果。对于许多顺序 conj 操作,这些比常规持久向量要快一些。
    • 对向量使用带原语的索引访问,而不是使用第一个/下一个遍历序列。这避免了创建临时 seq 对象(和相关的 GC)

    生成的代码可能类似于:

    (defn intersect-sorted-vector [x y]
      (loop [i (long 0), j (long 0), r (transient [])]
        (let [xi (nth x i nil), yj (nth y j nil)]
          (cond 
            (not (and xi yj)) (persistent! r)
            (< xi yj) (recur (inc i) j r)
            (> xi yj) (recur i (inc j) r)
            :else (recur (inc i) (inc j) (conj! r xi))))))
    
    (time (count (intersect-sorted-vector x y)))
    => "Elapsed time: 5.143687 msecs"
    => 40258
    

    如您所见,这可能会给您带来额外的 6 到 8 倍左右的加速。

    【讨论】:

    • 非常感谢您的回复。在我继续学习 Clojure 的过程中,您给了我很多思考和研究。但是,我认为这里的额外速度很大程度上归功于您优越的运行时环境。在我的系统上,这需要同样多的时间(几乎到毫秒)。
    • 或者,也许只是重复运行我的代码会在你的代码之前触发 GC。我将不得不进一步探索。好东西!
    • 我怀疑 mikera 尝试使用原语 longs 会减慢他的速度,因为 get 不支持原语。所以他最终不断地对整数进行装箱和拆箱,这与他删除的 next 调用一样多。我预测,改用nthint 会加快速度。
    • @amalloy - 好地方!切换到第 n 个,它似乎又给了 x2 的性能提升。
    • 我想你想写(not (and xi yj))。带有or 的版本因(intersect-sorted-vector [1 2 3] [2 3 4]) 上的 NullPointerException 而失败
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2015-12-19
    • 2011-08-24
    • 1970-01-01
    • 2011-05-04
    • 2018-07-23
    • 2016-10-05
    • 1970-01-01
    相关资源
    最近更新 更多