Quelles structures de données pouvez-vous utiliser pour obtenir la suppression et le remplacement d'O (1)? Ou comment éviter les situations où vous avez besoin de ces structures?
ST
monade de Haskell le fait très bien.
Quelles structures de données pouvez-vous utiliser pour obtenir la suppression et le remplacement d'O (1)? Ou comment éviter les situations où vous avez besoin de ces structures?
ST
monade de Haskell le fait très bien.
Réponses:
Il existe une vaste gamme de structures de données exploitant la paresse et d'autres astuces pour obtenir un temps constant amorti ou même (dans certains cas limités, comme les files d' attente ) des mises à jour à temps constant pour de nombreux types de problèmes. La thèse de doctorat de Chris Okasaki "Structures de données purement fonctionnelles" et le livre du même nom en sont un excellent exemple (peut-être le premier grand), mais le domaine a progressé depuis . Ces structures de données sont généralement non seulement purement fonctionnelles dans l'interface, mais peuvent également être implémentées en Haskell pur et dans des langages similaires, et sont entièrement persistantes.
Même sans aucun de ces outils avancés, de simples arbres de recherche binaires équilibrés fournissent des mises à jour logarithmiques, de sorte que la mémoire mutable peut être simulée avec au pire un ralentissement logarithmique.
Il existe d'autres options, qui peuvent être considérées comme de la triche, mais sont très efficaces en ce qui concerne l'effort de mise en œuvre et les performances réelles. Par exemple, les types linéaires ou les types d' unicité permettent la mise à jour sur place comme stratégie d'implémentation pour un langage conceptuellement pur, en empêchant le programme de conserver la valeur précédente (la mémoire qui serait mutée). C'est moins général que les structures de données persistantes: par exemple, vous ne pouvez pas facilement créer un journal d'annulation en stockant toutes les versions précédentes de l'état. C'est toujours un outil puissant, bien qu'AFAIK ne soit pas encore disponible dans les principaux langages fonctionnels.
Une autre option pour introduire en toute sécurité un état mutable dans un cadre fonctionnel est la ST
monade de Haskell. Il peut être implémenté sans mutation et sans unsafe*
fonctions, il se comporte comme s'il s'agissait simplement d'un habillage sophistiqué autour du passage implicite d'une structure de données persistante (cf. State
). Mais en raison de la ruse du système qui impose l'ordre d'évaluation et empêche l'échappement, il peut être implémenté en toute sécurité avec une mutation sur place, avec tous les avantages de performance.
Une structure mutable bon marché est la pile d'arguments.
Jetez un œil au calcul factoriel type SICP:
(defn fac (n accum)
(if (= n 1)
accum
(fac (- n 1) (* accum n)))
(defn factorial (n) (fac n 1))
Comme vous pouvez le voir, le deuxième argument de fac
est utilisé comme un accumulateur mutable pour contenir le produit à évolution rapide n * (n-1) * (n-2) * ...
. Cependant, aucune variable modifiable n'est en vue et il n'y a aucun moyen de modifier accidentellement l'accumulateur, par exemple à partir d'un autre thread.
Ceci est, bien sûr, un exemple limité.
Vous pouvez obtenir des listes liées immuables avec un remplacement bon marché du nœud principal (et par extension toute partie commençant par la tête): vous faites simplement pointer la nouvelle tête vers le même nœud suivant que l'ancienne tête. Cela fonctionne bien avec de nombreux algorithmes de traitement de liste ( fold
basés sur n'importe quoi ).
Vous pouvez obtenir de très bonnes performances à partir de tableaux associatifs basés, par exemple, sur des HAMT . Logiquement, vous recevez un nouveau tableau associatif avec des paires clé-valeur modifiées. L'implémentation peut partager la plupart des données communes entre les anciens et les nouveaux objets créés. Ce n'est cependant pas O (1); généralement, vous obtenez quelque chose de logarithmique, au moins dans le pire des cas. Les arbres immuables, en revanche, ne souffrent généralement pas de pénalité de performance par rapport aux arbres mutables. Bien sûr, cela nécessite une surcharge de mémoire, généralement loin d'être prohibitive.
Une autre approche est basée sur l'idée que si un arbre tombe dans une forêt et que personne ne l'entend, il n'a pas besoin de produire de son. Autrement dit, si vous pouvez prouver qu'un peu d'état muté ne quitte jamais une portée locale, vous pouvez muter les données qu'il contient en toute sécurité.
Clojure a des transitoires qui sont des ombres mutables de structures de données immuables qui ne fuient pas en dehors de la portée locale. Clean utilise Uniques pour obtenir quelque chose de similaire (si je me souviens bien). La rouille aide à faire des choses similaires avec des pointeurs uniques vérifiés statiquement.
ref
et les limiter dans une certaine portée. Voir IORef
ou STRef
. Et puis bien sûr il y a des TVar
s et des MVar
s qui sont similaires mais avec une sémantique concurrente saine (stm pour TVar
s et mutex basé sur MVar
s)
Ce que vous demandez est un peu trop large. O (1) retrait et remplacement de quelle position? Le chef d'une séquence? La queue? Une position arbitraire? La structure de données à utiliser dépend de ces détails. Cela dit, 2-3 Finger Trees semblent être l'une des structures de données persistantes les plus polyvalentes:
Nous présentons 2-3 arbres à doigts, une représentation fonctionnelle de séquences persistantes supportant l'accès aux extrémités en temps constant amorti, et la concaténation et la division en temps logarithmique de la taille de la plus petite pièce.
(...)
De plus, en définissant l'opération de division sous une forme générale, nous obtenons une structure de données à usage général qui peut servir de séquence, de file d'attente prioritaire, d'arbre de recherche, de file d'attente de recherche prioritaire et plus encore.
Les structures de données généralement persistantes ont des performances logarithmiques lors de la modification de positions arbitraires. Cela peut ou non être un problème, car la constante dans un algorithme O (1) peut être élevée, et le ralentissement logarithmique peut être "absorbé" dans un algorithme global plus lent.
Plus important encore, les structures de données persistantes facilitent le raisonnement sur votre programme, et cela devrait toujours être votre mode de fonctionnement par défaut. Vous devez privilégier les structures de données persistantes dans la mesure du possible et n'utiliser une structure de données modifiables qu'une fois que vous avez profilé et déterminé que la structure de données persistante est un goulot d'étranglement des performances. Tout le reste est une optimisation prématurée.