2010-07-21 18 views
9

He diseñado una función para calcular el promedio de una lista. Aunque funciona bien, pero creo que puede no ser la mejor solución porque requiere dos funciones en lugar de una. ¿Es posible hacer este trabajo con una sola función recursiva?Calcular el promedio de una lista de manera eficiente en 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 
+1

Es bueno tener en cuenta que cualquier solución a este problema que equivalga a una división simple producirá NaN para la lista vacía. No necesariamente es un problema, solo algo que pensé que valía la pena mencionar. – Chuck

+0

posible duplicado de [La pereza y la recursividad de cola en Haskell, ¿por qué se cuelga?] (Http://stackoverflow.com/questions/1618838/laziness-and-tail-recursion-in-haskell-why-is-this-crashing) –

+0

Duplicado de http://stackoverflow.com/questions/1618838/laziness-and-tail-recursion-in-haskell-why-is-this-crashing/1618864#1618864 –

Respuesta

3

Aunque no estoy seguro de si es o no sería 'mejor' para escribirlo en una función, se puede hacer de la siguiente manera:

Si conoce la longitud (llamémosle 'n 'aquí') de antemano es fácil: puedes calcular cuánto 'suma' cada valor al promedio; ese va a ser valor/longitud. Desde avg (x1, x2, x3) = suma (x1, x2, x3)/longitud = (x1 + x2 + x3)/3 = x1/3 + x2/3 + x2/3

Si no lo hace t saber la longitud de antemano, es un poco más complicado:

digamos que usamos la lista {x1, x2, x3} sin saber su n = 3.

primera iteración sólo sería x1 (ya que se supone su única n = 1) segunda iteración añadiría x2/2 y dividir el promedio existente en un 2 por lo que ahora tenemos x1/2 + x2/2

después de la tercera iteración tenemos n = 3 y quisiéramos tener x1/3 + x2/3 + x3/3 pero tenemos x1/2 + x2/2

por lo que tendríamos que multiplicar por (n- 1) y divida por n para obtener x1/3 + x2/3 y simplemente agreguemos el valor actual (x3) dividido por n para terminar con x1/3 + x2/3 + x3/3

Generalmente :

da un promedio (media aritmética - Med) para n-1 elementos, si desea agregar un elemento (valorNuevo) con el promedio de la ecuación será:

promedio * (n-1)/n + newval/n. La ecuación puede ser probada matemáticamente usando la inducción.

Espero que esto ayude.

* tenga en cuenta que esta solución es menos eficiente que simplemente sumar las variables y dividir por la longitud total como lo hace en su ejemplo.

9

Su solución es buena, utilizando dos funciones no es peor que una. Aún así, puede poner la función recursiva de cola en una cláusula where.

Pero si quieres hacerlo en una sola línea:

calcMeanList = uncurry (/) . foldr (\e (s,c) -> (e+s,c+1)) (0,0) 
+0

¿por qué foldr y no foldl? me parece mucho mejor. – Axman6

+0

foldl, foldl 'o foldr se pueden usar aquí ya que debes atravesar la lista completa de todos modos (fue el que escogí) ... Creo que si el rendimiento importa foldl' puede usarse aquí – Kru

+0

muchas gracias, probé mucho hoy para lograr esto – user2664856

4

Cuando vi tu pregunta, inmediatamente pensé "desea una fold allí!"

y, efectivamente, a similar question se ha hecho antes en StackOverflow, y this answer tiene una solución de buen calidad, que se puede probar en un entorno interactivo como 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 
3

Para aquellos que tienen curiosidad de saber lo glowcoder de y el enfoque de Assaf se vería en Haskell, aquí está una traducción:

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

de esta manera se asegura de que cada paso tiene el "promedio hasta ahora" calculado correctamente, pero lo hace a costa de un quién el grupo de multiplicación/división redundante por longitudes, y cálculos de longitud muy ineficientes en cada paso. Ningún Haskeller experimentado lo escribiría de esta manera.

Una manera sólo ligeramente mejor es:

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) 

Esto evita repitió cálculo de la longitud. Pero requiere una función auxiliar, que es precisamente lo que el cartel original está tratando de evitar. Y todavía requiere un montón de cancelación de términos de longitud.

Para evitar la cancelación de longitudes, podemos simplemente construimos la suma y la longitud y dividir al final:

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) 

y esto puede ser mucho más sucinta escrito como una 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) 

que se puede simplificar aún más según las publicaciones anteriores.

Doblar realmente es el camino a seguir aquí.

7

Acerca de lo mejor que puede hacer es this version:

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

Funde a un bucle óptimo en Core (la mejor Haskell se podría escribir):

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

Y el siguiente montaje:

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 

Basado en Data.Vector. Por ejemplo,

$ 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 

Ver las implementaciones eficientes en the statistics package.

0

Para dar seguimiento a la respuesta de Don de 2010, en GHC 8.0.2 podemos hacerlo mucho mejor. Primero probemos su versión.

module Main (main) where 

import System.CPUTime.Rdtsc (rdtsc) 
import Text.Printf (printf) 
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 :: IO() 
main = do 
    s <- rdtsc 
    let r = mean' (U.enumFromN 1 30000000) 
    e <- seq r rdtsc 
    print (e - s, r) 

Esto nos da

[nix-shell:/tmp]$ ghc -fforce-recomp -O2 MeanD.hs -o MeanD && ./MeanD +RTS -s 
[1 of 1] Compiling Main    (MeanD.hs, MeanD.o) 
Linking MeanD ... 
(372877482,1.50000005e7) 
    240,104,176 bytes allocated in the heap 
      6,832 bytes copied during GC 
      44,384 bytes maximum residency (1 sample(s)) 
      25,248 bytes maximum slop 
      230 MB total memory in use (0 MB lost due to fragmentation) 

            Tot time (elapsed) Avg pause Max pause 
    Gen 0   1 colls,  0 par 0.000s 0.000s  0.0000s 0.0000s 
    Gen 1   1 colls,  0 par 0.006s 0.006s  0.0062s 0.0062s 

    INIT time 0.000s ( 0.000s elapsed) 
    MUT  time 0.087s ( 0.087s elapsed) 
    GC  time 0.006s ( 0.006s elapsed) 
    EXIT time 0.006s ( 0.006s elapsed) 
    Total time 0.100s ( 0.099s elapsed) 

    %GC  time  6.2% (6.2% elapsed) 

    Alloc rate 2,761,447,559 bytes per MUT second 

    Productivity 93.8% of total user, 93.8% of total elapsed 

Sin embargo, el código es simple: lo ideal no debería haber ninguna necesidad de vector: código óptimo debería ser posible desde sólo inlining la generación de la lista. Afortunadamente, GHC puede hacer esto por nosotros [0].

module Main (main) where 

import System.CPUTime.Rdtsc (rdtsc) 
import Text.Printf (printf) 
import Data.List (foldl') 

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

mean' :: [Double] -> Double 
mean' xs = v/fromIntegral l 
    where 
    Pair l v = foldl' f (Pair 0 0) xs 
    f (Pair l' v') x = Pair (l' + 1) (v' + x) 

main :: IO() 
main = do 
    s <- rdtsc 
    let r = mean' $ fromIntegral <$> [1 :: Int .. 30000000] 
     -- This is slow! 
     -- r = mean' [1 .. 30000000] 
    e <- seq r rdtsc 
    print (e - s, r) 

Esto nos da:

[nix-shell:/tmp]$ ghc -fforce-recomp -O2 MeanD.hs -o MeanD && ./MeanD +RTS -s 
[1 of 1] Compiling Main    (MeanD.hs, MeanD.o) 
Linking MeanD ... 
(128434754,1.50000005e7) 
     104,064 bytes allocated in the heap 
      3,480 bytes copied during GC 
      44,384 bytes maximum residency (1 sample(s)) 
      17,056 bytes maximum slop 
       1 MB total memory in use (0 MB lost due to fragmentation) 

            Tot time (elapsed) Avg pause Max pause 
    Gen 0   0 colls,  0 par 0.000s 0.000s  0.0000s 0.0000s 
    Gen 1   1 colls,  0 par 0.000s 0.000s  0.0000s 0.0000s 

    INIT time 0.000s ( 0.000s elapsed) 
    MUT  time 0.032s ( 0.032s elapsed) 
    GC  time 0.000s ( 0.000s elapsed) 
    EXIT time 0.000s ( 0.000s elapsed) 
    Total time 0.033s ( 0.032s elapsed) 

    %GC  time  0.1% (0.1% elapsed) 

    Alloc rate 3,244,739 bytes per MUT second 

    Productivity 99.8% of total user, 99.8% of total elapsed 

[0]: Observe cómo tenía que trazar fromIntegral: sin esto, GHC no elimina [Double] y la solución es mucho más lento. Eso es algo triste: no entiendo por qué GHC no puede alinear/decide que no necesita hacerlo sin esto. Si tienes una colección genuina de fraccionarios, este truco no funcionará para ti y el vector puede ser necesario.

+0

También una nota interesante: si trabajamos sobre '[Int]' y usamos '-fllvm', en este caso podemos obtener una respuesta casi constante. –

Cuestiones relacionadas