Î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.
Operația de adăugare a unei perechi (cheie-valoare) în dicționar are două părți. Prima parte este transformarea cheii într-un index întreg, strict pozitiv, printr-o funcție de hashing. În mod ideal, chei diferite mapează indexuri diferite în dicționar, însa în realitate nu se întamplă acest lucru. De aceea, partea a doua a operației de adăugare constă în procesul de rezolvare a coliziunilor.
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) - dupa hash vom avea toate elementele in acelasi bucket, formand astfel o lista inlantuita care va trebui parcursa in intregime. 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 ale cheilor î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__
Exemplu pentru construcția de funcții hash care minimizează numărul de coliziuni
O implementare a unui hashtable care tratează 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.
void put(key, value, hash_table) index <- hash(key) % DIMENSIUNE_DICTIONAR pentru element it in bucketul hash_table[index] // Se itereaza prin lista inlantuita de la index, pana se // gaseste cheia dorita; daca nu este gasita, vom insera // un entry nou in cadrul bucketului daca it->key == key // Daca exista key deja in bucket // doar se updateaza valoarea it->value <- value return; // Cheia a fost actualizata, iesim din functie // Daca nu a fost gasita cheia in bucket, inseram una noua creeaza un nou element pe baza key, value adauga elementul in hashtable void remove(key, hash_table) index <- hash(key) % DIMENSIUNE_DICTIONAR pentru element it in bucketul hash_table[index] daca it->key == key break // it va pointa ori dupa ultimul element (hash_table.end) => nu avem ce sterge // ori catre un element deja existent in bucket => stergem elementul it daca it nu indica finalul listei sterge elementul de la pozitia lui it TypeValue get(key, hash_table) index <- hash(key) % DIMENSIUNE_DICTIONAR pentru element it in bucketul hash_table[index] daca it->key == key return it->value return null bool has_key(key, hash_table) index <- hash(key) % DIMENSIUNE_DICTIONAR pentru element it in bucketul hash_table[index] daca it->key == key return true return false
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 de căutare 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.
Un alt mod de a utiliza o listă inlanțuită pentru crearea unui dicționar presupune folosirea linear probing. Atunci când la inserarea unei perechi (cheie-valoare) în dicționar apărea o coliziune, algoritmul caută primul “spațiu gol” și inserează acolo perechea.
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.
[5p] Implementaţi structura de date dicţionar, plecând de la pseudocodul de mai sus şi de la schelet. (problema Hashmap Implementation pe LambdaChecker)
[2p] Rezolvati problema desemnata semigrupei voastre. O veti gasi pe LambdaChecker sub forma SD-CA-LAB-04 31XCAy.
(try at home) Implementați și testați redimensionarea unui hashtable: funcția resize dublează dimensiunea structurii interne a tabelei de dispersie. Dublarea se va face în momentul în care raportul dintre numărul de elemente introduse în hashtable şi numărul de bucket-uri HMAX este mai mare decâti o valoare aleasă (ex: size / HMAX > 0.75). Comportamentul dorit pentru această funcţionalitate este următorul: se redimensionează array-ul de bucket-uri, iar apoi fiecare bucket este parcus în ordine și elementele sunt redistribuite după valoarea noului hash.
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.