This is an old revision of the document!


TODO

Scopul laboratorului:

  • recapitularea conceptelor învățate
  • programarea cu o structură funcțională
  • înțelegerea conceptului de “leftist heap”

gg

O 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ă

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. Avem astfel complexitățile:

Funcție Complexitate
isEmpty O(1)
insert O(1)
top O(n)
delete O(n)

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. Avem astfel complexitățile:

Funcție Complexitate
isEmpty O(1)
insert O(n)
top O(1)
delete O(1)

Implementarea cu liste nu este ideală și putem obține performanțe mai bune.

Binary Heap

Un binary heap este un arbore binar cu următoarele două proprietăți:

  • pentru orice nod, valoarea asociată este mai mare sau egală cu valorile copiiilor
  • arborele este complet: fiecare nivel este plin, cu excepția ultimului, pe care nodurile sunt cât mai la stânga

Leftist Heap

Un leftist heap este

În 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))

<interactive>: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 <interactive>: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.

  1. Folosiți liste Haskell pentru a implementa o coadă de priorități.
    1. implementați o coadă de priorități care scoate valoarea minimă
    2. implementați o coadă de priorități care scoate valoarea maximă
  2. Folosiți arbori binari pentru a implementa un leftist heap
Clasa PQueue conține unele funcții cu implementări default. Considerați înlocuirea acestora cu implementări particularizate.