【问题标题】:Split vector at max value in Clojure -- better way?在 Clojure 中以最大值拆分向量——更好的方法?
【发布时间】:2015-11-27 06:53:23
【问题描述】:

新手问题:

如何在其中包含最大值的第一个实例处拆分数字向量?

所以,从这个[1 2 3 4 5 4 3 2 1],得到[1 2 3 4 5] [4 3 2 1]

我的做法似乎过于复杂:

(def up5 [1 2 3 4 5 4 3 2 1])
(split-at (inc (.indexOf up5 (apply max up5))) up5) ; => [1 2 3 4 5] [4 3 2 1]

这看起来有点尴尬吗?例如使用定义的向量三次。而我们需要使用Java来获取索引吗?

什么是更好、更惯用或更高效的方式?

谢谢。

【问题讨论】:

  • 使用reduce-kv 可以提高性能,以避免.indexOf 调用。但它比你的版本还要长。
  • 输入总是一个向量吗?您是否要求结果为 seq/vector/whatever?
  • 我认为这无关紧要。无论如何,除了试图改进之外,我没有将这段代码用于任何目的......
  • @Mallory-Erik 我太关心 performant 部分,显然你不是在寻找死性能。 :D tnoda 的解决方案已经足够惯用了。
  • @Andre reduce-kv.indexOf 快吗?

标签: clojure


【解决方案1】:

替代变体(只是为了好玩):

  • 您生成具有拆分位置(项目的索​​引 + 1)和项目本身的元组序列
  • 使用max-key找到最大项的元组
  • 在所需的索引处拆分您的集合(元组中的第一项)

    (defn split-at-max [items]
       (->> items
            (map vector (rest (range)))
            (apply max-key second)
            first
            (#(split-at % items))))
    
    user> (split-at-max [-1 20 3 4 1 3 5 101 4 2 6 4])
    [(-1 20 3 4 1 3 5 101) (4 2 6 4)]
    

此外,您可以轻松修改它以使用任意标准来估计值。

(defn split-at-max [items & {identity-fn :by :or {identity-fn identity}}]
  (->> items
       (map vector (rest (range)))
       (apply max-key (comp identity-fn second))
       first
       (#(split-at % items))))

最大身份:

user> (split-at-max [-1 20 3 4 1 3 5 101 4 2 6 4])
[(-1 20 3 4 1 3 5 101) (4 2 6 4)]

最大尺寸:

user> (split-at-max ["i" "wanna" "rock'n'roll" "all" "night" 
                     "and"  "party" "every" "day"] 
                    :by count)
[("i" "wanna" "rock'n'roll") ("all" "night" "and" "party" "every" "day")]

或通过一些外部值,例如:

user> (split-at-max [:a :b :c :d] :by {:a 0 :b 121 :c 2 :d -100})
[(:a :b) (:c :d)]

所以对我来说,它似乎更实用(而且对于那种更“clojure 方式”),虽然可能不是最有成效的。

【讨论】:

  • 相当投入,但真的很好!
  • 嗯,这就是为什么我说这很有趣。其他解决方案肯定更好
【解决方案2】:

如果顺序不重要,你可以使用这个

(def up5 [1 2 3 4 5 4 3 2 1 0])
(def up5max (apply max up5)

(->> up5 
     reverse 
     (split-with (partial > up5max)) 
     (map reverse))

#=> ((4 3 2 1 0) (1 2 3 4 5))

【讨论】:

    【解决方案3】:

    如果性能很重要,我会这样做:

    (defn vec-split-at [idx v]
      (if (empty? v)
        [[] []]
        [(subvec v 0 idx) (subvec v idx)]))
    
    (defn split-at-max [xs]
      (let [m-el (reduce-kv
                   (fn [max k v]
                     (if (<= v (second max))
                       max
                       [k v])) [0 (first xs)] xs)]
        (if (vector? xs)
          (vec-split-at (-> m-el first inc) xs)
          (split-at (-> m-el first inc) xs))))
    
    (split-at-max [1 10 10 1])
    

    应该是N + C 向量比较。其中C相对较小。

    【讨论】:

    • 这个只兼容矢量或地图,因为使用reduce-kv
    • 为什么是(if (vector? xs) ...)?你被告知xs 是一个向量。此外,xs 必须是具有位置索引的关联集合,reduce-kvsplit-at 才能工作。此外,您可以将if 向下推:((if vector? xs vec-split-at split-at) (-&gt; m-el first inc) xs),但也许我们不习惯在运算符位置读取条件。仍然是最好的。
    • reduce-kv 调用中似乎存在一些缺陷:&lt; 应该是&lt;= 以坚持最大值的第一个实例,作为问题要求; 1 应该是 0 以匹配 first;最后的xs 也可能是(rest xs),以避免两次查看第一个元素。
    • Thumbnail:您对所有 cmets 的看法都是正确的!我的版本当然可以改进。我很晚才意识到reduce-kv 是如此有限。
    • 我冒昧地进行了前两个更正。第三个错误:(rest xs) 返回一个序列。 (subvec xs 1) 应该可以工作,但不能。 reduce-kv 中的错误?
    【解决方案4】:
    (defn split-at-max [v]
      (when (seq v)
        (let [m (apply max v)
              point (inc (count (reduce (fn [a b] (if (> m b) (conj a b) 
                                                  (reduced a))) [] v)))]
          ((juxt #(take point %) #(drop point %)) v))))
    
    (split-at-max [1 2 9 2 -7  33 3 4 53 1 22 4 -44 444 3 2 3 0 -21])
    ;;=> [(1 2 9 2 -7 33 3 4 53 1 22 4 -44 444) (3 2 3 0 -21)]
    (split-at-max [])
    ;;=> nil
    (split-at-max [26 27 28 29 30 31 32 33])
    ;;=> [(26 27 28 29 30 31 32 33) ()]
    (split-at-max [33 32 31 30 29 28 27 26])
    ;;=> [(33) (32 31 30 29 28 27 26)]
    ;; works also with sets and lists:
    (split-at-max '(1 2 9 2 -7  33 3 4 53 1 22 4 -44 444 3 2 3 0 -21))
    ;;=> [(1 2 9 2 -7 33 3 4 53 1 22 4 -44 444) (3 2 3 0 -21)]
    (split-at-max '())
    ;;=> nil
    (split-at-max (hash-set))
    ;;=> nil
    (split-at-max (sorted-set))
    ;;=> nil
    (split-at-max (sorted-set 1 2 9 2 -7 33 3 4 53 1 22 4 -44 444 3 2 3 0 -21))
    ;;=> [(-44 -21 -7 0 1 2 3 4 9 22 33 53 444) ()]
    (split-at-max (hash-set 1 2 9 2 -7 33 3 4 53 1 22 4 -44 444 3 2 3 0 -21))
    ;;=> [(0 1 4 -21 33 22 -44 3 2 444) (-7 9 53)]
    

    使用split-with 在最大点拆分的另一种类似方式(如果有机会有空集合,还需要先在输入上执行seq):

    (let [v [1 2 9 2 -7 33 3 4 53 1 22 4 -44 444 3 2 3 0 -21]
          m (apply max v)]
      ((juxt #(concat (first %) [(first (second %))]) #(rest (second %)))
       (split-with (partial > m) v)))
    ;;=> [(1 2 9 2 -7 33 3 4 53 1 22 4 -44 444) (3 2 3 0 -21)]
    

    【讨论】:

    • 如果最大元素是第一个或最后一个,则您的第一个版本会失败。空向量也失败
    • 非常感谢您指出这一点,我已使用reduce 更改它以找到最大点而不是partition-by
    【解决方案5】:

    首先,鉴于.indexOf 列在Clojure cheatsheet 中,我认为使用它是惯用的。

    这里还有两种选择:

    这个类似于tnoda的第二种方案:

    (let [[a b c] (partition-by #(< % (apply max up5) up5)]
      [(concat a b) c])
    ;=> [(1 2 3 4 5) (4 3 2 1)]
    

    下一个看起来更复杂,但它在一个方面更优雅:它延迟了&lt;的效果,以便包含=项目,因此无需使用conjconcat之后将= 项重新粘贴到第一个序列中:

    (let [the-max (apply max up5)]
      (loop [the-start []
             the-rest up5
             continue? true]
        (if continue?
          (let [this-one (first the-rest)]
            (recur (conj the-start this-one)
                   (rest the-rest)
                   (< this-one the-max)))
          [the-start the-rest])))
    ;=> [[1 2 3 4 5] (4 3 2 1)]
    

    结果的第二个元素是clojure.lang.PersistentVector$ChunkedSeq,顺便说一句。在大多数情况下,序列的类型无关紧要,但如果你真的想要一个向量,你可以将vec 应用于它。我的第一个示例的结果也是如此。

    【讨论】:

    • 第一个版本意外二次,实在不应该用。
    • 谢谢@Andre。太糟糕了。它是如此简单,尽管它显然做了很多不必要的工作。也许对于小序列......
    【解决方案6】:

    如果您不想将 Java 方法带入 Clojure 世界,可以将 indexOf() 方法替换为 counttake-while 的组合。

    user> (def up5 [1 2 3 4 5 4 3 2 1])
    #<Var@20c4449f: [1 2 3 4 5 4 3 2 1]>
    
    user> (split-at (inc (count (take-while #(< % (apply max up5)) up5))) up5)
    [(1 2 3 4 5) (4 3 2 1)]
    

    但是,我更喜欢以下解决方案而不是前一个解决方案,尽管这比基于索引的解决方案要长。

    user> (let [x (apply max up5)
                [lhs rhs] (split-with #(< % x) up5)]
            [(conj (vec lhs) (first rhs)) (vec (next rhs))])
    [[1 2 3 4 5] [4 3 2 1]]
    

    【讨论】:

    • 坏主意!每次调用 take-while/split-with 的谓词时都会调用(apply max up5)。这意味着巨大的开销。最好先找到最大值,然后在谓词中使用它。
    • 感谢您发现我的解决方案中的性能问题。我修改了答案。
    • @tnoda 我认为问题仍然存在于您的第一个示例中,apply max up5 应该在每次take-while 想要使用集合的下一个元素检查其结果时进行评估(如果我不正确请纠正我...)。
    【解决方案7】:

    我开始了

    (defn split-at-max [v]
      (let [m (apply max v)
            n (count (take-while #(> m %) v))]
        (split-at (inc n) v)))
    

    这很笨拙。我应该使用split-with 而不是split-at,避免计算n。但是,我们可以修改它以在整个过程中使用向量:

    (defn split-at-max [v]
      (let [m (apply max v)
            n (loop [i 0]
                (if (= (v i) m) i (recur (inc i))))
            n (inc n)]
        [(subvec v 0 n) (subvec v n)]))
    

    这避免了实现拆分序列,因此使用起来更快。

    loop 查找最大元素的第一个匹配项。从@Mars 得到提示,我们可以改用Java ArrayListindexOf 方法:

    (defn split-at-max [v]
      (let [m (apply max v)
            n (inc (.indexOf v m))]
        [(subvec v 0 n) (subvec v n)]))
    

    这是快速、简洁和清晰的。

    【讨论】:

      猜你喜欢
      • 2012-03-28
      • 2017-04-01
      • 1970-01-01
      • 1970-01-01
      • 2014-02-18
      • 1970-01-01
      • 2019-11-18
      • 2017-07-18
      • 1970-01-01
      相关资源
      最近更新 更多