Вычисления среднего из списка эффективно в Haskell

Я разработал функцию для вычислений среднего из списка. Хотя это хорошо работает, но я думаю, что это не может быть лучшее решение из-за него, берет две функции, а не один. Действительно ли возможно сделать это задание, сделанное только с одной рекурсивной функцией?

calcMeanList (x:xs) = doCalcMeanList (x:xs) 0 0

doCalcMeanList (x:xs) sum length =  doCalcMeanList xs (sum+x) (length+1)
doCalcMeanList [] sum length = sum/length
11
задан Don Stewart 20 April 2011 в 22:46
поделиться

5 ответов

Ваше решение хорошее, использование двух функций не хуже, чем одной. Тем не менее, вы могли бы поместить хвостовую рекурсивную функцию в предложение where.

Но если вы хотите сделать это в одной строке:

calcMeanList = uncurry (/) . foldr (\e (s,c) -> (e+s,c+1)) (0,0)
10
ответ дан 3 December 2019 в 02:29
поделиться

Лучшее, что вы можете сделать, это эту версию :

import qualified Data.Vector.Unboxed as U

data Pair = Pair {-# UNPACK #-}!Int {-# UNPACK #-}!Double

mean :: U.Vector Double -> Double
mean xs = s / fromIntegral n
  where
    Pair n s       = U.foldl' k (Pair 0 0) xs
    k (Pair n s) x = Pair (n+1) (s+x)

main = print (mean $ U.enumFromN 1 (10^7))

Она сливается с оптимальным циклом в Core (лучший Haskell, который вы могли написать):

main_$s$wfoldlM'_loop :: Int#
                              -> Double#
                              -> Double#
                              -> Int#
                              -> (# Int#, Double# #)    
main_$s$wfoldlM'_loop =
  \ (sc_s1nH :: Int#)
    (sc1_s1nI :: Double#)
    (sc2_s1nJ :: Double#)
    (sc3_s1nK :: Int#) ->
    case ># sc_s1nH 0 of _ {
      False -> (# sc3_s1nK, sc2_s1nJ #);
      True ->
        main_$s$wfoldlM'_loop
          (-# sc_s1nH 1)
          (+## sc1_s1nI 1.0)
          (+## sc2_s1nJ sc1_s1nI)
          (+# sc3_s1nK 1)
    }

И следующую сборку:

Main_mainzuzdszdwfoldlMzqzuloop_info:
.Lc1pN:
        testq %r14,%r14
        jg .Lc1pQ
        movq %rsi,%rbx
        movsd %xmm6,%xmm5
        jmp *(%rbp)
.Lc1pQ:
        leaq 1(%rsi),%rax
        movsd %xmm6,%xmm0
        addsd %xmm5,%xmm0
        movsd %xmm5,%xmm7
        addsd .Ln1pS(%rip),%xmm7
        decq %r14
        movsd %xmm7,%xmm5
        movsd %xmm0,%xmm6
        movq %rax,%rsi
        jmp Main_mainzuzdszdwfoldlMzqzuloop_info

По материалам Data.Vector. Например,

$ ghc -Odph --make A.hs -fforce-recomp
[1 of 1] Compiling Main             ( A.hs, A.o )
Linking A ...
$ time ./A
5000000.5
./A  0.04s user 0.00s system 93% cpu 0.046 total

См. Эффективные реализации в статистическом пакете .

10
ответ дан 3 December 2019 в 02:29
поделиться

Когда я увидел ваш вопрос, я сразу подумал: «Вы хотите фолд там!»

И, конечно же, аналогичный вопрос уже задавался ранее на StackOverflow, и этот ответ имеет очень производительное решение, которое вы можете протестировать в интерактивной среде, такой как GHCi:

import Data.List

let avg l = let (t,n) = foldl' (\(b,c) a -> (a+b,c+1)) (0,0) l 
            in realToFrac(t)/realToFrac(n)

avg ([1,2,3,4]::[Int])
2.5
avg ([1,2,3,4]::[Double])
2.5
5
ответ дан 3 December 2019 в 02:29
поделиться

Для тех, кому интересно узнать, как будет выглядеть подход светового кодера и Ассафа в Haskell, вот один перевод:

avg [] = 0
avg x@(t:ts) = let xlen = toRational $ length x
                   tslen = toRational $ length ts
                   prevAvg = avg ts
               in (toRational t) / xlen + prevAvg * tslen / xlen

Этот способ гарантирует, что на каждом шаге будет правильно вычислено «среднее на данный момент», но это будет происходить за счет целой кучи избыточного умножения / деления на длину и очень неэффективных вычислений длины на каждом шаге. Ни один опытный Хаскеллер не стал бы так писать.

Только немного лучший способ:

avg2 [] = 0
avg2 x = fst $ avg_ x
    where 
      avg_ [] = (toRational 0, toRational 0)
      avg_ (t:ts) = let
           (prevAvg, prevLen) = avg_ ts
           curLen = prevLen + 1
           curAvg = (toRational t) / curLen + prevAvg * prevLen / curLen
        in (curAvg, curLen)

Это позволяет избежать повторного вычисления длины. Но для этого требуется вспомогательная функция, а именно ее пытается избежать исходный постер. И это по-прежнему требует отмены слишком длинных сроков.

Чтобы избежать сокращения длин, мы можем просто построить сумму и длину и разделить в конце:

avg3 [] = 0
avg3 x = (toRational total) / (toRational len)
    where 
      (total, len) = avg_ x
      avg_ [] = (0, 0)
      avg_ (t:ts) = let 
          (prevSum, prevLen) = avg_ ts
       in (prevSum + t, prevLen + 1)

И это можно гораздо более кратко записать в виде foldr:

avg4 [] = 0
avg4 x = (toRational total) / (toRational len)
    where
      (total, len) = foldr avg_ (0,0) x
      avg_ t (prevSum, prevLen) = (prevSum + t, prevLen + 1)

, который можно упростить как согласно сообщениям выше.

Фолд - действительно правильный выбор.

3
ответ дан 3 December 2019 в 02:29
поделиться

Хотя я не уверен, было бы «лучше» записать его в одной функции, это можно сделать следующим образом:

Если вы знаете длину (давайте назовем ее здесь 'n') заранее это просто - вы можете подсчитать, сколько каждое значение «прибавляет» к среднему; это будет значение / длина. Поскольку avg (x1, x2, x3) = sum (x1, x2, x3) / length = (x1 + x2 + x3) / 3 = x1 / 3 + x2 / 3 + x2 / 3

Если вы не знаю заранее длину, это немного сложнее:

допустим, мы используем список {x1, x2, x3} , не зная его n = 3.

первая итерация будет просто x1 (поскольку мы предполагаем, что ее только n = 1) вторая итерация добавит x2 / 2 и разделит существующее среднее на 2, так что теперь у нас есть x1 / 2 + x2 / 2

, после третьей итерации у нас n = 3, и мы хотели бы чтобы иметь x1 / 3 + x2 / 3 + x3 / 3 , но у нас есть x1 / 2 + x2 / 2

, поэтому нам нужно будет умножить на (n-1) и разделим на n , чтобы получить x1 / 3 + x2 / 3 , и к этому мы просто добавляем текущее значение (x3), разделенное на n, чтобы получить x1 / 3 + x2 / 3 + x3 / 3

Обычно:

с учетом среднего (среднее арифметическое - avg) для n-1 элементов, если вы хотите добавить один элемент (newval) к среднему значению, ваше уравнение будет быть:

средн * (n-1) / n + newval / n . Уравнение может быть доказано математически с помощью индукции.

Надеюсь, это поможет.

* обратите внимание, что это решение менее эффективно, чем простое суммирование переменных и деление на общую длину, как в вашем примере.

3
ответ дан 3 December 2019 в 02:29
поделиться
Другие вопросы по тегам:

Похожие вопросы: