Kierros 5: Hajautus Tommi Junttila Aalto University School of Science Department of Computer Science CS-A1140 Data Structures and Algorithms Autumn 017 Tommi Junttila (Aalto University) Kierros 5 CS-A1140 / Autumn 017 1 / 47
Materiaali kirjassa Introduction to Algorithms, 3rd ed. (online via Aalto lib): Kappaleet 11.1 11.4 Vastaavaa materiaalia muualla: Linkkejä: kappale 3.4 kirjassa Algorithms, 4th ed. ja nämä kalvot (neliöinen kokeilu ei kirjassa) Kappale hashing OpenDSA-kirjassa MIT 6.006 OCW video on hashing with chaining MIT 6.006 OCW video on open addressing and cryptographic hashing Tommi Junttila (Aalto University) Kierros 5 CS-A1140 / Autumn 017 / 47
Monessa sovelluksessa joukoille ja kuvauksille riittää rajapinnan INSERT, SEARCH ja DELETE tehokas toteuttaminen Hajautuksen avulla yllä mainitut saadaan toimimaan keskimäärin vakioajassa pahimman tapauksen aikavaatimus on Θ(n), missä n on joukon/kuvauksen senhetkinen avainten lukumäärä, mutta hyvällä suunnittelulla tämä on erittäin epätodennäköistä pienimmän ja suurimman avaimen löytäminen jne ovat kuitenkin lineaariaikaisia operaatioita Toteutuksia: C++11-standardikirjastossa unordered set ja unordered map Javassa HashSet ja HashMap Scalassa HashSet ja HashMap... Tommi Junttila (Aalto University) Kierros 5 CS-A1140 / Autumn 017 3 / 47
Johdanto: pienet avainjoukot, bittivektorit ja suorahakutaulukot Tommi Junttila (Aalto University) Kierros 5 CS-A1140 / Autumn 017 4 / 47
Bittivektorit Oletetaan ensin, että mahdollisten avainten määrä n on pieni ja ne ovat tai voidaan helposti kuvata kokonaisluvuiksi 0,..., n 1 Joukkotietotyyppi on helppo toteuttaa bittivektorina: Esimerkki Varataan m = n 8 -tavua pitkä taulukko a = a 0a 1...n m 1 Avain k {0,...,n 1} on joukossa jos ja vain jos tavun a k/8 bitti k mod 8 on 1 Alla kuvattu taulukko esittää avainjoukon {0, 1,..., 999} osajoukkoa, joka sisältää mm. luvut 0 8 + 1 = 1, 0 8 + 4 = 4, 100 8 + 6 = 806, 14 8 + 7 = 999 mutta ei mm. lukua 100 8 + 5 = 805. a bit byte 00010010 7654310 0 01000000 7654310 100 10000000 7654310 14 Tommi Junttila (Aalto University) Kierros 5 CS-A1140 / Autumn 017 5 / 47
Vakioaikaiset INSERT, SEARCH ja DELETE todella helppo toteuttaa Tiivis esitysmuoto tiheille joukoille Harvoilla joukoilla alkoiden listaus jne raskasta Joitain toteutuksia BitSet Scalassa bitset C++-kielen standardikirjastossa Tommi Junttila (Aalto University) Kierros 5 CS-A1140 / Autumn 017 6 / 47
Suorahakutaulukot Oletetaan, että mahdollisten avainten joukko on pieni Esimerkiksi ISO 3166-1 alpha--standardissa maakoodit muodostetaan kahdesta aakkoston {A, B,..., Z} 6 kirjaimesta ja mahdollisia koodeja on siis 6 6 = 676, joista 49, kuten FI ja UK, ovat käytössä Voidaan helposti toteuttaa kuvaus koodeilta olioille (esim. pääkaupunkien nimille) tekemällä suorahakutaulukko, jossa on 676 alkiota Koodiin c 1 c assosioitu arvo v talletetaan taulukon alkioon kohdassa index(c 1 c ) = f(c 1 ) 6 + f(c ), missä f(a) = 0,f(B) = 1,...,f(Z) = 5 INSERT, SEARCH ja DELETE on nyt helppoa toteuttaa toimimaan vakioajassa Tommi Junttila (Aalto University) Kierros 5 CS-A1140 / Autumn 017 7 / 47
Esimerkki Maakoodien indeksointi suorahakutaulukkossa arr: AA AB AC DE FI UK US ZZ 0 1 8 138 530 538 675 arr..... Tommi Junttila (Aalto University) Kierros 5 CS-A1140 / Autumn 017 8 / 47
Maakoodikuvausten toteuttaminen Scala-kielellä: import scala. r e f l e c t. ClassTag class CountryMap [ B >: N u l l ] ( ) ( i m p l i c i t tag : ClassTag [ B ] ) { private val a r r = new Array [ B] ( 6 7 6 ) private def f ( c : Char ) = c. t o I n t A. t o I n t private def isvalidcode ( code : S t r i n g ) = code. length == && 0 <= f ( code ( 0 ) ) && f ( code ( 0 ) ) < 6 && 0 <= f ( code ( 1 ) ) && f ( code ( 1 ) ) < 6 def index ( code : S t r i n g ) : I n t = { require ( isvalidcode ( code ) ) } f ( code ( 0 ) ) * 6 + f ( code ( 1 ) ) def apply ( code : S t r i n g ) : Option [ B ] = { val v = a r r ( index ( code ) ) i f ( v == null ) None else Some( v ) } def update ( code : String, value : B) = { r e q u i r e ( value!= null ) a r r ( index ( code ) ) = value } def d elete ( code : S t r i n g ) = { a r r ( index ( code ) ) = null } } Tommi Junttila (Aalto University) Kierros 5 CS-A1140 / Autumn 017 9 / 47
Huom: null-arvon käyttöä tulee yleisesti ottaen välttää Scala-kielessä (käytetään Option-luokkaa) mutta anteeksiannettavissa tietorakenteiden sisäisissä toteutuksissa Luokan laajentaminen vakioajassa toimivalla size-metodilla on helppoa Esimerkki Pääkaupunki nimien assosiointi maakoodeihin: val c a p i t a l = new CountryMap [ S t r i n g ] ( ) c a p i t a l ( DE ) = B e r l i n c a p i t a l ( FI ) = H e l s i n k i c a p i t a l ( UK ) = London c a p i t a l ( US ) = Washington p r i n t l n ( c a p i t a l ( FI ) ) tuottaa Some( H e l s i n k i ) AA AB AC DE FI arr 0 1. 8. 138 Berlin Helsinki UK 530. London US 538. Washington ZZ 675. Tommi Junttila (Aalto University) Kierros 5 CS-A1140 / Autumn 017 10 / 47
Hajautus ja hajautustaulukot Tommi Junttila (Aalto University) Kierros 5 CS-A1140 / Autumn 017 11 / 47
Hajautus ja hajautustaulukot Laajennetaan suorahakutaulukoiden ideaa suurille (tai jopa äärettömille) avainjoukoille U Kunakin ajanhetkenä vain osajoukko K U mahdollisista avaimista on käytössä Perusideana on varata m-alkioinen hajautustaulukko ja käyttää hajautusfunktiota h : U {0,1,...,m 1} kuvaamaan avaimet taulukon indekseille Ideaalitapauksessa jokainen käytössä oleva avain kuvautuisi eri indeksille... mutta yleisesti ottaen tämä on vaikeaa toteuttaa tehokkaasti ja täten tapahtuu yhteentörmäyksiä (engl. collision) kun kaksi avainta kuvautuu samalle indeksille Palataan hajautusfunktioiden suunnitteluun myöhemmin Tommi Junttila (Aalto University) Kierros 5 CS-A1140 / Autumn 017 1 / 47
Esimerkki Oletetaan, että käytössä on hajautustaulukko, jossa on tilaa m = 13 alkiolle ja hajautusfunktio merkkijonoille, joka on toteutettu Scalalla (.11.8) seuraavasti: def h(s: String) = math.abs(s.hashcode) % 13 Monet merkkojonot kuvautuvat eri indekseille ja perusidea toimii ongelmitta Germany Finland United States Denmark United Kingdom Sweden 0 1 3 4 5 6 7 8 9 10 11 1 arr Berlin Helsinki Washington Copenhagen London Stockholm Mutta jotkin merkkijonot kuvautuvat samoille indekseille ja tuloksena on yhteentörmäyksiä Germany Finland United States Denmark Austria United Kingdom Sweden 0 1 3 4 5 6 7 8 9 10 11 1 arr Berlin Helsinki Washington London Stockholm Tommi Junttila (Aalto University) Kierros 5 CS-A1140 / Autumn 017 13 / 47
Kuinka todennäköisiä yhteentörmäykset ovat? Oletetaan hajautusfunktio h, jossa satunnaisella avaimella on sama todennäköisyys kuvautua mille tahansa indeksille {0, 1,..., m 1} toisistaan riippumatta (engl. simple uniform hashing assumption) Tällöin pitää lisätä ( ) 1 m ln 1 p avainta, jotta tapahtuu todennäköisyydellä p vähintään yksi yhteentörmäys Esimerkiksi hajautustauluun, jonka koko on m = 1000 000, tarvitsee lisätä vain 1178 satunnaista avainta jotta saadaan todennäköisyydellä 0.5 vähintään yksi yhteentörmäys Eli yhteentörmäykset ovat yleisiä Yllä olevan erikoistapaus on niin sanottu syntymäpäiväongelma (engl. birthday paradox): vuodessa on 365 päivää mutta jos samaan tilaan kokoontuu 3 satunnaisesti valittua henkilöä, niin todennäköisyys sille, että vähintään kahdella näistä on sama syntymäpäivä on vähintään 0.5 (olettaen, että syntymäpäivät ovat tasaisesti jakautuneet vuoden jokaiselle päivälle) Tommi Junttila (Aalto University) Kierros 5 CS-A1140 / Autumn 017 14 / 47
Yhteentörmäysten käsittely Tommi Junttila (Aalto University) Kierros 5 CS-A1140 / Autumn 017 15 / 47
Vääjäämättä tapahtuvien yhteentörmäysten käsittelyyn tarvitaan jokin strategia Seuraavassa tarkastellaan ketjutusta ja avointa osoittamista Tarvitaan yksi lisämäärittely: jos hajautustaulukkoon, jonka koko on m, on talletettu n avainta, niin taukon täyttösuhde (engl. load factor) on α = n/m Tommi Junttila (Aalto University) Kierros 5 CS-A1140 / Autumn 017 16 / 47
Ketjutus Myös avoin hajautus ; (engl. chaining, open hashing) Perusidea: hajautustaulukon jokaisesta indeksistä alkaa linkitetty lista, johon siihen indeksiin kuvautuvat avaimet (avain/arvo-parit) tallennetaan Indeksin h(k) laskemisen jälkeen jatko on kuten muuttuvatilaisilla linkitetyillä listoilla: SEARCH: etsitään avainta k listasta alkio kerrallaan INSERT: etsitään avainta listasta ja jos sitä ei löydy, lisätään se listan alkuun/loppuun lisäämäällä uusi lista-alkio DELETE: käydään listaa läpi ja poistetaan listan alkio, mikäli se sisältää poistettavan avaimen k Tommi Junttila (Aalto University) Kierros 5 CS-A1140 / Autumn 017 17 / 47
Esimerkki: Yhteentörmäysten käsittely ketjutuksella Listojen alkiot ovat muotoa key value next entry, missä key on avain (tai viittaus siihen) value on avaimeen liitetty arvo (tai viittaus siihen) ja next entry on viittaus listan seuraavaan alkioon (null jos viimeinen) arr Germany Finland 0 1 Germany Berlin Finland Helsinki United States 3 United States Washington 4 5 6 7 Denmark Austria 8 9 Denmark Copenhagen Austria Vienna United Kingdom 10 United Kingdom London Sweden 11 Sweden Stockholm 1 Tommi Junttila (Aalto University) Kierros 5 CS-A1140 / Autumn 017 18 / 47
Esimerkki: Joukkojen toteutus hajautuksella ja ketjutuksella Jos ei olla kiinnostuttu kuvauksista vaan joukoista, jätetään vain kenttä value pois ja listan alkiot ovat muotoa key next entry. Lisätään luvut 131, 9833, 344, 6, 17, 434, 653 ja -13 alunperin tyhjään hajautustaulukkoon käyttäen hajautusfunktiota h(k) = k mod m: 0 1 arr 6-13 3 4 5 6 344 653 17 9833 7 8 9 10 131 434 11 1 Tommi Junttila (Aalto University) Kierros 5 CS-A1140 / Autumn 017 19 / 47
unordered sets C++11-standardikirjastossa GNU ISO C++ -kirjasto toteuttaa järjestämättömät joukot ketjutuksella Hajautusfunktiot ja avainten yhtäsuuruus toteutettu valmiiksi perustyypeille # i n c l u d e <iostream> # i n c l u d e <unordered set> i n t main ( ) { / / A set of small prime numbers std : : unordered set<i n t> myset = {3,5,7,11,13,17,19,3,9}; myset. erase ( 1 3 ) ; / / erasing by key myset. erase ( myset. begin ( ) ) ; / / erasing by i t e r a t o r std : : cout << myset contains : ; f o r ( const i n t & x : myset ) std : : cout << << x ; std : : cout << std : : endl ; r e t u r n 0; } Eräs mahdollinen lopputulos (huomaa järjestämättömyys): myset contains : 19 17 11 9 7 5 3 Vastaavasti unordered multisets, maps ja multimaps Tommi Junttila (Aalto University) Kierros 5 CS-A1140 / Autumn 017 0 / 47
Analyysiä Olkoon lisätty n satunaista avainta hajautustaulukkoon, jossa on m alkiota Oletetaan kuten aiemminkin, että hajautusfunktio jakaa avaimet tasaisesti ja toisistaan riippumattomasti eri indekseille Hajautustaulukon indeksistä i alkavassa listassa on nyt keskimäärin n m avainta Oletetaan, että arvon h(k) laskenta voidaan tehdä vakioajassa Avaimen etsimiseen listasta kuluu aikaa keskimäärin O(1 + n ), eli m O(1 + α), sekä silloin kun avain löytyy että kun sitä ei löydy Jos avainten määrä n on suoraan verrannollinen taulukon kokoon m, eli n cm jollekin vakiolle c, niin α c ja avaimen etsintä vie keskimäärin ajan O(1 + cm m ) = O(1) Jos/kun avainten lisääminen ja poistaminen etsivät ensin avainta listasta, myös niiden aikavaatimus on sama Tommi Junttila (Aalto University) Kierros 5 CS-A1140 / Autumn 017 1 / 47
Pahimman tapauksen aikavaatimus toteutuu kun kaikki avaimet (tai suuri osa) kuvautuvat samalle indeksille: hajautustaulu muuntuu käytännössä linkitetyksi listaksi ja avainten etsiminen jne vaatii lineaarisen ajan hyvien hajautusfunktioiden suunnittelu on tärkeää Tommi Junttila (Aalto University) Kierros 5 CS-A1140 / Autumn 017 / 47
Uuudelleenhajautus Kuinka suuri taulukko pitäisi varata alussa kun ei välttämättä tiedetä, kuinka monta avainta siihen tullaan tallentamaan myöhemmin? Tai mitä tehdään, kun taulukon täyttösuhde kasvaa liian suureksi? Vastaus on uudelleenhajautus: aloitetaan pienehköllä taulukolla ja kasvatetaan sen kokoa aina kun täyttösuhde kasvaa jotain tiettyä raja-arvoa suuremmaksi Kun taulukon kokoa kasvatetaan, kaikki vanhan taulukon avaimet (tai avain/arvo-parit) täytyy lisätä uuteen taulokkoon koska niiden indeksit suuremmassa taulukossa ovat luultavasti erilaiset kuin vanhassa Mikä on hyvä raja-arvo uudelleenhajautuksen käynnistämiseksi? Ei ole yhtä parasta vastausta; GNU ISO C++ -kirjaston versio 4.6 tekee oletuksena uudelleenhajautuksen kun täyttösuhde kasvaa yli arvon 1,0 ja kasvattaa tällöin taulukon koon kaksinkertaiseksi Tommi Junttila (Aalto University) Kierros 5 CS-A1140 / Autumn 017 3 / 47
Avoin osoittaminen Myös suljettu hajauttaminen (engl. open addressing, closed hashing) Toinen törmäyksien käsittelymenetelmä Idea: taulukko pidetään niin suurena, että kaikki lisätyt avaimet mahtuvat siihen Jokainen taulukon alkio sisältää yhden avaimen (tai avain/arvo-parin) tai on null tms jos se on vapaa Eli täyttösuhde on korkeintaan 1,0 avoimessa osoittamisessa Kun avainta lisättäessä tapahtuu törmäys, kokeillaan (engl. probe) jotain toista taulukon indeksiä kunnes vapaa paikka löytyy Samoin avainta etsittäessä kokeillaan indeksejä kunnes avain löytyy tai löytyy tyhjä alkio Tommi Junttila (Aalto University) Kierros 5 CS-A1140 / Autumn 017 4 / 47
Kokeilemista varten täytty määrittää, mitä indeksiä kokeillaan seuraavaksi Tätä varten määritellään hajautusfunktio muotoon h : U {0,1,...,m 1} {0,1,...m 1} missä toinen argumentti kertoo kokeilun järjestysnumeron Jokaiselle avaimelle k saadaan siten kokeilujono h(k,0),h(k,1),...,h(k,m 1) Jotta kokeiltaisiin jokaista indeksiä vuorollaan, tulee kokeilujonon olla joukon {0, 1,..., m 1} permutaatio jokaiselle avaimelle k Tommi Junttila (Aalto University) Kierros 5 CS-A1140 / Autumn 017 5 / 47
Lineaarinen kokeilu Yksinkertaisin tapa muodostaa kokeilujonoja Olkoon apuhajautusfunktio h : U {0,1,...,m 1} h on yleensä avaimen luokan tai käyttäjän määrittelemä alkuperäinen hajautusfunktio (esim. hashcode Scala-kielessä) Määritellään nyt hajautusfunktio Selvästikin kokeilujono h(k,i) = (h (k) + i) mod m h(k,0),h(k,1),...,h(k,m 1) on nyt joukon {0,1,...,m 1} permutaatio Tommi Junttila (Aalto University) Kierros 5 CS-A1140 / Autumn 017 6 / 47
Esimerkki: Törmäysten käsittely lineaarisella kokeilulla Taulukon alkiot ovat muotoa key value, missä jälleen key on avain (tai viittaus siihen) ja value on avaimeen liitetty arvo (tai viittaus siihen). Lisätään kuvaukset Finland Helsinki United States Washington United Kingdom London Denmark Copenhagen Austria Vienna Sweden Stockholm tässä järjestyksessä taulukkoon, jonka koko on 13, käyttämällä aiemmin esitettyä hajautusfunktiota ja lineaarista kokeilua Tulos on oikealla Lisättäessä kuvaus Austria Vienna huomataan indeksin 8 olevan jo varattu avaimelle Denmark. Kokeillaan seuraavaa, ja koska se oli vapaa, lisätään avain/arvo-pari siihen. 0 1 3 4 5 6 7 8 9 10 11 1 arr Germany Berlin Finland Helsinki United States Washington Denmark Copenhagen Austria Vienna United Kingdom London Sweden Stockholm Tommi Junttila (Aalto University) Kierros 5 CS-A1140 / Autumn 017 7 / 47
Esimerkki: Joukot hajautuksella ja lineaarisella kokeilulla Jos toteutetaan joukkoja, taulukon alkiot ovat vain avaimia (tai viittauksia niihin). Lisätään kokonaislukuavaimet 131, 9833, 344, 6, 17, 434, 653 ja -13 käyttämällä apuhajautusfunktiota h (k) = k: 1 131 lisätään indeksiin h(131,0) = (131 + 0) mod 13 = 9 9833 lisätään kohtaan h(9833,0) = (9833 + 0) mod 13 = 5 3 344 lisätään kohtaan h(344,0) = (344 + 0) mod 13 = 3 4 6 lisätään kohtaan h(6,0) = (6 + 0) mod 13 = 0 5 17 lisätään kohtaan h(17,0) = (17 + 0) mod 13 = 4 6 koska indeksi h(434,0) = (434 + 0) mod 13 = 9 on varattu, kokeillaan indeksiä h(434,1) = (434 + 1) mod 13 = 10 ja lisätään 434 siihen 7 koska indeksi h(653,0) = (653 + 0) mod 13 = 3 kuten myös kaksi seuraavaa h(653,1) = 4 ja h(653,) = 5 ovat varattuja, lisätään 653 kohtaan h(653,3) = 6 8 koska h( 13,0) = 0 on varattu, avain 13 lisätään kohtaan h( 13,1) = 1 0 1 3 344 4 5 6 7 8 9 10 434 11 1 arr 6-13 17 9833 653 131 Tommi Junttila (Aalto University) Kierros 5 CS-A1140 / Autumn 017 8 / 47
Seuraavissa esimerkeissä tarkastellaan esityksen yksinkertaisuuden vuoksi vain kokonaislukujoukkoja yleistys muille avaintyypeille ja kuvauksille on suoraviivaista Lineaarinen kokeilu kärsii ilmiöstä nimeltä primäärinen kasautuminen (engl. primary clustering): Esimerkki vapaa indeksi, jonka edeltäjistä i on varattuja, tulee täytetyksi todennäköisyydellä (i + 1)/m eikä 1/m, ja täten varatut indeksit alkavat kasautumaan Olkoon h (k) = k kokonaislukuavaimille ja m = 17. Lisätään avaimet 1, 50,, 0, 38 ja 35 tässä järjestyksessä lineaarisella kokeilulla ja saadaan 0 1 1 50 3 4 0 5 38 6 7 8 9 10 11 1 13 14 15 16 35 missä nuolet visualisoivat avaimen 35 kokeilujonon. Huomaa, että vain avaimet 1, 50 ja 35 kuvautuvat samalle indeksille. Tommi Junttila (Aalto University) Kierros 5 CS-A1140 / Autumn 017 9 / 47
Neliöinen kokeilu Päästään eroon primäärisestä kasautumisesta käyttämällä monimutkaisempaa kokeilujonoa Määritellään h(k,i) = (h (k) + c 1 i + c i ) mod m missä c 1 ja c ovat positiivisia vakioita Jotta kokeilujonot olisivat joukon {0, 1,..., m 1} permutaatioita, tulee arvojen c 1, c ja m olla hyvin valittuja Esimerkki Olkoon m = 11 (eli alkuluku) ja h(k,i) = (h (k) + i + i ) mod m Jos h (k) = 0, niin kokeilujono 0,,6,1,9,8,9,1,6,,0 ei ole joukon {0, 1,..., 10} permutaatio vaan kokeilee vain 6 indeksiä 11 mahdollisesta. Jos taulukon täyttösuhde on suurehko, tämä kokeilujono ei välttämättä löydä vapaita indeksejä. Tommi Junttila (Aalto University) Kierros 5 CS-A1140 / Autumn 017 30 / 47
Esimerkki Olkoon m jokin kahden potenssi. Tässä tapauksessa voidaan todistaa, että hajautusfunktio h(k,i) = (h i(i + 1) (k) + ) mod m = (h (k) + 0.5i + 0.5i ) mod m tuottaa kokeilujonoja, jotka ovat joukon {0, 1,..., m} permutaatioita. Esimerkiksi jos m = 3 = 8 ja h (k) = 0, niin kokeilujono on 0,1,3,6,,7,5,4. Esimerkki Olkoon h (k) = k kokonaislukuavaimille ja m = 16. Lisätään avaimet 1, 50,, 0, 38 ja 35 tässä järjestyksessä käyttämällä neliöistä kokeilua ja hajautusfunktiota h(k) = (h (k) + i(i+1) ) mod m. Nuolet kuvaavat jälleen kokeilujonoja. 0 1 3 4 5 6 7 8 9 10 11 1 13 14 15 Tommi Junttila (Aalto University) Kierros 5 CS-A1140 / Autumn 017 31 / 47
Esimerkki Olkoon m jokin kahden potenssi. Tässä tapauksessa voidaan todistaa, että hajautusfunktio h(k,i) = (h i(i + 1) (k) + ) mod m = (h (k) + 0.5i + 0.5i ) mod m tuottaa kokeilujonoja, jotka ovat joukon {0, 1,..., m} permutaatioita. Esimerkiksi jos m = 3 = 8 ja h (k) = 0, niin kokeilujono on 0,1,3,6,,7,5,4. Esimerkki Olkoon h (k) = k kokonaislukuavaimille ja m = 16. Lisätään avaimet 1, 50,, 0, 38 ja 35 tässä järjestyksessä käyttämällä neliöistä kokeilua ja hajautusfunktiota h(k) = (h (k) + i(i+1) ) mod m. Nuolet kuvaavat jälleen kokeilujonoja. 0 1 3 4 5 6 7 8 9 10 11 1 13 14 15 1 Tommi Junttila (Aalto University) Kierros 5 CS-A1140 / Autumn 017 31 / 47
Esimerkki Olkoon m jokin kahden potenssi. Tässä tapauksessa voidaan todistaa, että hajautusfunktio h(k,i) = (h i(i + 1) (k) + ) mod m = (h (k) + 0.5i + 0.5i ) mod m tuottaa kokeilujonoja, jotka ovat joukon {0, 1,..., m} permutaatioita. Esimerkiksi jos m = 3 = 8 ja h (k) = 0, niin kokeilujono on 0,1,3,6,,7,5,4. Esimerkki Olkoon h (k) = k kokonaislukuavaimille ja m = 16. Lisätään avaimet 1, 50,, 0, 38 ja 35 tässä järjestyksessä käyttämällä neliöistä kokeilua ja hajautusfunktiota h(k) = (h (k) + i(i+1) ) mod m. Nuolet kuvaavat jälleen kokeilujonoja. 0 1 1 3 4 5 6 7 8 9 10 11 1 13 14 15 50 Tommi Junttila (Aalto University) Kierros 5 CS-A1140 / Autumn 017 31 / 47
Esimerkki Olkoon m jokin kahden potenssi. Tässä tapauksessa voidaan todistaa, että hajautusfunktio h(k,i) = (h i(i + 1) (k) + ) mod m = (h (k) + 0.5i + 0.5i ) mod m tuottaa kokeilujonoja, jotka ovat joukon {0, 1,..., m} permutaatioita. Esimerkiksi jos m = 3 = 8 ja h (k) = 0, niin kokeilujono on 0,1,3,6,,7,5,4. Esimerkki Olkoon h (k) = k kokonaislukuavaimille ja m = 16. Lisätään avaimet 1, 50,, 0, 38 ja 35 tässä järjestyksessä käyttämällä neliöistä kokeilua ja hajautusfunktiota h(k) = (h (k) + i(i+1) ) mod m. Nuolet kuvaavat jälleen kokeilujonoja. 0 1 1 50 3 4 5 6 7 8 9 10 11 1 13 14 15 Tommi Junttila (Aalto University) Kierros 5 CS-A1140 / Autumn 017 31 / 47
Esimerkki Olkoon m jokin kahden potenssi. Tässä tapauksessa voidaan todistaa, että hajautusfunktio h(k,i) = (h i(i + 1) (k) + ) mod m = (h (k) + 0.5i + 0.5i ) mod m tuottaa kokeilujonoja, jotka ovat joukon {0, 1,..., m} permutaatioita. Esimerkiksi jos m = 3 = 8 ja h (k) = 0, niin kokeilujono on 0,1,3,6,,7,5,4. Esimerkki Olkoon h (k) = k kokonaislukuavaimille ja m = 16. Lisätään avaimet 1, 50,, 0, 38 ja 35 tässä järjestyksessä käyttämällä neliöistä kokeilua ja hajautusfunktiota h(k) = (h (k) + i(i+1) ) mod m. Nuolet kuvaavat jälleen kokeilujonoja. 0 1 1 50 3 4 5 6 7 8 9 10 11 1 13 14 15 0 Tommi Junttila (Aalto University) Kierros 5 CS-A1140 / Autumn 017 31 / 47
Esimerkki Olkoon m jokin kahden potenssi. Tässä tapauksessa voidaan todistaa, että hajautusfunktio h(k,i) = (h i(i + 1) (k) + ) mod m = (h (k) + 0.5i + 0.5i ) mod m tuottaa kokeilujonoja, jotka ovat joukon {0, 1,..., m} permutaatioita. Esimerkiksi jos m = 3 = 8 ja h (k) = 0, niin kokeilujono on 0,1,3,6,,7,5,4. Esimerkki Olkoon h (k) = k kokonaislukuavaimille ja m = 16. Lisätään avaimet 1, 50,, 0, 38 ja 35 tässä järjestyksessä käyttämällä neliöistä kokeilua ja hajautusfunktiota h(k) = (h (k) + i(i+1) ) mod m. Nuolet kuvaavat jälleen kokeilujonoja. 0 1 1 50 3 4 5 0 6 7 8 9 10 11 1 13 14 15 38 Tommi Junttila (Aalto University) Kierros 5 CS-A1140 / Autumn 017 31 / 47
Esimerkki Olkoon m jokin kahden potenssi. Tässä tapauksessa voidaan todistaa, että hajautusfunktio h(k,i) = (h i(i + 1) (k) + ) mod m = (h (k) + 0.5i + 0.5i ) mod m tuottaa kokeilujonoja, jotka ovat joukon {0, 1,..., m} permutaatioita. Esimerkiksi jos m = 3 = 8 ja h (k) = 0, niin kokeilujono on 0,1,3,6,,7,5,4. Esimerkki Olkoon h (k) = k kokonaislukuavaimille ja m = 16. Lisätään avaimet 1, 50,, 0, 38 ja 35 tässä järjestyksessä käyttämällä neliöistä kokeilua ja hajautusfunktiota h(k) = (h (k) + i(i+1) ) mod m. Nuolet kuvaavat jälleen kokeilujonoja. 0 1 1 50 3 4 5 0 6 7 8 38 9 10 11 1 13 14 15 35 Tommi Junttila (Aalto University) Kierros 5 CS-A1140 / Autumn 017 31 / 47
Neliöllinen kokeilu ei kärsi primäärisestä kasautumisesta Mutta edelleen kahdella avaimella k 1 ja k, joilla on sama hajautusarvo h (k 1 ) = h (k ), on myös sama kokeilujono Tämä (vähemmän haitallinen) ongelma on nimeltään sekundäärinen kasautuminen Yleisemminkin lineaarinen ja neliöinen kokeilu ovat epäoptimaalisia koska m-alkioisille hajautustaulukoille on olemassa m! erilaista kokeilujonoa mutta lineaarinen ja neliöinen kokeilu tuottavat vain m kokeilujonoa (kun apuhajautusfunktio h ja vakiot ovat kiinnitettyjä) Tommi Junttila (Aalto University) Kierros 5 CS-A1140 / Autumn 017 3 / 47
Kaksoishajautus Tuottaa enemmän kokeilujonoja käyttämällä kahta apuhajautusfunktiota Yleinen muoto on h(k,i) = (h 1 (k) + i h (k)) mod m Kaksoishajauttaminen voi tuottaa m erilaista kokeilujonoa Jotta kokeilujonot olisivat permutaatioita, täytyy funktiolla h olla tiettyjä ominaisuuksia Vaaditaan, että arvoilla h (k) ei saa olla yhteisiä tekijöitä taulukon koon m kanssa (engl. relatively prime) Tommi Junttila (Aalto University) Kierros 5 CS-A1140 / Autumn 017 33 / 47
Esimerkki Eräs helppo tapa pakottaa kokeilujonot h(k,0),h(k,1),...,h(k,m 1) olemaan joukon {0, 1,..., m 1} permutaatioita on vaatia, että m on kahden potenssi ja h (k) on pariton luku. Esimerkki Toinen tapa saada kokeilujonot h(k,0),h(k,1),...,h(k,m 1) joukon {0, 1,..., m 1} permutaatioiksi on asettaa m alkuluvuksi ja h (k) positiivikseksi luvuksi, joka on pienempi kuin m. Esimerkiksi kokonaislukuvaimille voidaan asettaa h 1 (k) = k mod m ja h (k) = 1 + (k mod m ) jollekin arvolle m, joka on jonkin verran pienempi kuin m. Tommi Junttila (Aalto University) Kierros 5 CS-A1140 / Autumn 017 34 / 47
Avainten poistaminen Avainten poistaminen ketjutuksessa oli helppoa Avoimessa osoittamisessa täytyy pitää huoli, ettei avainten poistaminen jätä aukkoja taulukkoon ja muiden avainten kokeilujonoihin Esimerkki Olkoon h (k) = k kokonaislukuavaimille, m = 16 ja käytetään neliöistä kokeilua hajautusfunktiolla h(k) = (h (k) + i(i+1) ) mod m. Lisätään avaimet 3, 0 ja 35 taulukkoon. Tulos on 0 1 3 3 4 5 0 6 7 8 9 10 11 1 13 14 15 35 Jos nyt poistettaisiin avain 0 vain tuhoamalla se, tulos on 0 1 3 4 5 3 6 7 8 9 10 11 1 13 14 15 35 Mutta nyt avainten hakeminen ei enää löydä avainta 35 koska se kokeilee vain indeksejä 3 sekä 4 ja tämän jälkeen lopettaa havaittuaan vapaan indeksin 4. Tommi Junttila (Aalto University) Kierros 5 CS-A1140 / Autumn 017 35 / 47
Eräs ratkaisu on korvata poistettu avain erikoissymbolilla del Haku ja poistaminen kohtelevät näitä oikeina avaimina mutta avainten lisääminen voi kirjoittaa niiden päälle Jos taulukossa on monta tällaista arvoa, operaatiot hidastuvat ja taulukon avaimet kannattanee uudelleenhajauttaa samaan taulukkoon Esimerkki Tarkastellaan edellisen esimerkin taulukkoa 0 1 3 4 5 6 7 8 9 10 11 1 13 14 15 3 0 35 Poistetaan avain 0 edellä kuvatulla tavalla: 0 1 3 4 5 6 7 8 9 10 11 1 13 14 15 3 del 35 Nyt haku toimii kuten aiemminkin ja löytää avaimen 35 kokeiltuaan indeksejä 3, 4 ja 6. Jos lisätään avain 19, kokeillaan indeksejä 3 sekä 4 ja lisätään avain indeksiin 4. Tommi Junttila (Aalto University) Kierros 5 CS-A1140 / Autumn 017 36 / 47
Analyysiä Analyysiä varten oletetaan, että jokaisen avaimen kokeilujono on yhtä todennäköisesti mikä tahansa joukon {0, 1,..., m 1} m! permutaatiosta Esitetyistä kokeilumenetelmistä kaksoishajautus on lähimpänä vaatimusta Teoreema (11.6 kirjasta Introduction to Algorithms, 3rd ed. (online via Aalto lib)) Edellisellä oletuksella avaimen hakeminen tekee keskimäärin korkeintaan 1/(1 α) kokeilua silloin kun avainta ei löydy. Täyttösuhde α on pienempi kuin 1 ei-täysillä hajautustaulukoilla avointa osoittamista käytettäessä ja täten 1/(1 α) = 1 + α + α + α 3 +... Epämuodollinen selitys: 1 tulee ensimmäisestä aina tehtävästä kokeilusta Ensimmäinen kokeiltu indeksi oli varattu todennäköisyydellä α ja täten toinen kokeilu tehdään todennäköisyydellä α Ensimmäinen ja toinen kokeiltu indeksi olivat varattuja todennäköisyydellä α ja täten kolmas kokeilu tehdään todennäköisyydellä α... Tommi Junttila (Aalto University) Kierros 5 CS-A1140 / Autumn 017 37 / 47
Eli jos täyttösuhde pidetään pienempänä kuin jokin vakioraja-arvo, niin avainten lisääminen, etsiminen ja poistaminen vievät keskimäärin vakioajan Esimerkiksi jos täyttösuhde pidetään alle arvon 0.5, niin kokeilujen lukumäärä on keskimäärin korkeintaan Huom: poistetut arvot ( del -symboli) lesketaan mukaan täyttöasteeseen tässä yhteydessä. Jos avainten poistot ovat erittäin yleisiä, ketjutus voi olla parempi törmäystenkäsittelyratkaisu. Jälleen jos täyttösuhde nousee liian suureksi, tehdään uudelleenhajautus: kasvatetaan taulukon kokoa ja lisätään kaikki avaimet uuteen taulukkoon Kuten dynaamisten taulukoiden tapauksessa, taulukon kokoa kasvatetaan tyypillisesti kaksinkertaiseksi Mikä on hyvä raja-arvo täyttösuhteelle? Jälleen ei ole yksiselitteisesti parasta arvoa mutta tyypillisesti avoimessa osoittamisessa käytetään arvoja 0.50 tai 0.75 Tommi Junttila (Aalto University) Kierros 5 CS-A1140 / Autumn 017 38 / 47
Hajautusfunktioiden suunnittelusta Tommi Junttila (Aalto University) Kierros 5 CS-A1140 / Autumn 017 39 / 47
Aiemmin oletettiin hajautusfunktio h, jossa satunnaisella avaimella on sama todennäköisyys kuvautua mille tahansa indeksille {0, 1,..., m 1} toisistaan riippumatta Mutta ei välttämättä tiedetä etukäteen lisättävien avainten jakaumaa eikä niitä välttämättä valita toisistaan riippumattomasti Pyritään laskemaan hajautusarvot h(k) niin, että ne odotusarvoisesti ovat riippumattomia avaimissa mahdollisesti esiintyvistä säännönmukaisuuksista Esimerkiksi kääntäjän symbolitaulukossa ei tahdota yleisesti esiintyvien merkkijonojen i ja j hajautusarvojen olevan lähellä toisiaan jos käytetään suljettua hajautusta lineaarisella kokeilulla Karkea ohje voisi olla: mitä satunnaisemmalta hajautusfunktion arvot näyttävät, sen parempi Avaimen k hajautusarvon tulisi myös olla tehokkaasti laskettavissa Tommi Junttila (Aalto University) Kierros 5 CS-A1140 / Autumn 017 40 / 47
Kokonaislukujen hajautus Tarkastellaan ensin hajautusfunktioita kokonaisluvuille Ollaan jo nähty yleinen jakolaskumenetelmä, missä hajautusarvo h(k) on jakojäännös kun jaetaan avaimen arvo taulukon koolla m: h(k) = k mod m Tämä on tehokkaasti laskettavissa, tarvitaan vain yksi jakolasku Mutta tämä tapa ei välttämättä ole paras jos m on kahden potenssi eli m = p : vain p vähiten merkitsevää bittiä vaikuttaa hajautusfunktion arvoon Jos mahdollista, m pitäisi mieluummin olla alkuluku, joka ei ole liian lähellä kahden potenssia Tommi Junttila (Aalto University) Kierros 5 CS-A1140 / Autumn 017 41 / 47
Kertolaskumenetelmä tuottaa hajautusarvon w-bittiselle kokonaislukuavaimelle k kertomalla sen hyvin valitulla w-bittisellä vakioluvulla A, jolloin tulokseksi saadaan w-bittinen kokonaisluku r = r w 1 r w...r 0 = ka ja ottamalla tämän tuloksen vähemmän merkitsevän osuuden r w 1...r 0 eniten merkitsevistä biteistä tarvittavan määrän bittejä tulokseksi Nyt m on yleensä kahden potenssi, m = p, joten voidaan vain ottaa p eniten merkitsevää bittiä osuudesta r w 1...r 0 käyttämällä shift- ja and-operaatioita Tarkastellaan esimerkiksi Scala-funktiota def h(x: Int): Int = (x * 654435769L).toInt Nyt h(1).tohexstring = 9e3779b9 h().tohexstring = 3c6ef37 h(3).tohexstring = daa66db jne; arvot vaikuttavat kohtuullisen satunnaisilta Tommi Junttila (Aalto University) Kierros 5 CS-A1140 / Autumn 017 4 / 47
Merkkijonojen hajautus Tulkitaan merkkijono s = c 0...c n 1 kokonaisluvuksi prosessoimalla sen merkit c i yksi kerrallaan Esim. nykyisissä Javan toteutuksissa käytetään hajautusfunktiota n 1 h(s) = i=0 c i 31 n 1 i Toteutuksesta openjdk Java 8 (huomaa ohjelmallinen välimuisti hajautusarvolle eli sitä ei aina lasketa uudelleen): p u b l i c f i n a l class S t r i n g implements java. io. Serializable, Comparable<String >, CharSequence { / * * The value i s used f o r character storage. * / p r i v a t e f i n a l char value [ ] ; / * * Cache the hash code f o r the s t r i n g * / private i n t hash ; / / Default to 0... p u b l i c i n t hashcode ( ) { i n t h = hash ; i f ( h == 0 && value. l ength > 0) { char val [ ] = value ; f o r ( i n t i = 0; i < value. length ; i ++) { h = 31 * h + v a l [ i ] ; } hash = h ; } r e t u r n h ; } } Tommi Junttila (Aalto University) Kierros 5 CS-A1140 / Autumn 017 43 / 47
Rakenteellisten olioiden hajautus Taulukoille ja rakenteellisille olioille, joilla on useita kenttiä jne, voidaan laskea hajautusarvo samalla lailla yhdistämällä komponenttien hajautusarvot yhdeksi Esimerkiksi hajautusarvon laskenta kokonaislukutaulukoille toteutuksessa openjdk Java 6 p u b l i c s t a t i c i n t hashcode ( i n t a [ ] ) { i f ( a == n u l l ) r e t u r n 0; i n t r e s u l t = 1; f o r ( i n t element : a ) r e s u l t = 31 * r e s u l t + element ; return r e s u l t ; } Tommi Junttila (Aalto University) Kierros 5 CS-A1140 / Autumn 017 44 / 47
Tällä hetkellä Scala käyttää hieman monimutkaisempaa hajautusfunktiota, joka pohjautuu MurMurHash 3-funktioon Se pyrkii tuottamaan hyviä hajautusarvoja sekoittamalla avaintaulukon alkioiden arvojen bittejä tehokkaasti ja olemaan myös nopea Scalan lähdekoodista: f i n a l def arrayhash [@s p e c i a l i z e d T ] ( a : Array [ T ], seed : I n t ) : I n t = { var h = seed var i = 0 while ( i < a. length ) { h = mix ( h, a ( i ). ## ) / / ## i s hashcode i += 1 } f i n a l i z e H a s h ( h, a. length ) } f i n a l def mix ( hash : I n t, data : I n t ) : I n t = { var h = mixlast ( hash, data ) h = r o t l ( h, 13) / / r o t l i s I n t e r g e r. r o t a t e L e f t h * 5 + 0xe6546b64 } Tommi Junttila (Aalto University) Kierros 5 CS-A1140 / Autumn 017 45 / 47
f i n a l def mixlast ( hash : I n t, data : I n t ) : I n t = { var k = data k * = 0xcc9ed51 k = r o t l ( k, 15) k * = 0x1b873593 hash ˆ k } / * * F i n a l i z e a hash to i n c o r p o r a t e the length and make sure a l l b i t s avalanche. * / f i n a l def f i n a l i z e H a s h ( hash : I n t, length : I n t ) : I n t = avalanche ( hash ˆ length ) / * * Force a l l b i t s of the hash to avalanche. Used f o r f i n a l i z i n g the hash. * / private f i n a l def avalanche ( hash : I n t ) : I n t = { var h = hash h ˆ = h >>> 16 h * = 0x85ebca6b h ˆ = h >>> 13 h * = 0xcbae35 h ˆ = h >>> 16 h } Tommi Junttila (Aalto University) Kierros 5 CS-A1140 / Autumn 017 46 / 47
Lisää Hyvien hajautusfunktioiden tärkeydestä johtuen useita erilaisia hajautusfunktioita on kehitetty eri tarkoituksiin vuosien aikana Katso esim. seuraavia sivustoja: https://en.wikipedia.org/wiki/hash_function http://www.partow.net/programming/hashfunctions/ http://www.burtleburtle.net/bob/hash/doobs.html https://en.wikipedia.org/wiki/murmurhash https://gcc.gnu.org/viewcvs/gcc/trunk/libstdc%b%b-v3/ libsupc%b%b/hash_bytes.cc?view=markup#l74 Tommi Junttila (Aalto University) Kierros 5 CS-A1140 / Autumn 017 47 / 47