52 votes

Pourquoi les listes de différences sont-elles plus efficaces que la concaténation régulière?

Je suis actuellement en train de travailler mon chemin à travers la Apprendre vous un haskell livre en ligne, et en sont venus à un chapitre où l'auteur explique que certains de la liste des enchaînements peuvent être ineffiecient: Par exemple

((((a ++ b) ++ c) ++ d) ++ e) ++ f

Est censé être inefficace. La solution de l'auteur en vient, c'est d'utiliser "la différence les listes" définis comme

newtype DiffList a = DiffList {getDiffList :: [a] -> [a] }

instance Monoid (DiffList a) where
    mempty = DiffList (\xs -> [] ++ xs)
    (DiffList f) `mappend` (DiffList g) = DiffList (\xs -> f (g xs))

J'ai du mal à comprendre pourquoi DiffList est informatiquement plus efficace qu'une simple concaténation dans certains cas. Quelqu'un pourrait-il m'expliquer en termes simples pourquoi l'exemple ci-dessus est donc inefficace, et de quelle manière les DiffList permet de résoudre ce problème?

89voto

Daniel Fischer Points 114146

Le problème dans

((((a ++ b) ++ c) ++ d) ++ e) ++ f

est le site de nidification. Les applications de l' (++) sont de gauche imbriqués, et qui est mauvais, droit de la nidification

a ++ (b ++ (c ++ (d ++ (e ++f))))

ne serait pas un problème. C'est parce qu' (++) est défini comme

[] ++ ys = ys
(x:xs) ++ ys = x : (xs ++ ys)

donc, pour trouver l'équation de l'utilisation, de la mise en œuvre doit plonger dans l'arborescence d'expression

             (++)
             /  \
          (++)   f
          /  \
       (++)   e
       /  \
    (++)   d
    /  \
 (++)   c
 /  \
a    b

jusqu'à ce qu'il détermine si l'opérande de gauche est vide ou pas. Si elle n'est pas vide, sa tête est prise et on fait barboter le sommet, mais la queue de l'opérande de gauche est restée intacte, de sorte que lorsque l'élément suivant de la concaténation est de rigueur, la même procédure recommence.

Lorsque les concaténations sont de droite imbriquées, l'opérande gauche de l' (++) est toujours au top, et la vérification de la vacuité/jaillissant de la tête sont en O(1).

Mais quand les concaténations sont de gauche imbriqués, n couches de profondeur, pour atteindre le premier élément, n des nœuds de l'arbre doit être parcouru pour chaque élément du résultat (en provenance de la première liste, n-1 pour ceux qui viennent de le deuxième, etc.).

Permettez-nous de considérer a = "hello" dans

hi = ((((a ++ b) ++ c) ++ d) ++ e) ++ f

et nous voulons les évaluer en take 5 hi. Alors d'abord, il doit être vérifié si l'

(((a ++ b) ++ c) ++ d) ++ e

est vide. Pour cela, il doit être vérifié si l'

((a ++ b) ++ c) ++ d

est vide. Pour cela, il doit être vérifié si l'

(a ++ b) ++ c

est vide. Pour cela, il doit être vérifié si l'

a ++ b

est vide. Pour cela, il doit être vérifié si l'

a

est vide. Ouf. Ce n'est pas le cas, nous pouvons faire émerger de nouveau, l'assemblage

a ++ b                             = 'h':("ello" ++ b)
(a ++ b) ++ c                      = 'h':(("ello" ++ b) ++ c)
((a ++ b) ++ c) ++ d               = 'h':((("ello" ++ b) ++ c) ++ d)
(((a ++ b) ++ c) ++ d) ++ e        = 'h':(((("ello" ++ b) ++ c) ++ d) ++ e)
((((a ++ b) ++ c) ++ d) ++ e) ++ f = 'h':((((("ello" ++ b) ++ c) ++ d) ++ e) ++ f)

et pour l' 'e', il faut le répéter, et pour l' 'l's trop...

Dessin d'une partie de l'arbre, le phénomène de propagation qui va comme ceci:

            (++)
            /  \
         (++)   c
         /  \
'h':"ello"   b

devient première

     (++)
     /  \
   (:)   c
  /   \
'h'   (++)
      /  \
 "ello"   b

et puis

      (:)
      / \
    'h' (++)
        /  \
     (++)   c
     /  \
"ello"   b

tout le chemin du retour vers le haut. La structure de l'arbre qui devient le droit de l'enfant de le top-niveau (:) enfin, est exactement la même que la structure de l'arbre d'origine, à moins que la gauche de la liste est vide, lorsque le

 (++)
 /  \
[]   b

les nœuds est effondré juste b.

Donc, si vous avez laissé imbriquées les concaténations de courtes listes, la concaténation devient quadratique parce que pour obtenir la tête de la concaténation est un O(imbrication de la profondeur) de l'opération. En général, la concaténation de gauche imbriquée

(...((a_d ++ a_{d-1}) ++ a_{d-2}) ...) ++ a_2) ++ a_1

est - O(sum [i * length a_i | i <- [1 .. d]]) d'évaluer pleinement.

Avec la différence, les listes (sans le newtype wrapper pour la simplicité de l'exposition), il n'est pas important de savoir si les compositions sont de gauche imbriquée

((((a ++) . (b ++)) . (c ++)) . (d ++)) . (e ++)

droite ou imbriqués. Une fois que vous avez parcouru le site de nidification pour atteindre l' (a ++)que (++) est hissé en haut de l'arborescence d'expression, afin d'obtenir à chaque élément de l' a est de nouveau en O(1).

En fait, l'ensemble de la composition est réassociées avec la différence les listes, dès que vous avez besoin de le premier élément,

((((a ++) . (b ++)) . (c ++)) . (d ++)) . (e ++) $ f

devient

((((a ++) . (b ++)) . (c ++)) . (d ++)) $ (e ++) f
(((a ++) . (b ++)) . (c ++)) $ (d ++) ((e ++) f)
((a ++) . (b ++)) $ (c ++) ((d ++) ((e ++) f))
(a ++) $ (b ++) ((c ++) ((d ++) ((e ++) f)))
a ++ (b ++ (c ++ (d ++ (e ++ f))))

et après cela, chaque liste, qui est à l'opérande de gauche de haut-niveau (++) après la liste ci-dessus a été consommé.

La chose importante est que l'ajoutant la fonction (a ++) pouvez commencer à produire son résultat sans l'inspecter son argument, de sorte que la réassociation de

             ($)
             / \
           (.)  f
           / \
         (.) (e ++)
         / \
       (.) (d ++)
       / \
     (.) (c ++)
     / \
(a ++) (b ++)

via

           ($)---------
           /           \
         (.)           ($)
         / \           / \
       (.) (d ++) (e ++)  f
       / \
     (.) (c ++)
     / \
(a ++) (b ++)

pour

     ($)
     / \
(a ++) ($)
       / \
  (b ++) ($)
         / \
    (c ++) ($)
           / \
      (d ++) ($)
             / \
        (e ++)  f

n'a pas besoin de savoir quelque chose au sujet de la composée des fonctions de la liste définitive f, donc c'est juste un O(depth) de réécriture. Puis le haut-niveau

     ($)
     / \
(a ++)  stuff

devient

 (++)
 /  \
a    stuff

et tous les éléments de l' a peut être obtenu en une seule étape. Dans cet exemple, où nous avons eu pur gauche de la nidification, une seule réécriture est nécessaire. Si, au lieu de (par exemple) (d ++) la fonction, dans ce lieu avait été de gauche de la composition imbriquée, (((g ++) . (h ++)) . (i ++)) . (j ++), le niveau supérieur de la réassociation laisserait qui intacte et ce serait réassociées quand il devient l'opérande de gauche de haut-niveau ($) après toutes les listes précédentes ont été consommés.

Le travail total nécessaire pour toutes les reassociations est - O(number of lists), de sorte que le coût global pour la concaténation est - O(number of lists + sum (map length lists)). (Cela signifie que vous pouvez apporter de mauvaises performances dans ce domaine aussi, par l'insertion d'un lot de profondément de gauche imbriquée ([] ++).)

L'

newtype DiffList a = DiffList {getDiffList :: [a] -> [a] }

instance Monoid (DiffList a) where
    mempty = DiffList (\xs -> [] ++ xs)
    (DiffList f) `mappend` (DiffList g) = DiffList (\xs -> f (g xs))

juste enveloppe de sorte qu'il est plus facile à manipuler de façon abstraite.

DiffList (a ++) `mappend` DiffList (b ++) ~> DiffList ((a ++) . (b++))

Notez qu'il est efficace uniquement pour les fonctions qui n'ont pas besoin de vérifier leur argument de commencer à produire de sortie, si des fonctions arbitraires sont enveloppés dans DiffLists, vous n'avez pas une telle efficacité des garanties. En particulier, l'ajout d'une ((++ a), enveloppé ou non) peut créer de gauche imbriqués les arbres de (++) lorsque l'on compose à droite-imbriquées, de sorte que vous pouvez créer l' O(n²) de concaténation de comportement que si l' DiffList constructeur est exposée.

6voto

hugomg Points 29789

Il pourrait être utile d'examiner la définition de la concaténation:

[]     ++ ys = ys
(x:xs) ++ ys = x : (xs ++ ys)

Comme vous pouvez le voir, afin de concaténer deux listes dont vous avez besoin pour aller au-dessus de la liste de gauche et de créer une "copie" de celui-ci, de sorte que vous pouvez modifier à sa fin (c'est parce que vous ne pouvez pas directement changer la fin de l'ancienne liste, en raison de l'immutabilité).

Si vous faites votre concaténations dans le droit associatif façon, il n'y a pas de problème. Une fois inséré, une liste n'aurez jamais à être touchées (remarquez comment ++de définition jamais inspecte la liste sur la droite) pour chaque élément de la liste n'est insérée "une fois" pour un temps total de O(N).

--This is O(n)
(a ++ (b ++ (c ++ (d ++ (e ++ f)))))

Toutefois, si vous ne concaténation dans une associativité à gauche, puis la liste "actuelle" devra "être déchirée vers le bas" et "reconstruire""chaque fois que vous ajoutez une autre liste fragment à la fin. Chaque élément de la liste sera itéré lorsqu'il est inséré et à chaque fois que l'avenir des fragments sont ajoutés ainsi! C'est comme le problème que vous obtenez dans C si vous naïvement appel strcat plusieurs fois dans une rangée.


Comme pour la différence les listes, le truc, c'est qu'ils sorte de garder un explicite "trou" à la fin. Lorsque vous convertissez un DList le retour à la normale de la liste de vous transmettre ce que vous voulez être dans le trou et il sera prêt à aller. Normal listes, d'autre part, toujours à boucher le trou de la fin avec [] donc si vous voulez le changer (lors de la concaténation de), vous devez tirer sur la liste pour arriver à ce point.

La définition de la différence des listes avec les fonctions peuvent regarder intimidant au premier abord, mais il est en fait assez simple. Vous pouvez les consulter à partir d'un Objet Orienté point de vue en les considérant comme des objets opaques "toList" méthode qui reçoit la liste de ce que vous devez insérer dans le trou dans la fin renvoie le DL interne du préfixe en plus de la queue qui était prévu. C'est efficace parce que vous ne branchez le "trou" à la fin après que vous avez terminé la conversion de tout.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X