Clojure:将一个序列拆分为top-n并休息?

时间:2013-09-11 11:13:06

标签: clojure

我想将一个序列分成前n个元素和其余元素。这是使用内置排序和拆分的低效实现:

> (defn split-top-n
    [n comp coll]
    (split-at n (sort comp coll)))

> (split-top-n 2 #(- %2 %1) (list 6.2 5.1 88.0 90.1 1.2 16.9))
[(90.1 88.0) (16.9 6.2 5.1 1.2)]

这是否内置了高效的Clojure?或者我需要自己编写吗?

3 个答案:

答案 0 :(得分:5)

标准库中没有这样的功能。您已经编写的简单实现对n小值的特殊情况实际上效率不高,但在一般情况下完全没问题。

只要您不知道当前实现中的此功能确实是整个应用程序中的重要性能瓶颈,那么编写更复杂的版本可能会浪费精力。

编辑:更多地考虑这个问题,尝试编写一个将序列强制转换为矢量然后执行就地 Quickselect 的实现可能值得一试。将n个最佳元素分区到向量的开头。这应该是相对容易的,并且只要您的枢轴元素被很好地选择,就可以提供合理的更好的性能。

编辑2 :我决定自己尝试一下这个实现。它在一些简单的测试用例中运行良好,但我并不完全确定在某些边缘情况下可能会触发一个错误:

(defn split-top-n
  [n comp coll]
  (let [v (transient (vec coll))]
    (loop [start 0, end (count v)]
      (when (> end n)
        (let [pos (loop [i (inc start), pos start]
                    (if (< i end)
                      (if (comp (v i) (v start))
                        (let [pos* (inc pos)]
                          (assoc! v, i (v pos*), pos* (v i))
                          (recur (inc i) pos*))
                        (recur (inc i) pos))
                      (do
                        (assoc! v, start (v pos), pos (v start))
                        pos)))]
          (if (< pos n)
            (recur (inc pos) end)
            (recur start pos)))))
    (split-at n (persistent! v))))

澄清:这需要comp的简单布尔比较器函数,而不是负数/零/正数类型之一。

编辑3 :我再看一下瞬态文档,发现我正在利用未定义的行为。实际情况可能是上述版本实际上总是按预期工作,但正确的版本应该尊重语言文档。我将在此答案中留下以前的版本,因为答案已经被接受了,但是这里的版本使用assoc!的返回值作为文档要求:

(defn swap-in!
  [v i j]
  (assoc! v, i (v j), j (v i)))

(defn quickpartition!
  [comp v start end]
  (loop [v v, i (inc start), pos start]
    (if (< i end)
      (if (comp (v i) (v start))
        (recur (swap-in! v i (inc pos)) (inc i) (inc pos))
        (recur v (inc i) pos))
      [(swap-in! v start pos) pos])))

(defn split-top-n
  [n comp coll]
  (loop [v (transient (vec coll)), start 0, end (count v)]
    (if (> end n)
      (let [[v* pos] (quickpartition! comp v start end)]
        (if (< pos n)
          (recur v* (inc pos) end)
          (recur v* start pos)))
      (split-at n (persistent! v)))))

编辑4 :早期版本的可读性差仍然让我感到困惑,所以我现在将我的实现分成多个功能。

答案 1 :(得分:2)

您可以使用当前实现为 clojure.lang.PersistentTreeSet 的数据结构,例如sorted-set。通过这种方式,您可以在获得前n个元素之前避免排序(我会说)。

(-> (sorted-set-by >) 
    (conj 90) 
    (conj 10) 
    (conj 1))

#{90 10 1}

现在你可以调用split-at函数:

(split-at n previous-sorted-set)

但这取决于你是否想要/可以使用有序集。

答案 2 :(得分:0)

看起来可能用于finger trees

(require '[clojure.data.finger-tree :as ft])
(def css (apply ft/counted-sorted-set (list 6.2 5.1 88.0 90.1 1.2 16.9)))
(ft/ft-split-at css 3)

[(1.2 5.1 6.2) 16.9 (88.0 90.1)]