【问题标题】:Best way to lazily collapse multiple contiguous items of a sequence into a single item将序列的多个连续项目延迟折叠为单个项目的最佳方法
【发布时间】:2011-07-18 04:16:33
【问题描述】:

[注意:标题和文本经过大量编辑,以更清楚地表明我不是特别关注字符串,而是关注一般序列,以及相同的惰性处理]

以字符序列/字符串为例,说我想把字符串转成like

"\t a\r      s\td \t \r \n          f \r\n"

进入

“a s d f”

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

我想出了以下 partition-by/mapcat 组合,但想知道是否有更简单或更好的方法(可读性、性能等)来完成同样的事情。 p>

(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 更好的替代方案,如果可以针对“字符串”特殊情况进行优化,则没有那么多。

更新 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. 创建惰性序列 1M 次
  2. 创建惰性序列并取第一项 1M 次
  3. 创建惰性序列并取第二项 1M 次
  4. 创建惰性序列并取最后一项(即完全实现惰性序列)1M 次

测试 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"

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

【问题讨论】:

    标签: clojure


    【解决方案1】:

    这是我目前最快的解决方案:(与 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 "
    

    用于不同类型的序列:

    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 表示可读性,-1 表示花费的时间是 msmith 崩溃的两倍多,是我的惰性崩溃的三倍多,使用 "\t a\r s\td \t \ r \n f \r\n" 作为测试序列
    • 我会在 10 次中有 9 次权衡速度的可读性 :)
    • 取决于您想要实现的目标。如果您需要真正快速处理大量数据,您将优化这些点。在这些情况下,将执行时间缩短 2 或 3 倍意味着很多。但我要求“可读性,性能,任何东西”,所以这是一个答案。我基本上还在等待一个答案“统统统管”,它结合了可读性和性能。
    • 在我的解决方案中发现并修复了一个错误。现在慢了 20% 并且与您的初始版本相同......我将稍微研究一下性能。我很好奇时间都去哪儿了。
    • 注意通过第一项的真值检查列表是否为空。是的,它适用于字符列表,但如果 nil 或 false 在列表中,它就不起作用。在这些情况下,列表将被截断。
    【解决方案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 "
    

    它也适用于其他序列:

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

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

    【讨论】:

    • 如果我想做的就是你在你的例子中所做的,那么这就是完美的。但是,我的问题专门针对懒惰和字符序列。我使用字符串/字符序列/wsp 来创建一个简单的示例,但我真正想要的是序列上的通用函数,它将作为预定义项目集的一部分的项目折叠为另一个单个项目。将相应地更新问题以使其更清楚。
    • 我认为,我们可以使用reduce,稍后会尝试制作代码示例......
    • 我用reduce版本更新了答案...虽然我们可以尝试通过优化is-blank?函数等来使其更有效。
    • 嗯...你有(时间(dotimes [_ 500000] ...)它还是什么?如果我把我的代码变成和你一样的形式(函数参数等),我的是明显更快 - 除了更简单。顺便说一句,^Character 提示并没有给你买太多,除非你得到“由角色支付”:P
    • 但更重要的是reduce不是偷懒的。
    【解决方案3】:

    这是一个更简洁的完全惰性实现:

    <!-- 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)))))))
    

    【讨论】:

    • +1 更易于阅读,但在 "\t a\r s\td \t \r \n f \r\n" 上进行测试时,它比惰性折叠多 30% 的时间。
    【解决方案4】:

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

    (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 项测试中都表现出色。

    【讨论】:

    • 将近一年后,让我接受你的回答。它不是最快的,但可能是性能和简洁/可读性之间的良好折衷。
    【解决方案5】:

    clojure 中的字符串是 Java 字符串,因此您不能懒惰地创建字符串(也就是说,您需要消耗所有输入来构建字符串)。

    不过,您可以懒惰地创建字符序列:

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

    您可以在此处使用字符串或 seq 作为输入,因为 remove 在其最后一个参数上调用 seq。

    【讨论】:

    • 他不想完全删除空格,只需将几个空白字符实例替换为一个空格...
    猜你喜欢
    • 2019-08-27
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-02-07
    • 1970-01-01
    • 1970-01-01
    • 2023-03-04
    • 1970-01-01
    相关资源
    最近更新 更多