2010-07-21 8 views
9

J'ai conçu une fonction pour calculer la moyenne d'une liste. Bien que cela fonctionne bien, mais je pense que ce n'est peut-être pas la meilleure solution car il faut deux fonctions plutôt qu'une. Est-il possible de faire ce travail avec une seule fonction récursive?Calcul de la moyenne d'une liste de manière efficace dans 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

Il est bon de garder à l'esprit que toute solution à ce problème qui équivaut à une division simple produira NaN pour la liste vide. Pas nécessairement un problème, juste quelque chose que je pensais mériter d'être noté. – Chuck

+0

duplication possible de [La paresse et la récursivité queue dans Haskell, pourquoi est-ce s'écraser?] (Http://stackoverflow.com/questions/1618838/laziness-and-tail-recursion-in-haskell-why-is-this-crashing) –

+0

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

Répondre

3

Bien que je ne suis pas sûr si oui ou non il serait « meilleur » pour l'écrire dans une fonction, il peut se faire comme suit:

Si vous connaissez la longueur (permet de l'appeler « n 'ici) à l'avance c'est facile - vous pouvez calculer combien chaque valeur «ajoute» à la moyenne; cela va être valeur/longueur. Depuis avg (x1, x2, x3) = somme (x1, x2, x3)/longueur = (x1 + x2 + x3)/3 = x1/3 + x2/3 + x2/3

Si vous n '' Je connais la longueur à l'avance, c'est un peu plus compliqué:

disons que nous utilisons la liste {x1, x2, x3} sans connaître son n = 3.

première itération serait juste x1 (puisque nous supposons son seul n = 1) deuxième itération ajouterait x2/2 et diviser la moyenne actuelle de 2 maintenant nous avons x1/2 + x2/2

après la troisième itération nous avons n = 3 et nous voudrions avoir x1/3 + x2/3 + x3/3 mais nous avons x1/2 + x2/2

donc nous aurions besoin de multiplier par (n- 1) et diviser par n pour obtenir x1/3 + x2/3 et à cela nous ajoutons simplement la valeur courante (x3) divisée par n pour finir avec x1/3 + x2/3 + x3/3

Généralement :

donné en moyenne (moyenne arithmétique - moyenne) pour n-1 éléments, si vous souhaitez ajouter un élément (newval) à la moyenne de votre équation sera:

Avg * (n-1)/n + newval/n. L'équation peut être prouvée mathématiquement en utilisant l'induction.

Espérons que cela aide.

* Notez que cette solution est moins efficace que simplement additionner les variables et diviser par la longueur totale comme vous le faites dans votre exemple.

9

Votre solution est bonne, en utilisant deux fonctions n'est pas pire qu'une. Vous pouvez néanmoins placer la fonction récursive de queue dans une clause where.

Mais si vous voulez le faire en une ligne:

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

pourquoi foldr et pas foldl? semble un bien meilleur ajustement pour moi. – Axman6

+0

foldl, foldl 'ou foldr peut être utilisé ici comme vous devez traverser toute la liste de toute façon (c'est celui que j'ai choisi) ... je pense que si la performance compte foldl' peut être utilisé ici – Kru

+0

merci beaucoup, j'ai essayé très longtemps aujourd'hui pour y parvenir – user2664856

4

Quand j'ai vu votre question, je tout de suite pensé « vous voulez un y fold!"

Et bien sûr, a similar question a été posée sur StackOverflow et this answer a une solution très performante, que vous pouvez tester dans un environnement interactif comme 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

Pour ceux qui sont curieux de savoir quoi glowcoder et l'approche de Assaf ressemblerait à Haskell, voici une traduction:

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 cette façon, assure que chaque étape a la « moyenne jusqu'à présent » correctement calculé, mais le fait au prix d'un qui le tas de redondants multipliant/divisant par des longueurs, et des calculs de longueur très inefficaces à chaque étape. Aucun Haskeller chevronné ne l'écrirait de cette façon.

Une façon peu mieux est:

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) 

On évite ainsi répété calcul de la longueur. Mais cela nécessite une fonction d'assistance, ce qui est précisément ce que l'affiche originale tente d'éviter. Et il faut encore tout un tas d'annulations hors limites.

Pour éviter l'annulation des longueurs, nous pouvons simplement construire la somme et la longueur et diviser à la fin:

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) 

Et cela peut être beaucoup plus succinctement écrit comme 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) 

qui peut être encore simplifié selon les messages ci-dessus.

Fold est vraiment le chemin à parcourir.

7

A propos du mieux que vous pouvez faire est 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)) 

Il fusionne avec une boucle optimale de base (le meilleur Haskell vous pourriez écrire):

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

Et l'assemblée suivante:

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 

Basé sur Data.Vector. Par exemple,

$ 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 

Voir les implémentations efficaces the statistics package.

0

Pour faire suite à la réponse de Don en 2010, sur GHC 8.0.2, nous pouvons faire beaucoup mieux. Essayons d'abord sa version.

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) 

Cela nous donne

[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 

Cependant, le code est simple: il ne devrait idéalement pas besoin de vecteur: code optimal devrait être possible de simplement inline la génération de liste. Heureusement, GHC peut le faire pour nous [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) 

Cela nous donne:

[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]: Remarquez comment je devais carte fromIntegral: sans cela, GHC ne parvient pas à éliminer [Double] et la solution est beaucoup plus lente. C'est un peu triste: je ne comprends pas pourquoi GHC ne parvient pas à intégrer/décide qu'il n'a pas besoin de cela sans cela. Si vous avez une véritable collection de fractions, alors ce hack ne fonctionnera pas pour vous et le vecteur peut toujours être nécessaire.

+0

Egalement une note intéressante: si nous travaillons sur '[Int]' et que nous utilisons '-fllvm', nous pouvons obtenir une réponse assez constante dans ce cas. –

Questions connexes