În urma parcurgerii acestui articol studentul va fi capabil să:
Un dicţionar este un tip de date abstract compus dintr-o colecție de chei şi o colecție de valori, în care fiecărei chei îi este asociată o valoare.
Operația de găsire a unei valori asociate unei chei poartă numele de indexare, aceasta fiind și cea mai importantă operație. Din acest motiv dicționarele se mai numesc și array-uri asociative - fac asocierea între o cheie și o valoare.
O implementare frecvent întâlnită a unui dicționar este cea folosind o tabelă de dispersie - hashtable. Un hashtable este o structură de date optimizată pentru funcția de căutare - în medie, timpul de căutare este constant: O(1). Acest lucru se realizează transformând cheia într-un hash - un număr întreg fără semn pe 16 / 32 / 64 de biţi, etc. - folosind o funcție hash.
În cel mai defavorabil caz, timpul de căutare al unui element poate fi O(n). Totuși, tabelele de dispersie sunt foarte utile în cazul în care se stochează cantități mari de date, a căror dimensiune (mărime a volumului de date) poate fi anticipat.
Funcția hash trebuie aleasă astfel încât să se minimizeze numărul coliziunilor (chei diferite care produc aceleași hash-uri). Coliziunile apar în mod inerent, deoarece lungimea hash-ului este fixă, iar obiectele de stocare pot avea lungimi și conținut arbitrare. În cazul apariției unei coliziuni, valorile se stochează pe aceeaşi poziție - în același bucket. În acest caz, căutarea se va reduce la compararea valorilor efective în cadrul bucket-ului.
Exemplu de hash pentru șiruri de caractere:
#ifndef __HASH__H #define __HASH__H // Hash function based on djb2 from Dan Bernstein // http://www.cse.yorku.ca/~oz/hash.html // // @return computed hash value unsigned int hash_fct(char *str) { unsigned int hash = 5381; int c; while ((c = *str ++) != 0) { hash = ((hash << 5) + hash) + c; } return hash; } #endif //__HASH__H
O implementare a unui hashtable care trateaza coliziunile se numește înlănțuire directă - direct chaining. Cea mai simplă formă folosește câte o listă înlănțuită pentru fiecare bucket, practic un array de liste.
Fiecare listă este asociată unui anumit hash.
Dacă dimensiunea array-ului este exprimată în puteri ale lui 2, se mai poate folosi şi formula următoare → index = hash & (HMAX - 1).
HMAX reprezintă dimensiunea maximă a array-ului.
Avantajul tabelelor de dispersie constă în faptul că operația de ștergere este simplă, iar redimensionarea tabelei poate fi amânată mult timp, deoarece performanța este suficient de bună chiar și atunci când toate pozițiile din hashtable sunt folosite.
Dezavantajele acestei soluții sunt cele moștenite de la listele înlănțuite: pentru stocarea unor date mici, overhead-ul introdus poate fi semnificativ, iar parcurgerea unei liste este costisitoare.
Există și alte structuri de date cu ajutorul cărora se poate implementa un hashtable ca mai sus. Un exemplu ar fi un arbore binar echilibrat, pentru care timpul, pe cazul cel mai defavorabil, se poate reduce la O(log n) față de O(n). Totuși, această variantă se poate dovedi ineficientă dacă hashtable-ul este proiectat pentru puține coliziuni.
Acestea prezintă timpi de căutare mai buni pentru cel mai defavorabil caz și folosesc eficient spațiul de stocare în funcție de tipul de date folosit.
În acest exempmlu vom folosi clasa std::map din STL. Pentru mai multe detalii, vă sugerăm să citiți documentația oficială - std::map.
Atenție! Este nevoie să includem biblioteca queue.
#include <map> // std::map
Clasa std::map oferă toate funcționalitățile uzuale pentru un HashTable. Menționăm că nu există o implementare unică, astfel încât apar diferențe față de implementările sugerate în articol (de exemplu cheile din map sunt unice). În continuare ne vom referi doar la următoatele funcționalități:
Se dă un număr n foarte mare și n stringuri. Se cere să se afișeze pe ecran numărul de apariții al fiecărui string utilizând un spațiu de memorie cât mai mic.
Soluție: Vom simula funcționalitatea unui vector de frecvență folosinf std::map.
#include <iostream> // std::cout #include <map> // std::map int main() { int n; // numarul de elemente din lista std::string x; // variabila temporara std::map <std::string, int> hash; // map-ul (hash-ul) folosit // citire elemente si adaugare in stiva std::cout << "n = "; std::cin >> n; for (int i = 0; i < n; ++i) { // citeste un alt element std::cout << "x = "; std::cin >> x; // adauga o aparitie a lui x ++hash[ x ]; // afisare statistici std::cout << x << " apare de " << hash[x] << " ori; hash size = " << hash.size() << "\n"; } // parcurgerea elementelor din hash std::cout << "Stare finala hash\n"; std::cout << "hash size = " << hash.size() << '\n'; for (std::map <std::string, int> :: iterator it = hash.begin(); it != hash.end(); ++it) { // extrag key si value std::string key = it->first; int value = it->second; // afisez de cate ori a fost intalnit key std::cout << key << " apare de " << value << " ori\n"; } std::cout << "Golesc hash\n"; hash.clear(); std::cout << (hash.empty() ? "Hash gol" : "Hash contine elemente") << "\n"; return 0; }
Compilare şi rulare
g++ main.cpp -o main ./main
Exemplu output:
n = 10 x = SD SD apare de 1 ori; hash size = 1 x = PL PL apare de 1 ori; hash size = 2 x = MN MN apare de 1 ori; hash size = 3 x = SD SD apare de 2 ori; hash size = 3 x = SD SD apare de 3 ori; hash size = 3 x = PL PL apare de 2 ori; hash size = 3 x = CMOS CMOS apare de 1 ori; hash size = 4 x = BUCURIE BUCURIE apare de 1 ori; hash size = 5 x = cmos cmos apare de 1 ori; hash size = 6 x = proiect proiect apare de 1 ori; hash size = 7 Stare finala hash hash size = 7 BUCURIE apare de 1 ori CMOS apare de 1 ori MN apare de 1 ori PL apare de 2 ori SD apare de 3 ori cmos apare de 1 ori proiect apare de 1 ori Golesc hash Hash gol
Header-ul pentru clasa Hashtable:
#ifndef __HASHTABLE__H #define __HASHTABLE__H #include <list> template<typename Tkey, typename Tvalue> struct elem_info { Tkey key; Tvalue value; }; template<typename Tkey, typename Tvalue> class Hashtable { private: std::list<struct elem_info<Tkey, Tvalue> > *H; int HMAX; unsigned int (*hash) (Tkey); public: Hashtable(int hmax, unsigned int (*h)(Tkey)); ~Hashtable(); void put(Tkey key, Tvalue value); void remove(Tkey key); Tvalue get(Tkey key); bool has_key(Tkey key); }; #endif //__HASHTABLE__H
Această secțiune nu este punctată și încearcă să vă facă o oarecare idee a tipurilor de întrebări pe care le puteți întâlni la un job interview (internship, part-time, full-time, etc.) din materia prezentată în cadrul laboratorului.