2010-07-21 13 views
9

Ho progettato una funzione per calcolare la media di un elenco. Anche se funziona bene, ma penso che potrebbe non essere la soluzione migliore perché richiede due funzioni piuttosto che una. È possibile fare questo lavoro con una sola funzione ricorsiva?Calcolare in modo efficiente la media di un elenco in 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

È bene tenere presente che qualsiasi soluzione a questo problema che equivale a una semplice divisione produrrà NaN per la lista vuota. Non necessariamente un problema, solo qualcosa che pensavo valesse la pena di notare. – Chuck

+0

possibile duplicazione di [Ricorsione a pigrizia e coda in Haskell, perché questo si blocca?] (Http://stackoverflow.com/questions/1618838/laziness-and-tail-recursion-in-haskell-why-is-this-crashing) –

+0

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

risposta

3

Mentre io non sono sicuro se sia o non sarebbe 'migliore' di scriverlo in una funzione, può essere fatto come segue:

Se si conosce la lunghezza (consente di chiamare 'n 'qui) in anticipo è facile - puoi calcolare quanto ogni valore' aggiunge 'alla media; questo sarà valore/lunghezza. Poiché avg (x1, x2, x3) = somma (x1, x2, x3)/lunghezza = (x1 + x2 + x3)/3 = x1/3 + x2/3 + x2/3

Se non si Sappiamo la lunghezza in anticipo, è un po 'più complicato:

diciamo che usiamo la lista {x1, x2, x3} senza conoscere il suo n = 3.

prima iterazione sarebbe solo x1 (poiché si assume la sua unica n = 1) seconda iterazione aggiungerebbe x2/2 e dividere il media esistente 2 così ora abbiamo x1/2 + x2/2

dopo la terza iterazione abbiamo n = 3 e vorremmo avere x1/3 + x2/3 + x3/3 ma abbiamo x1/2 + x2/2

quindi dovremmo moltiplicare per (n- 1) e dividere per n per ottenere x1/3 + x2/3 e che è sufficiente aggiungere il valore corrente (x3) diviso per n per finire con x1/3 + x2/3 + x3/3

Generalmente :

dato una media (media aritmetica - avg) per n-1 elementi, se si desidera aggiungere un elemento (newval) alla media l'equazione sarà:

avg * (n-1)/n + newval/n. L'equazione può essere dimostrata matematicamente usando l'induzione.

Spero che questo aiuti.

* nota questa soluzione è meno efficiente di sommare semplicemente le variabili e dividendo per la lunghezza totale come fai nel tuo esempio.

9

La soluzione è buona, l'utilizzo di due funzioni non è peggio di uno. Tuttavia, è possibile inserire la funzione ricorsiva della coda in una clausola where.

Ma se si vuole fare in una sola riga:

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

perché foldr e non foldl? sembra molto più adatto a me. – Axman6

+0

foldl, foldl 'o foldr possono essere usati qui come devi attraversare comunque l'intera lista (era quello che ho scelto) ... Penso che se le prestazioni contino foldl' può essere usato qui – Kru

+0

grazie mille, ho provato molto a lungo oggi per ottenere questo – user2664856

4

Quando ho visto la tua domanda, ho subito pensato "che si desidera un fold lì!"

E infatti, a similar question è stato chiesto prima su StackOverflow, e this answer ha una soluzione molto performante, che è possibile testare in un ambiente interattivo come 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

Per coloro che sono curiosi di sapere ciò glowcoder e di approccio di Assaf sarebbe simile in Haskell, ecco una traduzione:

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 

in questo modo assicura che ogni passo ha la "media finora" correttamente calcolato, ma lo fa al costo di una che il mazzo di ridondanti moltiplicando/dividendo per lunghezze, e calcoli di lunghezza molto inefficienti ad ogni passo. Haskeller non l'avrebbe scritto in questo modo.

Un modo solo leggermente migliore è:

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) 

Questo evita numerosi calcolo lunghezza. Ma richiede una funzione di supporto, che è esattamente ciò che il poster originale sta cercando di evitare. E richiede ancora un sacco di termini di cancellazione.

Per evitare la cancellazione di fuori di lunghezze, possiamo solo costruiamo la somma e la lunghezza e dividere alla fine:

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) 

e questo può essere molto più succintamente scritto come un 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) 

che può essere ulteriormente semplificato come per i post sopra.

Fold è davvero la strada da percorrere qui.

7

Circa il meglio che puoi fare è 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)) 

Fonde ad un ciclo ottimale nel nucleo (il migliore Haskell si potrebbe scrivere):

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

E la seguente assemblea:

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 

Basato su Data.Vector. Ad esempio,

$ 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 

Vedi le implementazioni efficienti in the statistics package.

0

Per dare seguito alla risposta di Don's 2010, su GHC 8.0.2 possiamo fare molto meglio. Per prima cosa proviamo la sua versione.

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) 

Questo ci dà

[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 

Tuttavia il codice è semplice: idealmente non ci dovrebbe essere bisogno di vettore: il codice ottimale dovrebbe essere possibile dal proprio inlining della generazione lista. Fortunatamente GHC può farlo per noi [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) 

Questo ci dà:

[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]: Notate come ho dovuto mappare fromIntegral: senza questa, GHC non riesce ad eliminare [Double] e la soluzione è molto più lento. Questo è un po 'triste: non capisco perché GHC non riesce a inline/decide che non è necessario senza questo. Se hai una vera raccolta di frazionari, allora questo trucco non funzionerà per te e il vettore potrebbe essere ancora necessario.

+0

Anche una nota interessante: se lavoriamo su '[Int]' e usiamo '-fllvm', in questo caso siamo in grado di ottenere praticamente una risposta a tempo costante. –

Problemi correlati