Tietorakenteet, laskuharjoitus 8, malliratkaisut 1. Seuraavassa on yksi tapa toteuttaa metodit hashcode ja equals: public int hashcode() { return this.x * 31 + this.y; public boolean equals(object o) { if (!(o instanceof Kordinaatti)) return false; Kordinaatti v = (Kordinaatti)o; if (this.x!= v.x) return false; if (this.y!= v.y) return false; return true; 2. Seuraavassa on yksi tapa toteuttaa metodit hashcode ja equals: public int hashcode() { int koodi = this.postinumero; koodi = koodi * 29 + katu.tolowercase().hashcode(); koodi = koodi * 29 + kaupunki.tolowercase().hashcode(); return koodi; public boolean equals(object o) { if (!(o instanceof Osoite)) return false; Osoite v = (Osoite)o; if (this.postinumero!= v.postinumero) return false; if (!this.katu.equalsignorecase(v.katu)) return false; if (!this.kaupunki.equalsignorecase(v.kaupunki)) return false; return true; 3. Ylivuotoketjuihin ja avoimeen hajautukseen perustuvat hajautustaulut toteutettiin yhteisen yliluokan avulla, joka helpotti yhteisen toiminnallisuuden jakamista. Luokalla on mm. edellisessä tehtävässä esitellyt metodit. import java.util.arraylist; import java.math.biginteger; // Yliluokka avoimelle ja ylivuotoketjuja käyttävälle hajautukselle abstract class HajautusTaulu { 1
/* Eri hajautusfunktioiden tyypit */ public enum hajautus {UNIV, KERTO, JAKOJ public static final double MAGIC = 0.6180339887498948; public static final BigInteger BIGPRIME = new BigInteger("2147483647"); protected int koko; protected hajautus haj; //ensisijainen hajautusfunktio //universaalihajautuksen parametrit protected int a; protected int b; public int annakoko() { return this.koko; protected int kerto_haj(int num) { return (int) Math.floor(this.koko*murto_osa(MAGIC*num)); protected int jakoj_haj(int num) { return num % this.koko; protected int universaali_haj(int num) { protected int hash(int avain, hajautus haja) { int arvo = 0; switch(haja) { case JAKOJ: arvo = jakoj_haj(avain); break; case KERTO: arvo = kerto_haj(avain); break; case UNIV: arvo = universaali_haj(avain); break; return arvo; public static double murto_osa(double num) { 2
return num - Math.floor(num); public static ArrayList<Integer> alkuluvut(int max) { public static int alukulahelta(int luku) { public static int stringtoint(string mjono) { Koska Java ei tue etumerkittömiä kokonaislukuja pitää avoimessa hajautuksessa olla hyvin tarkkana törmäysten sattuessa. Jos luku vuotaa yli, niin se pyörähtää ja saa hyvin pienen negatiivisen arvon. Javassa %-operaattori (jakojäännös) voi myös palauttaa negatiivisen arvon, joten lukujen oikeellisuus pitää tarkistaa käsin. Ylivuotoketjuja käyttävän hajautustaulun toteutus on Javassa melko suoraviivaista, koska apuna voidaan käyttää kirjastosta löytyviä valmiita toteutuksia linkitetyille listoille. Hajautustaulujen toteutukset ovat tiedostoissa HajautusTauluA (avoin hajautus) sekä HajautusTauluK (ylivuotoketjuja käyttävä hajautus). 4. Kuhunkin erityyppiseen hajautustauluun lisättiin kaikki Aleksis Kiven Seitsemän veljestä-romaanissa esiintyneet sanat. Kuvassa 1 on esitetty lisäyksiin kulutettu aika. Tarkastelemalla tuloksia havaitaan, että eräs kaksoishajautuksella toteutettu avoin hajautustaulu sattui olemaan erittäin hidas verrattuna muihin. Tämä selittyy luultavasti sillä, että törmäysten yhteensattuessa kaksoishajautuksella saatu kokeilujono on huono. Tämänlainen tilanne syntyy esimerkiksi silloin, jos n:n mittaisessa taulukossa kaksoishajautusfunktio palauttaa arvon n/2. Testeissä avointa hajautusta kokeiltiin myös 30000:n kokoisella taulukolla, mutta ne eivät mahtuneet kuvaan koska suoritus kesti niillä 10-30 kertaa enemmän kuin kuvassa esitetyillä tauluilla. Tämä johtuu siitä, että tällöin kokeilujonot ovat hyvin pitkiä. Kuvassa 2 on esitetty keskimääräinen ylivuotoketjun pituus kun käytetään ylivuotoketjuja. Empiiriset tulokset vahvistavat, että ylivuotoketjut kasvavat taulukon koon pienetessä. Aineistossa oli noin 30000 eri sanaa, joten hajautusfunktiot ovat onnistuneet jakamaan avaimet keskimäärin melko hyvin. 3
Lisäyksiin kulunut aika 135.5 144.7 111.6 112.9 107.5 105.5 111.2 110.7 110.3 111.7 103.9 101.3 121.5 423.5 118.6 117 116.1 112.2 109.4 159.4 144.9 140.2 123.9 117.9 114.3 115.6 110.7 106.6 153.6 106.7 Avoin: universaali, lineaarinen (40000) Avoin: universaali, lineaarinen (50000) Avoin: kertolasku, lineaarinen (40000) Avoin: kertolasku, lineaarinen (50000) Avoin: jakojäännös, lineaarinen (40009) Avoin: jakojäännös, lineaarinen (49999) Avoin: universaali, neliöllinen (40000) Avoin: universaali, neliöllinen (50000) Avoin: kertolasku, neliöllinen (40000) Avoin: kertolasku, neliöllinen (50000) Avoin: jakojäännös, neliöllinen (40009) Avoin: jakojäännös, neliöllinen (49999) Avoin: universaali, kertolasku (40000) Avoin: universaali, kertolasku (50000) Avoin: universaali, kertolasku (50001) Avoin: kertolasku, universaali (40000) Avoin: kertolasku, universaali (50000) Avoin: jakojäännös, kertolasku (40009) Avoin: jakojäännös, kertolasku (49999) Ylivuotokeju: universaali (5000) Ylivuotokeju: universaali (10000) Ylivuotokeju: universaali (15000) Ylivuotokeju: kertolasku (5000) Ylivuotokeju: kertolasku (10000) Ylivuotokeju: kertolasku (15000) Ylivuotokeju: jakojäännös (4999) Ylivuotokeju: jakojäännös (10007) Ylivuotokeju: jakojäännös (15013) TreeSet HashSet 0 100 200 300 400 ms Kuva 1: Lisäyksiin kulunut aika 4
Ylivuotoketjujen keskimääräiset pituudet 6.2113 3.255 2.3734 6.2128 3.2512 2.3734 Ylivuotokeju: universaali (5000) Ylivuotokeju: universaali (10000) Ylivuotokeju: universaali (15000) Ylivuotokeju: kertolasku (5000) Ylivuotokeju: kertolasku (10000) Ylivuotokeju: kertolasku (15000) Ylivuotokeju: jakojäännös (4999) Ylivuotokeju: jakojäännös (10007) Ylivuotokeju: jakojäännös (15013) 6.2079 3.2539 2.3705 0 1 2 3 4 5 6 ylivuotoketjun keskimääräinen pituus Kuva 2: Ketjujen keskimääräiset pituudet käytettäessä ylivuotoketjuja 5
5. Edellisessä tehtävässä oli jo vertailtu Javan valmista kalustoa (HashSet, TreeSet) lisäysten tapauksessa. Kuvassa 3 on esitetty hakuihin kulunut aika, kun Seitsemän veljesten sanojen joukosta koitettiin hakea jokaista Dostojevskin Rikos ja Rangaistusromaanin englanninkielisessä versiossa esiintyvää sanaa. Ensimmäiseksi havaitaan, että hakuun kului huomattavasti vähemmän aikaa kuin lisäykseen. Tämä johtuu siitä, että haun yhteydessä ei tarvi jatkuvasti kopioida merkkijonoja talteen. Tuloksissa ei näy silmiinpistäviä eroja. Ehkä kiinnostavin tulos on, että Javan HashSet tuntuisi olevan ainakin lisäyksissä sekä hauissa nopampi kuin TreeSet kun talletettavana on merkkijonoja. Hakuihin kulunut aika 21.3 18 15.5 16 18.7 14.9 16 14.7 16 14.4 15 13.6 22.6 179.4 17.1 17.6 15.6 15.4 14.3 18.4 15.9 16.1 18.4 16.4 15.9 18.5 15.8 15.5 16.6 13.4 Avoin: universaali, lineaarinen (40000) Avoin: universaali, lineaarinen (50000) Avoin: kertolasku, lineaarinen (40000) Avoin: kertolasku, lineaarinen (50000) Avoin: jakojäännös, lineaarinen (40009) Avoin: jakojäännös, lineaarinen (49999) Avoin: universaali, neliöllinen (40000) Avoin: universaali, neliöllinen (50000) Avoin: kertolasku, neliöllinen (40000) Avoin: kertolasku, neliöllinen (50000) Avoin: jakojäännös, neliöllinen (40009) Avoin: jakojäännös, neliöllinen (49999) Avoin: universaali, kertolasku (40000) Avoin: universaali, kertolasku (50000) Avoin: universaali, kertolasku (50001) Avoin: kertolasku, universaali (40000) Avoin: kertolasku, universaali (50000) Avoin: jakojäännös, kertolasku (40009) Avoin: jakojäännös, kertolasku (49999) Ylivuotokeju: universaali (5000) Ylivuotokeju: universaali (10000) Ylivuotokeju: universaali (15000) Ylivuotokeju: kertolasku (5000) Ylivuotokeju: kertolasku (10000) Ylivuotokeju: kertolasku (15000) Ylivuotokeju: jakojäännös (4999) Ylivuotokeju: jakojäännös (10007) Ylivuotokeju: jakojäännös (15013) TreeSet HashSet 0 50 100 150 ms Kuva 3: Hakuihin kulunut aika Iteraattoreita voidaan käyttää Javassa kahdella eri tavalla. Ensimmäisessä tavassa niitä käytetään suoraan Iterator-olion välityksellä. Seuraava koodinpätkä käy läpi TreeSet<String>-tyyppiä olevan puu-muuttujan avaimet kasvavassa järjestyksessä. import java.util.iterator Iterator<String> iter = puu.iterator(); while(iter.hasnext()) 6
System.out.println(iter.next()); Jos Javan olio toteuttaa Iterable-rajapinnan niin iteraattoria voidaan käyttää myös niin kutsutun for-each-silmukan avulla. Seuraava koodi tekee tismalleen saman kuin edellinenkin. //ei tarvitse importata Iterator-luokkaa for(string mjono : puu) System.out.println(mjono); Koska Javan TreeSet on järjestetty joukko niin iteraattori käy avaimet läpi kasvavassa järjestyksessä. Jos sama tehdään HashSet-oliolle niin avaimet tulostuvat mielivaltaisessa järjestyksessä. Kirjojen yhteisten sanojen tulostus ainoastaan kerran voitaisiin suorittaa käyttämällä kahta hajautustaulua. Toiseen talletettaisiin Seitsemässä veljeksessä esiintyvät sanat ja toiseen talletettaisiin jo kertaalleen tulostetut sanat. 7