懒散地将序列的多个连续项目折叠为单个项目的最佳方法

时间:2011-07-18 04:16:33

标签: clojure

[注意:标题和文本经过大量编辑,以便更清楚地表明我不是特别在字符串之后,而是在一般序列之后,并且对它进行延迟处理]

以字符序列/字符串为例,假设我想转换字符串

  

“\ t a \ r s \ td \ t \ r \ n f \ r \ n”

  

“a s d f”

更一般地说,我想将序列中的所有连续空格(或任何其他任意项目集)转换为单个项目,并且懒得。

我已经提出了以下 partition-by / mapcat combo ,但想知道是否有更简单或更好的方法(可读性,性能,任何东西)来完成同样的事情。

(defn is-wsp?
  [c]
  (if (#{\space \tab \newline \return} c) true))

(defn collapse-wsp
  [coll]
  (mapcat
   (fn [[first-elem :as s]]
     (if (is-wsp? first-elem) [\space] s))
   (partition-by is-wsp? coll)))

行动中:

=> (apply str (collapse-wsp "\t    a\r          s\td  \t \r \n         f \r\n"))
" a s d f "

更新: 我使用了字符串/字符序列/ wsp作为示例,但我真正想要的是任何类型的序列上的泛型函数,它会折叠任意数量的连续项,这些项是预定义项集的一部分,由一些预定义项。我特别想知道是否有更好的partition-by / mapcat替代方案,如果可以针对'string'特殊情况进行优化,那就更好了。

更新2

这是一个完全懒惰的版本 - 上面的那个并不完全是懒惰的,我担心,除了它正在做多余的is-wsp?检查。我概括了参数名称等,所以它看起来不像是一个你可以通过String.whatever()调用轻松替换的东西 - 它是关于任意序列的。

(defn lazy-collapse
  ([coll is-collapsable-item? collapsed-item-representation] (lazy-collapse coll is-collapsable-item? collapsed-item-representation false))
  ([coll is-collapsable-item? collapsed-item-representation in-collapsable-segment?]
  (let [step (fn [coll in-collapsable-segment?]
               (when-let [item (first coll)]
                 (if (is-collapsable-item? item)
                   (if in-collapsable-segment?
                     (recur (rest coll) true)
                     (cons collapsed-item-representation (lazy-collapse (rest coll) is-collapsable-item? collapsed-item-representation true)))
                   (cons item (lazy-collapse (rest coll) is-collapsable-item? collapsed-item-representation false)))))]
    (lazy-seq (step coll in-collapsable-segment?)))))

这很快,完全懒惰,但我希望能够更简洁地表达,因为我自己很懒。

到目前为止懒惰合作者的基准: 通过查看代码可以很容易地判断代码是否可读,但为了了解它们在性能方面的比较,以下是我的基准测试。我首先检查函数是否完成它应该做的事情,然后我吐出了多长时间

  1. 创建懒惰的seq 1M次
  2. 创建懒惰的seq并将第一个项目移植1M次
  3. 创建懒惰的seq并将第二个项目进行1M次
  4. 创建懒惰的seq并取最后一项(即完全实现懒惰的seq)1M次
  5. 测试1到3是为了至少测量一下懒惰。 我运行了几次测试,并且执行时间没有显着变化。

    user=> (map
       (fn [collapse]
         (println (class collapse) (str "|" (apply str (collapse test-str is-wsp? \space)) "|"))
         (time (dotimes [_ 1000000] (collapse test-str is-wsp? \space)))
         (time (dotimes [_ 1000000] (first (collapse test-str is-wsp? \space))))
         (time (dotimes [_ 1000000] (second (collapse test-str is-wsp? \space))))
         (time (dotimes [_ 1000000] (last (collapse test-str is-wsp? \space)))))
       [collapse-overthink collapse-smith collapse-normand lazy-collapse])
    
    user$collapse_overthink | a s d f |
    "Elapsed time: 153.490591 msecs"
    "Elapsed time: 3064.721629 msecs"
    "Elapsed time: 4337.932487 msecs"
    "Elapsed time: 24797.222682 msecs"
    
    user$collapse_smith | a s d f |
    "Elapsed time: 141.474904 msecs"
    "Elapsed time: 812.998848 msecs"
    "Elapsed time: 2112.331739 msecs"
    "Elapsed time: 10750.224816 msecs"
    
    user$collapse_normand | a s d f |
    "Elapsed time: 314.978309 msecs"
    "Elapsed time: 1423.779761 msecs"
    "Elapsed time: 1669.660257 msecs"
    "Elapsed time: 8074.759077 msecs"
    
    user$lazy_collapse | a s d f |
    "Elapsed time: 169.906088 msecs"
    "Elapsed time: 638.030401 msecs"
    "Elapsed time: 1195.445016 msecs"
    "Elapsed time: 6050.945856 msecs"
    

    到目前为止的底线:最好的代码是最慢的,最丑的代码是最快的。我很确定它不一定是这样......

5 个答案:

答案 0 :(得分:4)

到目前为止,这是我最快的解决方案:(基本上与M Smith相同,但没有解构)

(defn collapse [xs pred rep]
  (when-let [x (first xs)]
    (lazy-seq 
      (if (pred x)
        (cons rep (collapse (drop-while pred (rest xs)) pred rep))
        (cons x (collapse (rest xs) pred rep))))))

这是一个更漂亮的解决方案,但速度要快3倍(!)(实际上与SuperHorst的初始版本相同......)

(defn collapse [col pred rep]
  (let [f (fn [[x & more :as xs]] (if (pred x) [rep] xs))]
    (mapcat f (partition-by #(if (pred %) true) col))))

迷你基准(full code)输出:

$ clj collapse.clj 
     SuperHorst: "Elapsed time: 58535.737037 msecs"
      Overthink: "Elapsed time: 70154.744605 msecs"
        M Smith: "Elapsed time: 89484.984606 msecs"
   Eric Normand: "Elapsed time: 83121.309838 msecs"

示例:

(def test-str "\t a\r      s\td \t \r \n          f \r\n")
(def is-ws? #{\space \tab \newline \return})

user=> (apply str (collapse test-str is-ws? \space))
" a s d f "

用于不同类型的seq:

user=> (collapse (range 1 110) #(= 2 (count (str %))) \X)
(1 2 3 4 5 6 7 8 9 \X 100 101 102 103 104 105 106 107 108 109)

这完全是懒惰的:

user=> (type (collapse test-str is-ws? \space))
clojure.lang.LazySeq
user=> (type (collapse (range 1 110) #(= 2 (count (str %))) \X))
clojure.lang.LazySeq

旧越野车版本:

(defn collapse-bug [col pred rep]
  (let [f (fn [x] (if (pred x) rep x))]
    (map (comp f first) (partition-by f col))))

该错误是它吃掉了与pred不匹配的连续项目。

答案 1 :(得分:2)

为什么不简单地使用Java的String函数replaceAll

user=> (.replaceAll "\t    a\r          s\td  \t \r \n         f \r\n" "\\s+" " ")
" a s d f "

我认为,它已针对此类操作进行了大量优化......

更新:澄清后更新的答案版本:

(defn is-blank? [^Character c] (not (nil? (#{\space \tab \newline \return} c))))

(defn collapse [coll fun replacement]
  (first (reduce (fn [[res replaced?] el]
                   (if (fun el)
                     (if replaced?
                       [res replaced?]
                       [(conj res replacement) true])
                     [(conj res el) false]))
                 [[] false] coll)))

(def aa-str "\t    a\r          s\td  \t \r \n         f \r\n")

user> (apply str (collapse aa-str is-blank? \space))
" a s d f "

它也适用于其他seqs:

user> (collapse [1 1 2 3 1 1 1 4 5 6 1 1] #(= % 1) 9)
[9 2 3 9 4 5 6 9]

另一项性能优化可能是使用瞬态而不是标准序列......

答案 2 :(得分:2)

这是一个完全懒惰的实现,有点清洁:

<!-- language: lang-clojure -->
(defn collapse [ss pred replacement]  
    (lazy-seq    
          (if-let [s (seq ss)]   
              (let [[f & rr] s]  
               (if (pred f)   
                   (cons replacement (collapse (drop-while pred rr) pred replacement))  
                   (cons f (collapse rr pred replacement)))))))

答案 3 :(得分:1)

一种完全懒惰的解决方案,以递归方式编写:

(defn collapse [l p v]
  (cond
    (nil? (seq l))
    nil
    (p (first l))
    (lazy-seq (cons v (collapse (drop-while p l) p v)))
    :otherwise
    (lazy-seq (cons (first l) (collapse (rest l) p v)))))

l是列表,p是谓词,v是替换与谓词匹配的子序列的值。

如果你以牺牲可读性为代价追求纯粹的速度,你可以这样做:

(defn collapse-normand2
  ([l p v]
    (lazy-seq (collapse-normand2 (seq l) p v nil)))
  ([l p v _]
    (when l
      (lazy-seq
        (let [f (first l)
              r (next l)]
          (if (p f)
            (cons v (collapse-normand2 r p v nil nil))
            (cons f (collapse-normand2 r p v nil)))))))
  ([l p v _ _]
     (when l
       (lazy-seq
         (let [f (first l)
               r (next l)]
           (if (p f)
             (collapse-normand2 r p v nil nil)
             (collapse-normand2 r p v nil)))))))

可能有一种方法可以让它更具可读性。它在所有4项测试中表现都很好。

答案 4 :(得分:0)

clojure中的字符串是Java字符串,因此您无法懒惰地创建字符串(意味着,您需要使用所有输入来构建字符串)。

你可以懒惰地创建一个字符序列:

USER> (remove #(Character/isWhitespace %) "\t    a\r          s\td  \t \r \n         f \r\n")
(\a \s \d \f)

你可以在这里使用字符串或seq作为输入,因为删除了最后一个参数的seq。