Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Split vector at max value in Clojure -- better way?

Tags:

clojure

Newbie question:

How to split a vector of numbers at and including the first instance of the maximum value in it?

So, from this [1 2 3 4 5 4 3 2 1], get [1 2 3 4 5] [4 3 2 1].

The way I'm doing it seems overly complex:

(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]

Does that seem a little awkward? For example using the defined vector three times. And do we need to use Java to get the index?

What would be a better, more idiomatic, or more performant way?

Thanks.

like image 924
Mallory-Erik Avatar asked Nov 27 '15 06:11

Mallory-Erik


3 Answers

alternative variant (just for fun):

  • you generate the sequence of tuples with split-position (item's index + 1) and item itself
  • find the tuple with max item using max-key
  • split your collection at the needed index (first item in a tuple)

    (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)]
    

moreover you could easily modify it to be used with an arbitrary criteria for estimation the value.

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

max by identity:

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

max by size:

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")]

or by some external value for example:

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

so to me it seems more functional (and for that more "clojure way"), though probably not the most productive.

like image 64
leetwinski Avatar answered Oct 24 '22 00:10

leetwinski


If order who goes first doesn't matter you could use this

(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))
like image 43
fl00r Avatar answered Oct 24 '22 00:10

fl00r


If performance is important I'd do it like this:

(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])

It should be N + C comparisons for vectors. Where C is relatively small.

like image 44
ClojureMostly Avatar answered Oct 24 '22 00:10

ClojureMostly