Table of Contents

Tema 3 - Bucket Dictionary

Deadline: 17.12.2014 Responsabili: Victor Ciurel, Vlad Păunescu Data publicarii: 07.12.2014 Data ultimei modificari: 10.12.2014

Obiective

Descrierea problemei

Introducere

Tabela de dispersie asociativǎ este o structurǎ de date care asociazǎ chei cu valori. Aceasta permite cǎutarea eficientǎ a unei valori pe baza cheii corespunzǎtoare. De exemplu, putem avea drept tabelǎ de dispersie o agendǎ telefonicǎ din care sǎ obţinem numǎrul de telefon al unei persoane pe baza numelui sǎu.

Principiul de bazǎ este urmǎtorul: în locul dispunerii într-o singurǎ listǎ liniarǎ a tuturor perechilor cheie-valoare (entry-uri) din tabelǎ, acestea sunt grupate în mai multe subliste (bucket-uri), pe baza unui anumit criteriu. Astfel, cǎutarea unei chei se va face doar în bucket-ul corespunzǎtor, şi nu în toatǎ lista, fapt ce reduce timpul de cǎutare. Pentru stabilirea bucket-ului în care va fi plasat un entry, cheii îi este aplicatǎ o funcţie de dispersie (hash function) care întoarce un numǎr întreg, numit cod de dispersie (hash code). Pe baza acestuia se determinǎ bucket-ul în care se va introduce intrarea. Iatǎ o tabelǎ de dispersie:

Sǎ luǎm exemplul agendei telefonice. Putem alege drept funcţie de dispersie h poziţia din alfabet a primei litere a numelui persoanei. De exemplu, h(“Andrei”) = 1 (litera A ocupǎ poziţia 1 în alfabet). Avem astfel 26 de valori posibile ale funcţiei h şi introducem în tabelǎ 26 de bucket-uri. Primul va reprezenta lista perechilor (nume, numar_de_telefon) pentru care numele încep cu litera A. Al doilea, cu litera B etc. Mai jos se aflǎ o reprezentare principialǎ a agendei. Desigur, în ultima coloanǎ se pot înlǎnţui mai multe intrǎri în acelaşi bucket.

Constatǎm cǎ eficienţa operaţiei de cǎutare depinde de alegerea funcţiei de dispersie. De exemplu, pentru o funcţie de dispersie constantǎ, h(key) = 1, pentru orice cheie key, obţinem o unicǎ listǎ înlǎnţuitǎ. În acest caz eficienţa este cea mai slabǎ. La polul opus, cu eficienţǎ maximǎ, s-ar afla o funcţie de dispersie injectivǎ, care genereazǎ un cod de dispersie unic pentru fiecare cheie, astfel cǎ fiecare bucket ar conţine o singurǎ intrare. În practicǎ, astfel de funcţii sunt foarte greu de gǎsit.

Scopul temei

Tema urmǎreşte implementarea unui tabel de dispersie asociativ, generic, ce va permite stocarea perechilor cheie-valoare, având orice tipuri.

Cerinţe

Implementare(90p)

MyHashMap

Clasa voastrǎ va implementa interfaţa MyHashMap<K, V> definitǎ de noi, conservând genericitatea:

class MyHashMapImpl<K, V> implements MyHashMap<K, V>

Interfaţa MyHashMap conţine şi interfeţele interioare Entry<K, V> şi Bucket<K, V>, pentru a reprezenta entitǎţile implicate. Va trebui sǎ implementaţi şi aceste interfeţe (puteţi folosi clase interne).

În cadrul implementǎrii voastre va trebui sǎ aveţi acces la bucket-urile tabelei direct prin intermediul unui indice. Folosiţi un vector de bucket-uri (ArrayList, [] nu il puteti folosi cu tipuri parametrizate) şi nu o listǎ înlǎnţuitǎ (cum ar fi LinkedList), pentru ca accesul la orice element sǎ se facǎ în timp constant. Ideal ar fi ca acest index sǎ fie tocmai codul de dispersie. Adesea însǎ, codul are un interval prea mare de valori pentru a putea reprezenta direct indicele într-un vector. Cum, în general, codul de dispersie are tipul int, este puţin probabil sǎ definiţi un vector cu indici între -2 miliarde şi 2 miliarde. De obicei se defineşte un vector cu un numǎr mai redus de bucket-uri (ex.: 20) iar codul de dispersie este translatat în acest interval. O funcţie de translatare des întrebuinţatǎ în acest sens este modulo: translate(hashCode) = |hashCode| % numBuckets. Aceastǎ funcţie garanteazǎ obţinerea unui indice între 0 şi numBuckets - 1. Am folosit valoarea absolută a lui hashCode deoarece poate fi şi negativ.

Clasa Object pune la dispoziţie metoda hashCode, care întoarce tocmai codul de dispersie (int) asociat unui obiect. Aceasta este echivalentǎ funcţiei de dispersie. Metoda este folositǎ de clasa HashMap din biblioteca Java. În mod implicit, la definirea unei noi clase ce moşteneşte Object, hashCode întoarce adresa de memorie a obiectului. Acest lucru nu este de foarte mare ajutor. În general metoda trebuie supradefinitǎ pentru a întoarce un cod calculat (eficient) pe baza datelor interne ale obiectului.

Majoritatea claselor din biblioteca Java (String, Integer etc.) supradefinesc deja metoda hashCode, astfel cǎ vǎ puteţi baza pe ea. Veţi folosi şi voi aceastǎ metodǎ pentru a obţine codul de dispersie al unei chei din tabelǎ, dar îi veţi aplica translatarea din paragraful anterior pentru a obţine indicele în vectorul de bucket-uri. Procesul de determinare a bucket-ului în care va ajunge o cheie se poate rezuma aşa:

key → key.hashCode() → translatare hashCode → indice în vectorul de bucket-uri

Operaţiile de bazǎ ale tabelei, aşa cum rezultǎ şi din interfaţa MyHashMap, vor fi get, put, remove, size, cu aceeaşi funcţionalitate ca cele din clasa HashMap, definitǎ în biblioteca Java. În implementarea voastrǎ puteţi folosi liste, dar nu puteţi folosi nici o implementare de tabel de dispersie din bibliotecǎ.

Dicționar(40p)

Bunii voștri prieteni Knuth, Morris și Pratt, va roaga să îi ajutați, folosind clasa implementată la subpunctul anterior. Ei vor să realizeze un dicționar de cuvinte cu ajutorul căruia să răspunda la o serie de interogări. Knuth vă pune la dispoziție o listă de cuvinte împreuna cu definițiile lor. Sarcina voastra este să implementați o clasă, bazată pe tabela de dispersie explicata la subpunctul anterior. În acest dicționar veți introduce cuvintele, împreuna cu definițiile lor, din lista lui Knuth. În urma procesării listei, va trebui sa răspundeți la o serie de interogări de forma “Care este definiția cuvântului X?”. Nu veți primi interogări pentru cuvinte ce nu apar in listă. Formatul listei lui Knuth si al răspunsului vostru este explicat mai jos.

Definiții multiple(15p)

Cum un cuvânt poate avea mai multe întelesuri, în lista lui Knuth poate apărea un cuvânt cu mai multe definiții. În cadrul dicționarului vostru trebuie sa apară toate aceste definiții. Pentru a verifica acest subpunct va trebui să raspundeti la interogări de forma “Câte definitii are cuvătul X?”. Dacă veți primi o interogare de forma “Care este definiția cuvântului X?”, iar cuvântul X are mai multe definiții, veți răspunde cu prima definiție din listă. Nu veți primi interogări pentru cuvinte ce nu apar în listă.

Definiții duplicate(15p)

Knuth este foarte obosit și în lista lui de cuvinte s-au mai strecurat greșeli, iar unele cuvinte împreună cu definițiile lor pot apărea de mai multe ori. Knuth vă roagă să corectați aceste greșeli. Astfel, daca în listă va aparea un cuvânt ce exista deja in dicționar si are aceeași definiție cu cea din dicționar, nu se va introduce o noua definiție.

Sinonime(20p)

Morris ar vrea sa adauge un pic mai multă funcționalitate dicționarului vostru și vă propune să adăugați și sinonimele cuvintelor alături de definițile lor. Astfel, el vă va da o listă de perechi de cuvinte ce sunt sinonime. Aceste cuvinte apar cel puțin o dată în lista lui Knuth. Pentru a verifica acest subpunct va trebui să răspundeți la o serie de interogări de forma “Care sunt sinonimele cuvântului X?”. Formatul listei lui Morris si al raspunsului vostru este explicat mai jos.

Pentru o pereche de sinonime X și Y, X va fi sinonim cu Y și cu toate sinonimele lui Y, Y va fi sinonim cu X si cu toate sinonimele lui X.

Exemplu: Dacă primiți perechile de sinonime (mare, vast) și (mare, imens), rezultatul este:

Pentru a implementa în mod eficient relațiile de sinonimie vom implementa o relație generală tranzitivă care să se propage la fiecare adăugare de de pereche de chei in map. Pentru a realiza acest lucru, veți folosi o structură de date numită DisjointSets, parametrizată cu un tip T.

Această structură suportă în mod eficient următoarele 3 operații:

Morris vă va descrie în continuare algoritmii ce pot fi folosiți pentru implementarea celor 3 metode. Algoritmii prezentați sunt rudimentari si nu sunt eficienți, fiind axați pe simplitate si nu pe performanță. În cazul în care vreți să învățați mai multe despre structura de date, Morris vă incurajează să implementați versiunea mai eficientă. Pentru o implementare mai eficientă puteti consulta laboratorul de structuri de date de aici, sau puteți sa citiți mai multe despre algoritm de pe Wikipedia sau infoarena.

Se vor folosi 2 structuri de date pentru implementare :

Algoritmul pentru funcția addElement este următorul :

Algoritmul pentru funcția mergeSetsOf :

Algoritmul pentru funcția getSetOf :

Exemplu : Presupunem că efectuăm următoarele operații : addElement(a), addElement(b), addElement( c), mergeSetsOf(a, b), mergeSetsOf(b, c), getSetOf(a) După cele 3 operații de tipul addElement structurile de date interne arată in felul următor : indexes : (a, 0), (b, 1), (c, 2) elements : (a, 0), (b, 1), (c, 2) După operația mergeSetsOf(a, b) indexes : (a, 0), (b, 0), (c, 2) elements : (a, 0), (b, 0), (c, 2) După operația mergeSetsOf(b, c) indexes : (a, 0), (b, 0), (c, 0) elements : (a, 0), (b, 0), (c, 0)

Analiză(10p)

Pratt nu e foarte convins că ce ați implementat este cu adevărat mai rapid ca o listă simpla, așa că dorește să realizați o analiză a eficienței. Pentru a analiza eficiența veți cronometra cât durează realizarea unui număr mare de interogări pentru o listă de cuvinte și definiții. Pentru a măsura timpul, puteți folosi System.currentTimeMillis() înainte și după rulare. Pratt vă va pune la dispoziție acest test, cât de curând posibil. Cronometarea se va face pentru un MyHashMap cu 1 bucket, 10 bucket-uri, respectiv 100 de bucket-uri. In testele puse la dispoziție se gasește un test (test_big), ce poate fi folosit pentru a observa diferenta de timp dintre rulării.

Ce observaţi? (rǎspundeţi în README).

Mod de testare

Pentru rezolvarea și testarea programului vostru veți implementa clasa Main (clasa trebuie sa se numească Main) cu metoda main. Veți primi ca argumente numărul de bucket-uri din dicționarul vostru, numele fișierului cu lista de cuvinte, numele fișierului de interogări și numele fișierului în care veți răspunde la interogări.

Format fișier de cuvinte

Fișierul unde veți primi cuvinte, impreună cu definițiile lor, și perechile de sinonime, va avea următorul format:

Format fișier de interogări

Fișierul unde veți primi lista de interogării va avea urmatorul format:

Format fișier de output

Fișierul unde veți afișa rezultatele interogărilor va avea următorul format:

Precizǎri generale

Corectare

Tema se va corecta folosind platforma vmchecker. Daca platforma de testare nu acorda niciun punct soluției, atunci acesta va fi punctajul final al implementării temei.

Toate soluțiile vor fi verificate folosind o unealtă de detectare a plagiatului. În cazul detectării unui astfel de caz, atât plagiatorul cât și autorul original vor primi punctaj 0 pe temă și nu li se va permite intrarea în examen!

Punctaj

70p - acordate de vmchecker pentru execuţia cu succes a suitei de teste 10p - acordate de asistent pentru analiza performanţei 10p - acordate de asistent pe baza calitǎţii implementării şi a respectǎrii principiilor POO 10p - acordate de asistent pe baza lizibilitǎţii codului, a calitǎţii comentariilor și javadoc-ului şi a fișierului Readme

Format Arhivă

Arhiva trebuie să conţină:

Resurse

Schelet Tema 3
Teste Tema 3

PDF enunț temă