Calculating the Moving Average of a List

后端 未结 18 1176
悲哀的现实
悲哀的现实 2020-12-07 09:01

This weekend I decided to try my hand at some Scala and Clojure. I\'m proficient with object oriented programming, and so Scala was easy to pick up as a language, but wante

18条回答
  •  醉酒成梦
    2020-12-07 09:28

    I know Clojure better than Scala, so here goes. As I write this the other Clojure entry here is imperative; that's not really what you're after (and isn't idiomatic Clojure). The first algorithm that comes to my mind is repeatedly taking the requested number of elements from the sequence, dropping the first element, and recurring.

    The following works on any kind of sequence (vector or list, lazy or not) and gives a lazy sequence of averages---which could be helpful if you're working on a list of indefinite size. Note that it takes care of the base case by implicitly returning nil if there aren't enough elements in the list to consume.

    (defn moving-average [values period]
      (let [first (take period values)]
        (if (= (count first) period)
          (lazy-seq 
            (cons (/ (reduce + first) period)
                  (moving-average (rest values) period))))))
    

    Running this on your test data yields

    user> (moving-average '(2.0, 4.0, 7.0, 6.0, 3.0, 8.0, 12.0, 9.0, 4.0, 1.0) 4)
    (4.75 5.0 6.0 7.25 8.0 8.25 6.5)
    

    It doesn't give "0" for the first few elements in the sequence, though that could easily be handled (somewhat artificially).

    The easiest thing of all is to see the pattern and be able to bring to mind an available function that fits the bill. partition gives a lazy view of portions of a sequence, which we can then map over:

    (defn moving-average [values period]
      (map #(/ (reduce + %) period) (partition period 1 values))
    

    Someone asked for a tail recursive version; tail recursion vs. laziness is a bit of a tradeoff. When your job is building up a list then making your function tail recursive is usually pretty simple, and this is no exception---just build up the list as an argument to a subfunction. We'll accumulate to a vector instead of a list because otherwise the list will be built up backwards and will need to be reversed at the end.

    (defn moving-average [values period]
      (loop [values values, period period, acc []]
        (let [first (take period values)]
          (if (= (count first) period)
            (recur (rest values) period (conj acc (/ (reduce + first) period)))
            acc))))
    

    loop is a way to make an anonymous inner function (sort of like Scheme's named let); recur must be used in Clojure to eliminate tail calls. conj is a generalized cons, appending in the manner natural for the collection---the beginning of lists and the end of vectors.

提交回复
热议问题