====== Structuri funcționale de date ====== Scopul laboratorului: * recapitularea conceptelor învățate * programarea cu o structură de date funcțională * înțelegerea conceptului de "leftist heap" ===== Priority Queue ===== O [[http://pages.cs.wisc.edu/~vernon/cs367/notes/11.PRIORITY-Q.html|coadă de priorități ("priority queue")]] este o structură de date în care putem stoca elemente și din care îl putem scoate pe cel cu prioritatea cea mai mare. Spre deosebire de o coadă normală, care respectă principiul First In, First Out, elementele unei cozi de priorități sunt scoase în ordinea //priorității//, o proprietate a obiectelor conținute. Operațiile neceseare implementării unei cozi de priorități sunt: * ''empty'' - care creează o coadă goală de priorități * ''isEmpty'' - verifică dacă există elemente în coadă * ''insert'' - inserează un element într-o coadă * ''top'' - returnează elementul cu prioritatea maximă * ''delete'' - scoate elementul cu prioritatea maximă din coadă O altă operație utilă (și necesară în unele implementări) este ''merge'', care primește două liste de priorități și le combină într-una singură. Coada de priorități este o structură abstractă, ce poate avea diverse implementări care diferă prin complexitatea diverselor operații (e.g. ''insert'', ''delete''). Un exemplu naiv este implementarea unei cozi de priorități folosind o listă. Avem două posibilități: să introducem complexitatea determinării priorității în funcția ''top'', sau în ''insert''. Putem pune mereu un element la începutul listei (astfel ''insert'' e echivalent cu '':''). Atunci când avem nevoie de cel cu prioritatea cea mai mare, pornim o căutare liniară prin elementele listei. Similar pentru ștergere. Pentru operația de ''merge'' putem folosi append din Haskell (''%%+%%%%+%%''). Avem astfel complexitățile: | Funcție | Complexitate | | ''isEmpty'' | ''O(1)'' | | ''insert'' | ''O(1)'' | | ''top'' | ''O(n)'' | | ''delete'' | ''O(n)'' | | ''merge'' | ''O(n)'' | ''n'' este lungimea listei care stă la baza cozii. Pentru ''merge'', ''n'' este lungimea listei din stânga. Alternativ, simplificând funcția ''top'', ne asigurăm că elementele sunt mereu //ordonate// în listă (astfel, ''top'' este echivalent cu ''head''). Inserarea unui element într-o listă ordonată se face în timp liniar (amintiți-vă de //insert sort//). Combinarea a două liste ordonate se face, deasemena în timp liniar (amintiți-vă de //merge sort//). Avem astfel complexitățile: | Funcție | Complexitate | | ''isEmpty'' | ''O(1)'' | | ''insert'' | ''O(n)'' | | ''top'' | ''O(1)'' | | ''delete'' | ''O(1)'' | | ''merge'' | ''O(n+m)'' | Pentru ''merge'', considerăm două liste cu dimensiunile ''n'', respectiv ''m''. Implementarea cu liste nu este ideală și putem obține performanțe mai bune. ==== Binary Heap ==== Un [[https://en.wikipedia.org/wiki/Binary_heap|binary heap]] este un arbore binar cu următoarele două proprietăți: * pentru orice nod, valoarea asociată acestuia este fie mai mare sau egală, fie mai mica sau egală cu valorile copiilor * arborele este [[https://en.wikipedia.org/wiki/Binary_tree#Types_of_binary_trees|complet]]: fiecare nivel este plin, cu excepția ultimului, pe care nodurile sunt cât mai la stânga - acest lucru permite ca arborele să fie ținut într-un array. Pentru inserare se va introduce noul element la finalul array-ului. Noua valoare introdusă poate schimba locul cu părintele, în cazul în care prioritățile nu corespund (nu respectă ordinea impusă de noi). Această operație poartă diverse denumiri: ''sift-up'', ''percolate-up'', ''cascade-up'', ... Pentru ștergere se va elimina primul element, se mută ultimul element din array ca nou "varf" al arborelui, după care se va face swap între acesta și copilul cel mai mare/mic (în funcție de ordine). Acestă operație se numește: ''sift-down'', ''percolate-down'', ''cascade-down'', ... Se poate observa că aceste operații ar fi greu de implementat într-un stil funcțional. ==== Leftist Heap === Un [[https://en.wikipedia.org/wiki/Leftist_tree|leftist heap]] este o structură de date pur funcțională. În acest tip de heap se mai menține o informație și anume un rank. Acesta reprezintă distanța la cea mai apropiată frunză. Leftist heap-ul are următoarea proprietate: * rank-ul oricărui subarbore stâng este cel puțin la fel de mare ca cel al subarborelui drept. Leftist heap-ul utilizeză o operație generală de ''merge'' pentru a-și defini operațiile de ''insert'' și ''delete''. Astfel, un ''insert'' înseamnă crearea unui nou nod și apelarea funcției de ''merge'' pe ''root'' și pe acesta. Un ''delete'' se implementează prin eliminarea ''root-ului'' și apelarea funcției de ''merge'' pe copilul stâng și cel drept. [[https://courses.cs.washington.edu/courses/cse326/08sp/lectures/markup/05-leftist-heaps-markup.pdf | Leftist heap - more info ]] Obținem complexitățile: | Funcție | Complexitate | | ''isEmpty'' | ''O(1)'' | | ''insert'' | ''O(log(n))'' | | ''top'' | ''O(1)'' | | ''delete'' | ''O(log(n))'' | | ''merge'' | ''O(log(n))'' | ===== newtype ===== În [[pp:l04|laboratorul de tipuri]], ați învățat să definiți noi tipuri de date în Haskell, folosind două metode oferite de cuvintele cheie ''data'' și ''type'': * ''data'' este folosit pentru a crea tipuri algebrice complexe, cu un număr arbitrar de constructori care pot avea, la rândul lor, un număr arbitrar de parametrii * ''type'' este folosit pentru a crea //un alias de tip//, în scopul clarității (e.g. ''type String = [Char]'') Cuvântul cheie ''newtype'' poate fi folosit pentru a crea un nou tip cu următoarele constrângeri: * tipul are un singur constructor * constructorul are un singur argument -- este foarte comun ca tipul și constructorul să aibă același nume -- amintiți-vă de record syntax, getPair este o funcție autogenerată -- cu tipul "Pair a b -> (a, b)" newtype Pair a b = Pair { getPair :: (a, b) } Obținem astfel un nou tip, echivalent cu perechea din Haskell, dar //distinct//: ''Pair a b'' și ''(a, b)'' **nu sunt interschimbabile**: Prelude> :t fst fst :: (a, b) -> a Prelude> fst (Pair (1, 3)) :17:6: error: • Couldn't match expected type ‘(a, b0)’ with actual type ‘Pair Integer Integer’ • In the first argument of ‘fst’, namely ‘(Pair (1, 3))’ In the expression: fst (Pair (1, 3)) In an equation for ‘it’: it = fst (Pair (1, 3)) • Relevant bindings include it :: a (bound at :17:1) Prelude> Astfel, ''newtype'' ajută atât la claritatea codului, cât și la type-safety. ==== newtype vs type ==== ''type'' creează //sinonime de tip// perfect interschimbabile. Având definiția ''type String = [Char]'' putem să trimitem un ''String'' oricărei funcții care așteaptă ''[Char]'' și vice-versa: Prelude> :t id id :: a -> a Prelude> (id :: String -> [Char]) ("asdf" :: [Char]) "asdf" Prelude> (id :: [Char] -> [Char]) ("asdf" :: String) "asdf" Prelude> Scopul lui ''type'' este de a ajuta programtorul, crescând lizibilitatea codului. De exemplu, având definiția ''type FilePath = String'', devine mai clar ce face funcția: *Main> :t combine combine :: FilePath -> FilePath -> FilePath ==== newtype vs data ==== Sintactic și semantic, funcționalitatea ''newtype'' poate fi înlocuită de ''data'', întrucât ''data'' este un fel mai general de a defini tipuri (număr arbitrar de constructori, număr arbitrar de argumente): data Pair a b = Pair { getPair :: (a, b) } Diferența este însă una de //performanță//: tocmai din cauza constrângerilor lui ''newtype'', compilatorul poate să elimine orice overhead de împachetare/despachetare la runtime. Avem astfel tipuri diferite și putem beneficia de verificarea sistemului de tipuri, fără dezvantajul împachetării/despachetării constructorilor la rulare. ==== Înrolarea în clase ==== Pentru că tipurile definite de ''type'' sunt sinonime perfecte acestea au aceeași înrolare într-o clasă anume, deci același comportament. De exemplu, nu putem avea formate diferite de afișare pentru un ''String'' și un ''[Char]''. Însă, drept consecință a faptului că ''newtype'' definește **un tip nou**, putem scrie, pentru perechea definită mai sus: instance (Show a, Show b) => Show (Pair a b) where show (Pair a b) = "<" ++ show a ++ "," ++ show b ++ ">" *Main> (7, 27) (7,27) *Main> Pair 7 27 <7,27> ===== Exerciții ===== - Folosiți liste Haskell pentru a implementa o coadă de priorități. - implementați o coadă de priorități care scoate valoarea //minimă// - implementați o coadă de priorități care scoate valoarea //maximă// (//hint:// folosiți ''newtype'' pentru a face diferența) - Folosiți arbori binari pentru a implementa un leftist heap {{:pp:laborator_6_-_schelet.zip|Lab 6 - Schelet}}\\ ===== Recommended Reading ===== * [[http://typeocaml.com/2015/03/12/heap-leftist-tree/|Heap - Leftist Tree (discuție și implementare în OCaml, puteți ignora codul în sine)]] * [[http://learnyouahaskell.com/types-and-typeclasses#typeclasses-101|Learn you a Haskell for Great Good - Chapter 2: Types and Typeclasses#Typeclasess 101]]\\ * [[http://learnyouahaskell.com/making-our-own-types-and-typeclasses#typeclasses-102|Learn you a Haskell for Great Good - Chapter 8: Making Our Own Types and Typeclasses#Typeclasess 102]]\\ * [[http://book.realworldhaskell.org/read/using-typeclasses.html|Real World Haskell - Chapter 6: Using Typeclasses]]\\ * [[https://www.haskell.org/tutorial/stdclasses.html|A Gentle Introduction to Haskell - Chapter 8: Standard Haskell Classes]]