This is an old revision of the document!
Responsabili:
Data publicării : 26 martie, ora: 21:00
Deadline: 16 aprilie, ora 23:55
În urma realizării acestei teme:
Problema estimarii cardinalitatii (a numararii elementelor distincte) este, in esenta, gasirea numarului de elemente unice dintr-o colectie de elemente care se pot repeta.
Pentru primele doua subpuncte, vom rezolva o problema si mai restrictiva: gasirea numarului de aparitii pentru fiecare element. Pentru restul, vom vedea ca acest lucru e mai greu realizabil cand vine vorba de volume mari de date si, de aceea, ne vom rezuma la gasirea numarului de elemente distincte.
La intrare se dau numere intre 0 si 2000000. Gasiti numarul de aparitii ale fiecarui element, utilizand un vector de frecventa.
Un vector de frecventa este un vector care are pe pozitia i numarul de aparitii ale elementului i.
Se garanteaza ca numarul de aparitii ale oricarui element este mai mic decat 256.
La intrare se dau siruri de caractere. Gasiti numarul de aparitii ale fiecarui sir folosind un Hashtable cu politica de rezolvare a conflictelor de tip open addressing prin linear probing.
Aceasta politica presupune ca, in momentul in care bucketul unde trebuie realizata insertia este deja ocupat, se va cauta secvential o pozitie libera incepand cu bucketul urmator.
Evident, daca si aceasta pozitie este ocupata, vom cauta prima pozitie libera in continuare.
Daca se va ajunge la finalul listei de bucketuri, se va continua de la inceput.
Se garanteaza existenta a cel putin unui bucket liber in momentul fiecarei operatii de insertie.
Evident, daca in momentul unei operatii de selectie nu gasim cheia in bucketul in care ne-am astepta, vom continua cautarea secvential, aplicand un procedeu similar cu cel din momentul insertiei.
Se garanteaza ca lungimea maxima a oricarui este maxim 100 de caractere.
Se garanteaza ca numarul de aparitii ale oricarui element este mai mic decat 256.
In cerintele anterioare am observat ca putem calcula cu exactitate numarul de elemente distincte (si numarul lor de aparitii) retinand, intr-un fel sau altul, fiecare element unic (ca pozitie intr-un vector, respectiv ca cheie intr-un hashtable). Din pacate, in aplicatiile din lumea reala, aceasta strategie nu este sustenabila.
Sa ne imaginam urmatoarea situatie: Youtube afiseaza pentru fiecare videoclip numarul de vizualizari. Pentru ca acest sistem sa nu fie abuzat (spre exemplu de un bot care vizioneaza acelasi videoclip incontinuu pentru a-i spori view count-ul), trebuie sa tina cont si de numarul de utilizatori unici care au accesat clipul.
Daca ar retine un id pentru fiecare utilizator, ar avea nevoie de o structura de date cu milioane de intrari, si asta doar pentru un singur clip. Tinand cont ca exista peste 31 de milioane de canale (iar multe dintre ele au peste 100 de clipuri), acest lucru iese din discutie.
Cum putem totusi sa ne indeplinim obiectivul?
Problema la abordarea anterioara este faptul ca foloseste o cantitate de memorie proportionala cu numarul de utilizatori distincti O(n). In cautarea unei solutii mai bune, va trebui sa obtinem o complexitate a spatiului mai mica (O(sqrt(n)), O(logn), O(1) etc.)
Solutia gasita (ce urmeaza a fi implementata in tema) aduce cu sine un sacrificiu din punct de vedere al preciziei: din moment ce nu stim exact ce utilizatori am avut, numarul total de utilizatori unici nu va mai fi unul precis, ci doar aproximativ.
In practica, acest lucru nu este un dezavantaj prea mare (intrucat rareori e relevanta diferenta dintre 1m vizualizari si 1.1m vizualizari).
In ilustrarea functionarii algoritmului HyperLogLog, vom incepe de la o serie de principii simple pe care le vom pune cap la cap, ajungand la descrierea algoritmului final.
Sa presupunem ca generam un numar la intamplare.
Probabilitatea ca numarul sa inceapa cu un bit 0 este 1/2 (deoarece poate incepe fie cu 0, fie cu 1).
Probabilitatea ca numarul sa inceapa cu 2 biti 0 este 1/4 (deoarece poate incepe cu 00, 01, 10 sau 11).
Similar, probabilitatea ca numarul sa inceapa cu 3 biti 0 este 1/8. Astfel, pentru a intalni un numar care sa inceapa cu 3 biti 0, va trebui sa generam, in medie, 8 numere.
Privind aceasta observatie in sens invers, daca am generat numere aleatoare si secventa cea mai lunga de 0 de la inceputul oricarui numar a fost de lungime 3, atunci avem urmatoarele posibilitati: - am generat cel putin 8 numere - am avut noroc si a trebuit sa generam mai putin de 8 numere
Evident, pentru valori mici precum 2 sau 3 biti consecutivi, exista o sansa semnificativa sa generam numarul mai rapid (chiar din prima incercare), dar cu cat valorile devin mai mari, cu atat scade aceasta sansa.
In principiu, daca am primit valori aleatoare la intrare, si numarul maxim de biti 0 consecutivi initiali ai oricarui numar este x, putem spune ca am primit intre 2^x si 2^(x+1) valori.
In viata reala, valorile primite la intrare nu vor fi neaparat valori aleatoare. Mai mult, nu toate valorile de intrare vor fi numere intregi. Din acest motiv, vom trece aceste valori printr-o functie de hash. O functie de hash buna ar trebui sa ofere la iesire valori uniform distribuite.
Daca vrem sa imbunatatim performanta algoritmului nostru va trebui sa: - Atenuam efectul negativ al generarii rapide unui numar cu multi biti de 0 initiali - Oferim estimari mai granulare decat puterile lui 2
O idee de rezolvare a primei probleme este sa impartim numerele in mai multe bucketuri. Cea mai usoara modalitate de a face acest lucru este sa impartim fiecare numar in 2 parti: prima parte va fi folosita pentru a determina bucketul, iar a doua va fi folosita ca pana acum.
Singura diferenta fata de metoda precedenta este ca acum vom face maximul de zerouri consecutive pentru fiecare bucket si nu pentru toate numerele.
Pentru a agrega aceste maxime vom folosi media geometrica.
Pentru a aduce algoritmul in forma finala, va trebui sa mai facem cateva ajustari matematice la procedeul descris anterior.
In primul rand, vom folosi o medie similara cu media armonica in loc de cea geometrica.
In al doilea rand, vom utiliza un factor de atenuare alfa, pentru a imbunatati eroarea de aproximare.
E reprezinta estimarea finala
m reprezinta numarul total de bucketuri
Z reprezinta media, calculata dupa urmatoarea formula:
alfa_m reprezinta factorul de atenuare, calculat in functie de m dupa urmatoarea formula:
* checkerul va fi publicat in scurt timp
Temele vor fi trimise pe vmchecker. Atenție! Temele trebuie trimise în secțiunea Structuri de Date (CA).
Arhiva trebuie să conțină:
De aceea, vă sfătuim să nu vă lăsați rezolvări ale temelor pe calculatoare partajate (la laborator etc), pe mail/liste de discuții/grupuri etc.