6. Sanakirjat. 6. luku 298

Samankaltaiset tiedostot
6. Sanakirjat Sanakirjan abstrakti tietotyyppi

AVL-puut. eräs tapa tasapainottaa binäärihakupuu siten, että korkeus on O(log n) kun puussa on n avainta

A TIETORAKENTEET JA ALGORITMIT

lähtokohta: kahden O(h) korkuisen keon yhdistäminen uudella juurella vie O(h) operaatiota vrt. RemoveMinElem() keossa

3. Hakupuut. B-puu on hakupuun laji, joka sopii mm. tietokantasovelluksiin, joissa rakenne on talletettu kiintolevylle eikä keskusmuistiin.

Hakupuut. tässä luvussa tarkastelemme puita tiedon tallennusrakenteina

7. Tasapainoitetut hakupuut

8. Lajittelu, joukot ja valinta

v 1 v 2 v 3 v 4 d lapsisolmua d 1 avainta lapsen v i alipuun avaimet k i 1 ja k i k 0 =, k d = Sisäsolmuissa vähint. yksi avain vähint.

Algoritmit 2. Luento 2 Ke Timo Männikkö

Algoritmit 2. Luento 2 To Timo Männikkö

Algoritmit 2. Luento 4 To Timo Männikkö

58131 Tietorakenteet ja algoritmit (kevät 2016) Ensimmäinen välikoe, malliratkaisut

Algoritmit 2. Luento 4 Ke Timo Männikkö

Algoritmit 2. Luento 3 Ti Timo Männikkö

Binäärihaun vertailujärjestys

Algoritmit 2. Luento 3 Ti Timo Männikkö

TKT20001 Tietorakenteet ja algoritmit Erilliskoe , malliratkaisut (Jyrki Kivinen)

(a) L on listan tunnussolmu, joten se ei voi olla null. Algoritmi lisäämiselle loppuun:

Hajautus. operaatiot insert ja search pyritään tekemään erittäin nopeiksi

A TIETORAKENTEET JA ALGORITMIT

Tiraka, yhteenveto tenttiinlukua varten

Pinot, jonot, yleisemmin sekvenssit: kokoelma peräkkäisiä alkioita (lineaarinen järjestys) Yleisempi tilanne: alkioiden hierarkia

Koe ma 1.3 klo salissa A111, koeaika kuten tavallista 2h 30min

10. Painotetut graafit

Algoritmit 1. Luento 7 Ti Timo Männikkö

58131 Tietorakenteet (kevät 2008) 1. kurssikoe, ratkaisuja

811312A Tietorakenteet ja algoritmit, , Harjoitus 7, ratkaisu

811312A Tietorakenteet ja algoritmit, , Harjoitus 5, Ratkaisu

Algoritmit 2. Luento 7 Ti Timo Männikkö

Algoritmit 2. Luento 5 Ti Timo Männikkö

Algoritmit 1. Luento 8 Ke Timo Männikkö

Tietorakenteet ja algoritmit Johdanto Lauri Malmi / Ari Korhonen

Algoritmit 2. Luento 6 To Timo Männikkö

Kierros 4: Binäärihakupuut

Algoritmit 2. Luento 6 Ke Timo Männikkö

811312A Tietorakenteet ja algoritmit, , Harjoitus 5, Ratkaisu

Algoritmit 1. Luento 12 Ti Timo Männikkö

Algoritmit 1. Luento 12 Ke Timo Männikkö

Tietorakenteet ja algoritmit - syksy

811312A Tietorakenteet ja algoritmit, VI Algoritmien suunnitteluparadigmoja

Algoritmit 2. Luento 5 Ti Timo Männikkö

CS-A1140 Tietorakenteet ja algoritmit

4 Tehokkuus ja algoritmien suunnittelu

Tietorakenteet, laskuharjoitus 7, ratkaisuja

5. Hajautus. Tarkastellaan edelleen sivulla 161 esitellyn joukkotietotyypin toteuttamista

A TIETORAKENTEET JA ALGORITMIT

58131 Tietorakenteet ja algoritmit (kevät 2013) Kurssikoe 1, , vastauksia

puuta tree hierarkkinen hierarchical

Algoritmit 1. Luento 3 Ti Timo Männikkö

2. Seuraavassa kuvassa on verkon solmujen topologinen järjestys: x t v q z u s y w r. Kuva 1: Tehtävän 2 solmut järjestettynä topologisesti.

811312A Tietorakenteet ja algoritmit V Hash-taulukot ja binääriset etsintäpuut

Tietorakenteet ja algoritmit Hakurakenteet Ari Korhonen

14 Tasapainotetut puurakenteet

Algoritmit 1. Luento 13 Ti Timo Männikkö

Tietorakenteet ja algoritmit. Hajautus. Ari Korhonen Tietorakenteet ja algoritmit - syksy

Tietorakenteet, laskuharjoitus 6,

9.3 Algoritmin valinta

1 Erilaisia tapoja järjestää

4. Joukkojen käsittely

Algoritmit 2. Luento 14 Ke Timo Männikkö

Kysymyksiä koko kurssista?

Luku 8. Aluekyselyt. 8.1 Summataulukko

Algoritmi on periaatteellisella tasolla seuraava:

Sekvenssi: kokoelma peräkkäisiä alkioita (lineaarinen

Algoritmit 1. Luento 1 Ti Timo Männikkö

Tehtävän V.1 ratkaisuehdotus Tietorakenteet, syksy 2003

Kaksiloppuinen jono D on abstrakti tietotyyppi, jolla on ainakin seuraavat 4 perusmetodia... PushFront(x): lisää tietoalkion x jonon eteen

1.1 Tavallinen binäärihakupuu

811312A Tietorakenteet ja algoritmit Kertausta jälkiosasta

811312A Tietorakenteet ja algoritmit II Perustietorakenteet

4. Sekvenssit Astetta soveltavat sekvenssit

58131 Tietorakenteet ja algoritmit Uusinta- ja erilliskoe ratkaisuja (Jyrki Kivinen)

B + -puut. Kerttu Pollari-Malmi

SQL-perusteet, SELECT-, INSERT-, CREATE-lauseet

4. Perustietorakenteet

Nopea kertolasku, Karatsuban algoritmi

Ohjelmoinnin peruskurssi Y1

811312A Tietorakenteet ja algoritmit Kertausta kurssin alkuosasta

A TIETORAKENTEET JA ALGORITMIT

Tietotyypit ja operaattorit

private TreeMap<String, Opiskelija> nimella; private TreeMap<String, Opiskelija> numerolla;

useampi ns. avain (tai vertailuavain) esim. opiskelijaa kuvaavassa alkiossa vaikkapa opintopistemäärä tai opiskelijanumero

811312A Tietorakenteet ja algoritmit V Verkkojen algoritmeja Osa 2 : Kruskalin ja Dijkstran algoritmit

Graafit ja verkot. Joukko solmuja ja joukko järjestämättömiä solmupareja. eli haaroja. Joukko solmuja ja joukko järjestettyjä solmupareja eli kaaria

Ohjelmoinnin perusteet Y Python

1 Puu, Keko ja Prioriteettijono

Algoritmit 1. Luento 5 Ti Timo Männikkö

Luku 7. Verkkoalgoritmit. 7.1 Määritelmiä

5. Keko. Tietorakenne keko eli kasa (heap) on tehokas toteutus abstraktille tietotyypille prioriteettijono, jonka operaatiot ovat seuraavat:

58131 Tietorakenteet (kevät 2009) Harjoitus 6, ratkaisuja (Antti Laaksonen)

CS-A1140 Tietorakenteet ja algoritmit

A TIETORAKENTEET JA ALGORITMIT

Algoritmit 1. Luento 6 Ke Timo Männikkö

58131 Tietorakenteet ja algoritmit (syksy 2015) Toinen välikoe, malliratkaisut

ALGORITMIT 1 DEMOVASTAUKSET KEVÄT 2012

Ohjelmoinnin perusteet Y Python

811312A Tietorakenteet ja algoritmit, , Harjoitus 3, Ratkaisu

TKHJ:ssä on yleensä komento create index, jolla taululle voidaan luoda hakemisto

815338A Ohjelmointikielten periaatteet Harjoitus 7 Vastaukset

Transkriptio:

6. Sanakirjat Tässä luvussa tarkastellaan käsitettä sanakirja (dictionary). Tällaisen tietorakenteen tehtävä on tallettaa alkioita niin, että tiedonhaku rakenteesta on tehokasta. Nimi vastaa melko hyvin todellista sanakirjaa, mutta on lisäksi dynaaminen; ovathan alkion lisäys ja poisto keskeisiä toimintoja. Käsitellään sanakirjojen toteuttamista binääripuun ja sekvenssin avulla. Nämä ovat yksinkertaisia, mutta eivät kovin tehokkaita. Parempia ovat mm. AVL puut ja hajautustaulut. 6. luku 298

6.1. Sanakirjan abstrakti tietotyyppi Sanakirjassa on talletettuna avain alkio pareja (k,e), joita tässä kutsutaan tietoyksiköiksi (item). Yleisyyden sallimiseksi avaimet ja alkiot voivat olla mitä tahansa tyyppiä. Sanakirja voisi sisältää esim. opiskelijatietueita käsittäen opiskelijan nimen, osoitteen ja kurssitiedot, jolloin avain olisi opiskelijan henkilötunnus. Avaimena saattaisi olla myös itse alkio. Esim. käytettäessä sanakirjaa alkulukujen tallettamiseen voitaisiin itse lukuja käyttää myös avaimina. Sanakirjoja on kahta tyyppiä, järjestämätön (unordered) ja järjestetty (ordered). Jälkimmäiselle oletetaan täydellisen järjestyksen relaation olevan voimassa avainten suhteen. Järjestetty sanakirja määrää kahden avaimen suhteellisen järjestyksen vertaimen avulla (luku 5.1.). Järjestämättömän sanakirjan tapauksessa mitään järjestysrelaatiota ei sovelleta, vaan ainoastaan yhtäsuuruutta testataan avainten välillä. 6. luku 299

Yleisyyden vuoksi sanakirjan määrittely sallii useiden tietoyksiköiden tallettamisen samalla avaimella. On tietysti sellaisiakin sovelluksia, joissa tätä ei sallita ja avaimet ovat yksikäsitteisiä, kuten edeltävässä opiskelijasanakirjassa kukin henkilötunnus olisi yksikäsitteinen. Tällöin niitä voidaan kutsua myös osoitteiksi. Sanakirjan metodit Geneerinen järjestämätön sanakirja D tukee abstraktina tietotyyppinä seuraavia operaatioita: size(): Palauttaa sanakirjan D tietoyksiköiden lukumäärän. Tulos: kokonaisluku isempty(): Testaa, onko D tyhjä. Tulos: totuusarvo 6. luku 300

findelement(k): Jos D sisältää tietoyksikön, jonka avain on k, niin palauttaa sen alkion, mutta muuten (sanakirjassa ei ole kyseistä avainta) palauttaa erikoisalkion EI_AVAINTA epäonnistuneen haun ilmoittamiseksi. Syöte: avain Tulos: alkio findallelements(k): Palauttaa sanakirjan kaikkien niiden alkioiden luettelon, joiden avain on k. Syöte: avain Tulos: alkioiden luettelo insertitem(k,e): avain k. Lisää sanakirjaan D tietoyksikön, jonka alkio on e ja Syöte: avain k ja alkio e 6. luku 301

remove(k): Poistaa sanakirjasta D tietoyksikön, jonka avain on k ja palauttaa alkion. Jos D:ssä ei ollut mainittua, niin palauttaa erikoisalkion EI_AVAINTA. Syöte: avain k Tulos: alkio removeall(k): Poistaa sanakirjasta D kaikki tietoyksiköt, joiden avain on k, ja palauttaa näiden alkioiden luettelon. Syöte: avain Tulos: alkioiden luettelo Kahdessa metodissa käytettiin erikoisalkiota EI_AVAINTA, jota kutsutaan vartijaksi (sentinel). Tyhjääkin alkiota voitaisiin käyttää, mutta on mielekästä erottaa em. tapaus tyhjästä, jotta voidaan käyttää myös varsinaista tyhjää alkiota sanakirjassa. Mikäli sanakirja sisältäisi samalle avaimelle useita tietoyksiköitä, operaatiot findelement(k) ja remove(k) palauttaisivat mielivaltaisesti valitun alkion niistä, joilla tuo avain on. 6. luku 302

Esim. 6.1. Katsotaan operaatioiden sarjan vaikutusta alunperin tyhjään sanakirjaan, johon talletetaan kokonaislukuavaimia ja yksimerkkisiä alkioita. operaatio tulos sanakirja insertitem(5,a) { (5,A) } insertitem(7,b) { (5,A),(7,B) } insertitem(2,c) { (5,A),(7,B),(2,C) } insertitem(8,d) { (5,A),(7,B),(2,C),(8,D) } insertitem(2,e) { (5,A),(7,B),(2,C),(8,D),(2,E) } findelement(7) B { (5,A),(7,B),(2,C),(8,D),(2,E) } findelement(4) EI_AVAINTA { (5,A),(7,B),(2,C),(8,D),(2,E) } findelement(2) C { (5,A),(7,B),(2,C),(8,D),(2,E) } findallelements(2) C, E { (5,A),(7,B),(2,C),(8,D),(2,E) } 6. luku 303

(jatkoa) size() 5 { (5,A),(7,B),(2,C),(8,D),(2,E) } remove(5) A { (7,B),(2,C),(8,D),(2,E) } removeall(2) C, E { (7,B),(8,D) } findelement(2) EI_AVAINTA { (7,B),(8,D) } Yhtäsuuruuden testaajat Jokainen em. metodeista edellyttää luonnollisesti, että on olemassa mekanismi kahden avaimen yhtäsuuruuden tutkimiseksi. Jos sanakirja on järjestetty, niin vertaimen metodi areequal (luku 5.1.) sanakirjaan liitettynä palvelee tätä tarkoitusta. Muuten, so. geneeriselle sanakirjalle, sovelletaan yhtäsuuruuden testaajaa, joka tukee avaimille käytettävää operaatiota areequal. 6. luku 304

Esitetty sanakirjan määritelmä on yleisempi kuin Javan abstrakti sanakirjaluokka (java.util.dictionary), sillä tämä ei salli useita tietoyksiköitä samalle avaimelle. Edellä esitettyjen metodien lisäksi järjestetylle sanakirjalle voidaan määritellä muita metodeja, sellaisia, jotka antavat esim. järjestyksessä edeltävän tai seuraavan avaimen syötetyn avaimen suhteen. 6.2. Sanakirjan toteuttaminen sekvenssillä Sanakirja on helppoa tehdä sekvenssin avulla. Jälleen kun se on yksinkertainen vaihtoehto, se ei ole kuitenkaan tehokkain. Järjestämättömät (lajittelemattomat) ja järjestetyt (lajitellut) sekvenssit Sanakirja voidaan toteuttaa usealla tavalla sekvenssiä soveltaen. 6. luku 305

Järjestämätön sekvenssitoteutus Yksinkertaisinta toteuttaa n tietoyksikön sanakirja D sekvenssiä S soveltaen on tallettaa tietoyksiköt sekvenssiin mielivaltaisesti valitussa järjestyksessä (kuva 6.1.(a)). Sekvenssi olkoon puolestaan toteutettu taulukkona tai kahteen suuntaan linkitettynä listana (ks. luku 4.3.). Tämä sanakirjan D toteutus on järjestämätön sekvenssitoteutus (unordered sequence implementation), sillä avaimilla ei ole mitään vaikutusta tietoyksikön lineaariseen järjestämiseen sekvenssissä S. 34 14 12 22 18 (a) 12 14 18 22 34 (b) Kuva 6.1. Sanakirjan toteuttaminen sekvenssinä: (a) järjestämätön sekvenssi ja (b) järjestetty (yksinkertaisuuden takia vain avaimet esitetty, ilman alkioita). 6. luku 306

Järjestämättömän sekvenssin tapauksessa tilavaatimus on Θ(n), jos sekvenssi on tehty kahteen suuntaan linkitettynä listana, ja Θ(N), jos se on tehty taulukolla kokoa N n. Operaatio insertitem(k,e) voidaan toteuttaa tehokkaasti käyttäen S:n metodien insertfirst tai insertlast yhtä kutsua, jolloin suoritusaika on kaikkiaan O(1) sanakirjan D lisäykselle. Tosin tämä toteutus ei mahdollista tehokasta hakua metodilla findelement(k), sillä tällöin täytyy selata sekvenssiä S läpi, kunnes haluttu löydetään tai tullaan sekvenssin loppuun. Pahimmassa tapauksessa suoritusaika on O(n). Samoin lineaarinen aika tulee poistolle remove(k) pahimmassa tapauksessa, koska poistettava on selattava esiin. Operaatiot findallelements ja removeall vaativat aina koko sekvenssin selaamisen läpi, joten niiden suoritusaika on Θ(n). Yhteenvetona todetaan järjestämättömän sekvenssitoteutuksen aikaansaavan sanakirjalle nopeat lisäykset hitaiden hakujen ja poistojen kustannuksella. Tällöin tämä toteutusvaihtoehto on mielekäs, kun sanakirja on suppea tai oletetaan lisäysten määrän olevan sangen suuri verrattuna hakujen ja poistojen määrään. 6. luku 307

Järjestetty sekvenssitoteutus Jos sanakirja D on järjestetty, tietoyksiköt voidaan tallettaa avainten eivähenevään järjestykseen sekvenssiin S (kuva 6.1.(b)). S on jälleen toteutettu taulukkona tai kahteen suuntaan linkitettynä listana. Nimetään tämä järjestetyksi sekvenssitoteutukseksi (ordered sequence implementation), kun avaimet määräävät lineaarisen järjestyksen sekvenssissä S. Aivan edellisen toteutuksen lailla tilavaatimus on Θ(n), mikäli sekvenssi tehdään kahteen suuntaan linkitetyn listan avulla, ja Θ(N), jos se tehdään taulukolla kooltaan N n. Järjestämättömästä sekvenssitoteutuksesta eroten operaatio insertitem(k,e) tarvitsee nyt ajan O(n) pahimmassa tapauksessa. Jos S toteutetaan kahteen suuntaan linkitettynä listana, näin paljon tarvitaan suoritusaikaa enimmillään uuden tietoyksikön (k,e) lisäyspaikan löytämiseksi. Taulukkoa sovellettaessa näin paljon aikaa kuluu enimmillään siirtää kaikki avainta k suurempien avainten tietoyksiköt tilan tekemiseksi uudelle tietoyksikölle. 6. luku 308

Samankaltainen tilanne pätee operaatioille remove(k) ja removeall(k). Jos S toteutetaan kahteen suuntaan linkitettynä listana, tarvitaan suoritusaikaa O(n) pahimmassa tapauksessa poistettavan tietoyksikön tai yksiköiden löytämiseksi. Taulukkoa käytettäessä sama aika menee tietoyksikön tai yksiköiden siirtämiseen poistetun jättämän aukon täyttämiseksi. Jos sekvenssi S laaditaan kahteen suuntaan linkitettynä listana, niin tietoyksikön haku avaimella k vaatii selauksen sekvenssin läpi ja kestää ajan O(n) pahimmassa tapauksessa eli haun epäonnistuessa (tätä avainta ei ole sanakirjassa). Näin ollen operaatiot findelement ja findallelements toimivat ajassa O(n), kuten on minkä tahansa operaation tapauksessa, jolla suoritetaan siinä hakua. Niinpä tällainen toteutus on melko tehoton. Haut ovat tehtävissä paljon nopeammin muodostettaessa sekvenssi S taulukon avulla. 6. luku 309

Binäärihaku Huomattava hyöty taulukon käyttämisessä sekvenssiä varten sanakirjan D yhteydessä on se, että alkion saanti sanakirjasta soveltaen astetta (rank) eli yksinkertaisesti indeksiä on tehtävissä ajassa O(1). Tällöin (luku 3.6.) n alkioisen sekvenssin ensimmäisen alkion on astetta 0 ja viimeinen n 1. Sekvenssin alkiot ovat yhtä kuin sanakirjan tietoyksiköt. Kun sekvenssi S on järjestetty avainten mukaan, S:n tietoyksiköllä astetta i on avain, joka ei ole pienempi kuin tietoyksiköiden avaimet astetta 0,.., i 1 ja ei suurempi kuin avaimet astetta i+1,, n 1. Tämä havainto mahdollistaa metodin findelement(k) suorituksen nopeuttamisen. Nyt sekvenssin täydellisen haun sijasta voidaan ikään kuin haarukoida kohtaa, jossa haettava avain k on sekvenssissä. Annetaan tässä alkukohta low ja loppukohta high, joiden väliin avainta paikallistetaan. Kutsutaan sanakirjan D sellaisia tietoyksiköitä, joita ei vielä ole haarukoitu pois, ehdokkaiksi (candidate). Ylläpidetään kahta em. hakuparametria niin, että ehdokastietoyksiköiden asteet ovat parametrien low ja high arvojen välistä sekvenssissä S. 6. luku 310

Aluksi ovat low = 0 ja high = n 1. Verrataan sitten avainta k sekvenssin S keskimmäiseen avaimeen, jonka aste on mid = (low + high)/2. Olkoot key(mid) keskimmäisen tietoyksikön avain ja elem(mid) alkio. On kolme tapausta: Jos on k = key(mid), niin etsitty alkio on löydetty ja haku päättyy menestyksellisesti palauttaen alkion elem(mid). Jos on k < key(mid), niin haetaan sekvenssin ensimmäisestä puoliskosta, jonka asteet ovat arvosta low arvoon mid 1. Jos k > key(mid), niin haetaan alueesta, jonka asteet ulottuvat arvosta mid+1 arvoon high. Esitettyä hakutekniikkaa kutsutaan binäärihauksi (binary search). Sitä varten on pseudokoodi kuvattu koodissa 6.1. ja esimerkki kuvassa 6.2. 6. luku 311

Algorithm BinarySearch(S,k,low,high): Input: Sekvenssi S, jossa on n tietoyksikköä avainten ei vähenevässä järjestyksessä, ja hakuavain k sekä kokonaisluvut low ja high. Output: Sekvenssin S alkio, jonka avain on k ja aste muuttujien low ja high arvojen välistä, mikäli tällainen alkio on olemassa, ja muuten erikoisalkio EI_AVAINTA. if low > high then return EI_AVAINTA else mid (low + high)/2 if k = key(mid) then return elem(mid) else if k < key(mid) then return BinarySearch(S,k,low,mid 1) else return BinarySearch(S,k,mid+1,high) Koodi 6.1. Binäärihaku järjestetystä sekvenssistä. 6. luku 312

2 4 5 7 8 9 12 14 17 19 22 25 27 28 33 37 low mid high 2 4 5 7 8 9 12 14 17 19 22 25 27 28 33 37 low mid high 2 4 5 7 8 9 12 14 17 19 22 25 27 28 33 37 low mid high 2 4 5 7 8 9 12 14 17 19 22 25 27 28 33 37 low=mid=high Kuva 6.1. Esimerkki binäärihaun suorittamisesta operaatiolla findelement(22) sanakirjassa, jonka avaimet ovat kokonaislukuja ja joka on toteutettu taulukkopohjaisena järjestettynä sekvenssinä. Ainoastaan avaimet on kuvattu. 6. luku 313

Operaatio findelement(k) käsittää kutsun BinarySearch(S, k, 0, n 1). Havaitaan kullakin rekursiivisella metodin BinarySearch kutsulla suoritettavan vakiomäärän alkeisoperaatioita. Tästä seuraa, että suoritusaika on suhteessa tehtyjen rekursiivisten kutsujen lukumäärään. Rekursiivisen kutsun sisältämä ehdokastietoyksiköiden määrä on high low + 1. Jokaisella kutsulla ehdokkaiden määrä ainakin puoliintuu. Muuttujan mid määritelmästä tulee, että jäljellä olevien ehdokkaiden määrä on joko (mid 1) low + 1 = (low + high)/2 low (high low + 1)/2 tai high (mid + 1) + 1 = high (low + high)/2 (high low + 1)/2. 6. luku 314

Ehdokkaiden määrä on aluksi n. Metodin BinarySearch ensimmäisen kutsun jälkeen se on enintään n/2, toisen kutsun jälkeen enintään n/4 jne. Yleisesti i:nnen kutsun jälkeen ehdokkaiden määrä on enintään n/2 i. Pahimmassa tapauksessa (epäonnistunut haku) rekursiiviset kutsut loppuvat ehdokkaiden loppuessa. Rekursiivisten kutsujen maksimimäärä on pienin kokonaisluku m, jolle on n/2 m < 1. Tästä saadaan m > log n. Edelleen tulee m = log n + 1, 6. luku 315

joka osoittaa, että BinarySearch(S, k, 0, n 1) ja samalla findelement toimii ajassa O(log n). Binäärihaun yksinkertainen muunnelma findallelements(k) toimii ajassa O(log n + s), missä s on palautettujen alkioiden määrä. Taulukko 6.1. vertaa sanakirjojen metodien suoritusaikoja, kun nämä on toteutettu järjestämättömällä ja järjestetyllä sekvenssillä (joko kahteen suuntaan linkitetty lista tai taulukko). Järjestämätön sekvenssi sallii nopeat lisäykset, mutta hitaat haut ja poistot, kun taas järjestetty taulukkopohjainen sekvenssi sallii nopeat haut, mutta hitaat lisäykset ja poistot. Järjestetty sekvenssi kahteen suuntaan linkitettynä listana on hidas kaikille operaatioille. 6. luku 316

Taulukko 6.1. Sanakirjaoperaatioiden suoritusaikojen vertailu. Sekvenssin tietoyksiköiden määrä on n metodia suoritettaessa. Operaatioiden findallelements ja removeall palauttamien alkioiden määrä on s. Kahteen suuntaan linkitettyä listaa sovellettaessa tilavaatimus on O(n) ja taulukolle O(N), missä N n. metodi järjestämätön sekvenssi järjestetty sekvenssi taulukko linkitetty lista taulukko linkitetty lista size, Θ(1) Θ(1) Θ(1) Θ(1) isempty findelement O(n) O(n) O(log n) O(n) findall Θ(n) Θ(n) O(log n + s) O(n) Elements insertitem Θ(1) Θ(1) O(n) O(n) remove O(n) O(n) O(n) O(n) removeall Θ(n) Θ(n) O(n) O(n) 6. luku 317

6.3. Binäärihakupuut Binääriset hakupuut ovat vaihtoehto tiedon tallettamista varten järjestettyyn sanakirjaan. Kuten edellä esitettiin (luku 4.3.) binäärihakupuu (binary search tree) T on sellainen, jossa jokainen sisäsolmu v käsittää tietoyksikön (k,e) ja solmun v vasempaan alipuuhun talletetut avaimet ovat pienempiä tai yhtä suuria kuin k sekä solmun v oikeaan alipuuhun talletetut avaimet ovat suurempia tai yhtä suuria kuin k. Huomattakoon, että lehdet eivät tässäkään sisällä tietoa, vaan ovat pelkästään paikanpitäjiä. Helposti on todettavissa, että välijärjestyskulkeminen (luku 4.3.) käy puun T avaimet ei vähenevässä järjestyksessä (kuva 6.3.(a)). 6. luku 318

Haku Operaation findelement(k) suorittamiseksi sanakirjassa D, joka on tehty binäärihakupuuna T, käytetään tätä tavallaan päätöspuuna. Tutkitaan puun sisäsolmussa v, onko hakuavain k pienempi, yhtä suuri tai suurempi kuin sisäsolmun v avain key(v). Jos vastaus on pienempi, hakua jatketaan vasempaan alipuuhun. Jos vastaus on yhtä suuri, haku päättyy menestyksellisesti. Jos vastaus on suurempi, jatketaan hakua oikeaan alipuuhun. Jos lopulta tullaan lehteen, haku on epäonnistunut eli avainta ei puussa esiintynyt. (kuva 6.3.(b)) 6. luku 319

44 17 88 32 65 97 28 54 82 29 76 80 (a) Kuva 6.3. (a) Järjestettyä sanakirjaa D kuvaava binäärihakupuu T, jossa avaimet ovat kokonaislukuja (yksinkertaisuuden takia vain nämä esitetty). 6. luku 320

find(25) find(76) 44 17 88 32 65 97 28 54 82 29 76 80 (b) Kuva 6.3.(b) Kuljetut solmut suoritettaessa operaatiot findelement(76) (onnistunut haku) ja findelement(25) (epäonnistunut haku) sanakirjaan. 6. luku 321

Koodissa 6.2. on rekursiivinen metodi TreeSearch, joka käsittää binäärihakupuuhaun. Olkoot hakuavain k ja solmu v puusta T. Metodi TreeSearch palauttaa solmun (paikan) w alipuusta T(v), jonka juuri on v, jolloin jompikumpi seuraavista tapauksista esiintyy: w on avaimen k sisältävän alipuun T(v) sisäsolmu. w on alipuun T(v) lehti, alipuun T(v) kaikilla sisäsolmuilla, jotka edeltävät solmua w välijärjestyskulkemisessa, on avaimet pienempiä kuin k ja alipuun T(v) kaikilla sisäsolmuilla, jotka seuraavat solmua w välijärjestyskulkemisessa, on avaimet suurempia kuin k. Metodi findelement(k) voidaan siis suorittaa sanakirjalle D kutsumalla metodia TreeSearch(k,T.root()) puulle T. Olkoon w puun T solmu, jonka tämä metodin TreeSearch kutsu palauttaa. Jos w on sisäsolmu, palautetaan alkio, joka on talletettu solmuun w. Muussa tapauksessa, kun w on lehti, palautetaan erikoisalkio EI_AVAINTA. 6. luku 322

Algorithm TreeSearch(k,v): Input: Hakuavain k ja binäärihakupuun T solmu v. Output: Alipuun T(v) (juurena v) solmu w, kun joko w on sisäsolmu avaimena k tai w on lehti, johon päädytään alipuun T(v) välijärjestyskulkemisessa kaikkien avainta k pienempien sisäsolmujen avainten käynnin jälkeen, mutta ennen kaikkia avainta k suurempia sisäsolmujen avaimia. if v on lehti then return v if k = key(v) then return v else if k < key(v) then return TreeSearch(k,T.leftChild(v)) else { tiedetään, että k > key(v) } return TreeSearch(k,T.rightChild(v)) Koodi 6.2. Rekursiivinen haku binäärihakupuusta. 6. luku 323

Suoritusajan pahimman tapauksen analyysi on yksinkertainen. Algoritmi TreeSearch on rekursiivinen ja suorittaa jokaisella rekursiivisella kutsulla vakiomäärän alkeisoperaatioita. Jokainen rekursiivinen kutsu koskee edellisen solmun lasta. Tästä johtuen metodia TreeSearch kutsutaan puun T erään polun solmuissa ja näiden määrää rajoittaa h+1, missä h on korkeus. Siispä metodi findelement suoriutuu ajassa O(h), missä h on sanakirjan D binäärihakupuun korkeus. Edellisen algoritmin muunnelma findallelements(k) toimii ajassa O(h+s), missä s on palautettujen alkioiden määrä. Puun T korkeus h voi olla enimmillään n, mutta keskimäärin se on huomattavasti pienempi. Luvussa 6.4. esitetään, kuinka voidaan taata yläraja O(log n) hakupuun korkeudelle. 6. luku 324

Päivitysoperaatiot Binäärihakupuu tukee operaatioiden insertitem ja remove toteutuksia soveltaen algoritmeja, jotka ovat hivenen sekvenssitoteutuksia monimutkaisempia. Lisäys Operaation insertitem(k,e) suorittamiseksi binäärihakupuulla T toteutetulle sanakirjalle D aloitetaan kutsumalla metodia TreeSearch(k,T.root()) puulle T. Olkoon w tämän palauttama solmu. Jos w on lehti (puuhun T ei ole talletettu tietoyksikköä avaimella k), korvataan w uudella sisäsolmulla, joka tallettaa parin (k,e) ja kaksi lehtilasta operaation expandexternal(w) avulla puuhun T. Jos w on sisäsolmu (solmuun w talletettu tietoyksikkö avaimella k), kutsutaan metodia TreeSearch(k,rightChild(w)) ja rekursiivisesti sovelletaan algoritmia metodin TreeSearch palauttamaan solmuun. 6. luku 325

Huomaa, että edellisessä olisi voinut yhtä hyvin olla kutsu TreeSearch(k,leftChild(w)), sillä lisättävä avain k on tällöin yhtä suuri kuin solmun w avain. Esitetty menettely tuottaa polun juuresta lehteen, joka muutetaan sisäsolmuksi ja tälle tulee uusi solmu lapseksi. Täten lisäysoperaatio lisää uuden tietoyksikön hakupuun pohjalle. Esimerkki on kuvattu kuvassa 6.4. Lisäysalgoritmin analyysi on analoginen haulle. Aikaa kulutetaan O(1) jokaisessa käytävässä solmussa ja pahimmassa tapauksessa käytyjen solmujen lukumäärä on suhteessa puun T korkeuteen h. Sanakirjan D lisäysmetodi toimii näin ollen ajassa O(h), missä h on puun korkeus. 6. luku 326

44 17 88 32 65 97 28 54 82 29 76 80 (a) Kuva 6.4. Avaimeen 78 liittyvän alkion lisääminen binäärihakupuuhun: (a) lisäyspaikan etsiminen. 6. luku 327

44 17 88 32 65 97 28 54 82 29 76 80 Kuva 6.4.(b) Binäärihakupuu lisäyksen jälkeen. (b) 78 6. luku 328

Poisto Poiston remove(k) toteutus binäärihakupuuhun T perustuvasta sanakirjasta D on hieman lisäystä monimutkaisempi, koska puuhun ei voi jättää reikiä. Operaatio alkaa yksinkertaisesti. Suoritetaan algoritmi TreeSearch(k,T,root()) puulle T avaimen k sisältävän solmun löytämiseksi. Jos TreeSearch palauttaa lehden, sanakirjassa D ei ole avaimella k solmua ja palautetaan erikoisalkio EI_AVAINTA. Jos TreeSearch palauttaa sisäsolmun w, tämä sisältää etsityn, poistettavaksi tarkoitetun tietoyksikön. Erotetaan kaksi tapausta: Jos solmun w lapsista toinen on lehti, olkoon se z, poistetaan yksinkertaisesti solmut w ja z puusta T operaation removeaboveexternal(z) avulla. Tämä rakentaa T:n uudelleen korvaamalla solmun w solmun z sisaruksella ja poistamalla solmut w ja z. Tätä esittää kuva 6.5. 6. luku 329

44 17 88 w 32 65 97 28 z 54 82 29 76 80 Kuva 6.5. Poisto binäärihakupuusta, missä poistetaan avaimen 32 solmu w ja tämän lehtilapsi z: (a) Ennen poistoa. 78 (a) 6. luku 330

44 17 88 28 65 97 29 54 82 76 80 Kuva 6.5. (b) Poiston jälkeen. 78 (b) 6. luku 331

Jos solmun w molemmat lapset ovat sisäsolmuja, ei voi yksinkertaisesti poistaa solmua w puusta T, sillä tämä jättäisi reiän. Toimitaan sen sijaan seuraavasti (kuva 6.6.): Etsitään ensimmäinen sisäsolmu y, joka seuraa solmua w kuljettaessa puuta välijärjestyksessä. Solmu y on solmun w oikean alipuun vasemmanpuoleinen sisäsolmu. Se löydetään menemällä ensin solmun w oikeaan lapseen ja siitä sitten alaspäin puuta pitkin vasemmanpuoleisia lapsia. Solmun y vasen lapsi x on lehti, joka seuraa välittömästi solmua w välijärjestyskulkemisessa. Talletetaan solmun w alkio väliaikaismuuttujaan t, ja siirretään solmun y tietoyksikkö solmuun w. Tällöin samalla poistetaan w:n aiempi sisältö. Poistetaan solmut x ja y puusta T operaation removeaboveexternal(x) avulla. Tämä korvaa y:n solmun x sisaruksella ja poistaa solmut x ja y. Palautetaan muuttujasta t alkio, joka oli aiemmin talletettuna solmuun w. 6. luku 332

44 17 88 28 w 65 97 29 54 82 76 y x 80 Kuva 6.6. Poisto binäärihakupuusta, jossa poistettavan solmun avain on 65 ja poistettavan molemmat lapset ovat sisäsolmuja: (a) Ennen poistoa. 78 (a) 6. luku 333

44 17 88 28 w 76 97 29 54 82 80 78 (b) Kuva 6.6. (b) Poiston jälkeen. 6. luku 334

Poistoalgoritmin analyysi on analoginen lisäykselle ja haulle. Jokaisessa solmussa käytäessä käytetään aikaa O(1) ja pahimmassa tapauksessa käytyjen solmun lukumäärä on suhteessa puun T korkeuteen h. Sanakirjaan D tehty metodin remove suoritus toimii ajassa O(h), missä h on puun korkeus. Edellisen poistometodin muunnelma removeall(k) toimii ajassa O(h+s), missä s on palautettujen eli puusta löydettyjen alkioiden määrä. Yhteenvetona todetaan binäärihakupuun T olevan tehokas n tietoyksikköä sisältävän järjestetyn sanakirjan toteutuksessa edellyttäen, että puun T korkeus on pieni. Parhaassa tapauksessa puun korkeus on h = log(n+1), joka antaa logaritmisen suoritusajan kaikille sanakirjaoperaatioille. Pahimmassa tapauksessa korkeus on n, mikä näyttää järjestetyltä sekvenssiltä. Tällainen esiintyy, mikäli esim. lisätään kasvavassa tai vähenevässä järjestyksessä joukko avaimia. 6. luku 335

Keskimäärin operaatiot toimivat ajassa O(log n) (monimutkaista perustelua ei tässä tarkastella). Operaatiot toimivat hyvin puun ollessa tasapainossa, missä ei ole erillisiä korkeita haaroja. Näin ollen tämä sanakirjatoteutus on käyttökelpoinen, jos sallitaan satunnaisesti hitaasti toimivat operaatiot lähellä pahimpaa tapausta olevissa tilanteissa. 6.4. AVL puut Edellä binäärihakupuun tilanteessa pahin tapaus tuotti lineaarisia suoritusaikoja sanakirjaoperaatioille, mikä on samaa luokkaa kuin sekvensseillä. Nyt kuvataan yksinkertainen menettely korjata tämä ongelma logaritmisen suoritusajan saamiseksi keskeisille sanakirjaoperaatioille. Suoraviivainen menettely on lisätä sääntö binäärihakupuun määrittelyyn puun logaritmisen korkeuden ylläpitämiseksi puussa T suhteessa solmujen määrään. Sääntö on seuraava korkeuden tasapainoisuus ominaisuus, joka luonnehtii binäärihakupuun T rakennetta sisäsolmujen korkeuden suhteen solmun v korkeus on pisin polku solmusta v lehteen. 6. luku 336

Korkeuden tasapainoisuusominaisuus: Jokaiselle T:n sisäsolmulle v sen lasten korkeudet eroavat toisistaan enintään yhden verran. Mitä tahansa binäärihakupuuta T, joka toteuttaa tämän ominaisuuden, kutsutaan AVL puuksi, mikä käsite on nimetty keksijöidensä nimien alkukirjainten mukaisesti (Adelson Velskij ja Landis). Esimerkki tästä on kuvassa 6.7. Korkeuden tasapainoisuusominaisuuden välitön seuraus on, että AVLpuun alipuu itse on AVL puu. Luonnollisesti on myös tärkeää sen seuraus, että korkeus pysyy pienenä, kuten seuraavassa esitetään. Lause 6.1. AVL puun T korkeus, kun puussa on n solmua, on O(log n). Perustelu jätetään esittämättä, mutta on helposti ymmärrettävissä lauseen tuloksen tuottava rinnastus, että AVL puu on likimain kuin täydellinen puu, jossa ei ole tyhjiä solmunpaikkoja. 6. luku 337

44 4 2 17 78 3 32 1 2 50 88 1 1 48 62 1 Kuva 6.7. Esimerkki AVL puusta. Avaimet on esitetty solmuissa ja näiden korkeudet solmujen vieressä. 6. luku 338

Lisäys Lisäyksen suorittaminen AVL puuhun alkaa operaation insertitem suorittamisella, kuten on kuvattu luvussa 6.3. (yleiselle) binäärihakupuulle. Tämä operaatio lisää uuden tietoyksikön puun T solmuun w, joka oli aiemmin lehti, ja tekee siitä sisäsolmun operaatiolla expandexternal. Se lisää siis kaksi lehtilasta solmulle w. Tämä toimenpide saattaa vahingoittaa korkeuden tasapainoisuusominaisuutta joidenkin solmujen korkeuksien kasvaessa yhdellä (kuva 6.8.(a)). Solmu w ja mahdollisesti jotkut sen esivanhemmat kasvattavat korkeuksiaan yhdellä. Kuvataan, kuinka T korjataan korkeuden tasapainoisuusominaisuuden säilyttämiseksi. Binäärihakupuun T solmu v on tasapainoitettu (balanced), jos sen lasten korkeuksien ero on enintään yksi; muussa tapauksessa se on epätasapainoitettu (unbalanced). Täten korkeuden tasapainoisuusominaisuus on ekvivalentti sen kanssa, että jokainen sisäsolmu on tasapainoitettu. Jos jokainen solmu on tasapainoitettu, niin selvästi T on tasapainoitettu ja lauseen 6.1. mukaan binäärihakupuun T korkeus on O(log n). 6. luku 339

Oletetaan puun T toteuttavan korkeuden tasapainoisuusominaisuuden ja on siis AVL puu alkutilanteessa, ennen lisäystä. Operaation expandexternal(w) suorittamisen puulle T jälkeen solmun w sekä joidenkin muiden solmujen korkeudet kasvavat. Ne kaikki sjaitsevat polulla solmusta w puun juureen. Ne ovat ainoat solmut, jotka voivat näin tulla epätasapainoitetuiksi (kuva 6.8.(a)). Mikäli näin tapahtuu, T ei ole tässä tilanteessa enää AVL puu. Niinpä tarvitaan mekanismi aiheutetun epätasapainon korjaamiseksi. Solmujen tasapaino palautetaan AVL puussa T yksioikoisella hae jakorjaa menettelyllä. Olkoon x ensimmäinen kohdattava solmu mentäessä ylös solmusta w kohti juurta, kun solmun x isovanhempi z on epätasapainoitettu (kuva 6.8.(a)). Solmu w voisi olla itse x. Olkoon y solmun x vanhempi, jolloin y on solmun z lapsi. Koska z tulee epätasapainoitetuksi lisäyksen tähden alipuuhun, jonka juuri y on, solmun y korkeus on yhtä kuin kaksi lisättynä solmun y sisaren korkeudella. Nyt uudelleentasapainoitetetaan alipuu (juurena z) suorittamalla rotaatio eli kierto (rotation). Tämä tehdään koodin 6.3. algoritmilla restructure kuvien 6.8. ja 6.9. mukaisesti. 6. luku 340

44 5 2 17 z 78 4 32 1 1 48 3 y 50 x 62 2 88 1 T 0 1 54 w T 2 T 3 Kuva 6.8. Esimerkki alkion avaimella 54 lisäyksestä AVL puuhun: (a) Uuden solmun lisäyksen jälkeen avainta 54 varten, jolloin avaimet 78 ja 44 sisältävät solmut tulevat epätasapainoitetuiksi. T 1 6. luku 341

44 4 2 17 x 62 3 32 1 2 y 50 z 78 2 1 1 48 54 88 1 T 2 T 0 T 1 T 3 Kuva 6.8. (jatkoa) (b) (Kaksois)rotaatio korjaa korkeuden tasapainoisuusominaisuuden. Solmujen korkeudet on esitetty ja pitää tunnistaa solmut x, y ja z. 6. luku 342

Rotaatio nimeää uudelleen väliaikaisesti solmut x, y ja z solmuiksi a, b ja c, jolloin a edeltää solmua b ja tämä solmua c välijärjestyskulkemisessa. On neljä erilaista mahdollista tapaa kuvata x, y ja z solmuiksi a, b ja c, kun esitettyjen edeltämisehtojen pitää olla voimassa. Nämä on annettu kuvassa 6.9. Rotaatio korvaa silloin solmun z solmulla b, tekee tämän lapsiksi solmut a ja c sekä tekee solmujen a ja c lapsiksi solmujen x, y ja z aiemmat lapset (muut kuin x ja y). Samalla ylläpidetään välijärjestyssuhde puun kaikille solmuille. Uudelleentasapainoittamisoperaatiota kutsutaan rotaatioksi tämän geometrisen rakentamistavan takia. Jos on b = y (koodi 6.3.), metodin restructure suoritusta sanotaan yksittäisrotaatioksi (single rotation), sillä se voidaan esittää solmun y kiertona yli solmun z (kuva 6.9. (a) ja (b)). Jos on b = x, tätä operaatiota kutsutaan kaksoisrotaatioksi (double rotation), koska se voidaan kuvata ensin solmun x kiertona solmun y yli ja sitten yli solmun z (kuva 6.9. (c) ja (d) sekä kuva 6.8.). Rotaatio muokkaa vanhempi lapsi suhteet puussa T ajassa O(1) säilyttäessään välijärjestyksen kaikille solmuille. 6. luku 343

Algorithm restructure(x): Input: Binäärihakupuun T solmu x, jolla on olemassa sekä vanhempi y ja isovanhempi z. Output: Puu T rotaatiolla korjattuna (joko yksittäis tai kaksoisrotaatio), missä mukana ovat x, y ja z. 1. Olkoon (a, b, c) solmujen x, y ja z välijärjestysesitys vasemmalta oikealle. Olkoon (T 0, T 1, T 2, T 3 ) solmujen x, y ja z neljän alipuun (juurena ei mikään solmuista x, y tai z) välijärjestysesitys vasemmalta oikealle. 2. Korvaa juureltaan z oleva alipuu uudella alipuulla, jonka juuri on b. 3. Olkoon a solmun b vasen lapsi, ja olkoot T 0 ja T 1 solmun a vasen ja oikea alipuu. 4. Olkoon c solmun b oikea lapsi. Olkoot T 2 ja T 3 solmun c vasen ja oikea alipuu. Koodi 6.3. Binäärihakupuun rotaatio, jolla puu korjataan jälleen AVLpuuksi. 6. luku 344

a=z b=y c=x yksittäisrotaatio a=z b=y c=x T 0 T 1 T2 T 3 (a) T 0 T 1 T 2 T 3 a=x b=y c=z yksittäisrotaatio a=x b=y c=z T 0 T 1 T 2 T 3 T 0 T 1 T 2 T 3 (b) Kuva 6.9. Rotaation kaavamainen esitys (koodista 6.6.): (a) ja (b) yksittäisrotaatiot. 6. luku 345

a=z b=x c=y kaksoisrotaatio a=z b=x c=y T 0 T 2 T 3 (c) T 0 T 1 T 2 T 3 T 1 a=y b=x c=z kaksoisrotaatio a=y b=x c=z T 0 T 1 T 2 T 3 (d) T 0 T 1 T 2 T 3 Kuva 6.9. (jatkoa) (c) ja (d) Kaksoisrotaatiot. 6. luku 346

Rotaatiota tarvitaan puun T solmujen korkeuksien muuttamiseksi, jotta tasapainoisuus palautuu. Rotaatio suoritetaan kutsulla restructure(x) solmun x isovanhemman z epätasapainoisuuden vuoksi. Tämä johtuu siitä, että toisella solmun x lapsista on nyt liian suuri korkeus suhteessa solmun z toiseen lapseen. Rotaation tuloksena siirretään ylös solmun x korkea lapsi samalla, kun solmun z matalaa lasta työnnetään alas. Tämän jälkeen kaikki juureltaan b alipuun solmut tulevat tasapainoisiksi (kuva 6.9.). Itse asiassa kysymyksessä on lokaali (local) palautus korkeuden tasapainoisuusominaisuudelle solmuissa x, y ja z. Lisäksi kun uuden tietoyksikön lisäyksen jälkeen alipuu juurenaan b korvaa aiemman juureltaan z, joka oli yhden verran korkeampi, solmun z kaikki aiemmat epätasapainoitetut esivanhemmat tulevat tasapainoitetuiksi. Tämä rotaation vaikutus on näin myös globaali (global) korkeuden tasapainoisuusominaisuuden palautus. 6. luku 347

Poisto Jälleen lähdetään muodostamaan operaatiota AVL puulle käyttäen säännöllisen binäärihakupuun algoritmia hyväksi. Tämä lähestymistapa saattaa kuitenkin rikkoa korkeuden tasapainoisuuden, kun sisäsolmun poiston jälkeen operaatiolla removeaboveexternal ja toisen lapsen noston jälkeen sisäsolmun sijaan, epätasapainoinen solmu voi esiintyä puun T polulla tuhotun solmun vanhemmasta w juureen (kuva 6.10.(a)). Sellaisia solmuja voi olla enintään yksi. Käytetään jälleen rotaatiota tasapainoisuuden palauttamiseksi AVLpuuhun. Olkoon z ensimmäinen tasapainottamaton solmu, joka kohdataan lähdettäessä solmusta w kohti juurta. Olkoon y solmun z lapsi, jonka korkeus on suurempi kuin toisen (y ei ole solmun w esivanhempi). Olkoon x solmun y lapsi, jonka korkeus on toista suurempi. Tämä ei ole välttämättä yksikäsitteinen, koska solmun y alipuut voivat olla yhtä korkeita. Joka tapauksessa suoritetaan restructure(x), joka palauttaa lokaalisesti korkeuden tasapainoisuusominaisuuden alipuussa alkuperäiseltä juureltaan z ja nyt väliaikaiselta nimeltään b (kuva 6.10.(b)). 6. luku 348

z 44 4 1 17 y 62 3 w 2 50 x 78 2 T 0 1 1 0 48 54 88 1 32 T 2 (a) T 1 T 3 Kuva 6.10. (alku) Alkion avaimeltaan 32 poisto kuvan 6.7. AVL puusta: (a) avaimen 32 sisältäneen solmun poiston jälkeen juuri tulee epätasapainoitetuksi. 6. luku 349

y 4 62 z x 44 3 78 2 1 2 17 1 48 50 54 1 0 T 2 88 1 T 0 T 3 (b) T 1 Kuva 6.10. (loppu) (b) Yksittäisrotaatio palauttaa korkeuden tasapainoisuusominaisuuden. 6. luku 350

Rotaatio saattaa vähentää alipuun juureltaan b korkeutta yhdellä ja tästä johtuen tehdä solmun b esivanhemman epätasapainoitetuksi. Tämän vuoksi yksi rotaatio ei ehkä riitä, vaan niitä on toistettava ylöspäin puussa T, kunnes ei ole enää epätasapainoitettuja solmuja. Kun puun korkeus on O(log n) lauseen 6.1. mukaan, O(log n) rotaatiota on riittävästi palauttamaan korkeuden tasapainoisuusominaisuuden. Analyysi Melko suoraviivaisesti on selvitettävissä, että sanakirjaoperaatiot insertitem ja remove tarvitsevat aikaa käymissään solmuissa O(1) kussakin. Solmut ovat puun T polulla juuresta lehteen. Kun puun korkeus on O(log n), niin se on samalla näiden operaatioiden aikakompleksisuus AVL puutoteutukselle sanakirjaa varten. Operaatio findelement on samaa luokkaa. Taulukossa 6.2. on yhteenveto suoritusajoista. 6. luku 351

Taulukko 6.2. AVL puuna toteutetun, n alkioisen sanakirjan suorituskyvyt eri metodien tapauksissa, missä s on palautettujen samaa avainta olevien alkioiden määrä. Tilavaatimus on O(n). operaatio size, isempty suoritusaika Θ(1) findelement, insertitem, remove O(log n) findallelements, removeall O(log n + s) 6.5. Hajautustaulut Sanakirjan toteuttamisessa hajautustauluna (hash table) hyödynnetään avaimia, jotka ovat järjestämättömiä niillä annetaan alkion osoite. Sovellusesimerkkinä on kääntäjän symbolitaulu. 6. luku 352

Vaikka sanakirjan toteutus hajautustaululla vaatii pahimmassa tapauksessa suoritusajan O(n), missä n on tietoyksiköiden lukumäärä, hyvin toteutetun hajautustaulun pitäisi kyetä suorittamaan nämä operaatiot keskimäärin ajassa O(1). Hajautustaulu sisältää kaksi osaa, hajautustaulukon ja hajautusfunktion. Hajautustaulukko Hajautus tai lokerotaulukko (hash, bucket array) on kokoa N oleva taulukko A, jonka jokainen alkio mielletään lokeroksi. Kokonaisluku N määrittelee taulukon kapasiteetin (capacity). Jos sanakirjassa käsiteltävät avaimet ovat kokonaislukuja väliltä [0,N 1], tämä hajautustaulukko on kaikki, mitä tarvitaan. Alkio e, jonka avain on k, talletetaan yksinkertaisesti lokeroon A[k]. Jokainen lokero, johon ei ole talletettu kyseisellä hetkellä mitään, merkitään tiedolla EI_AVAINTA. Jos avaimet eivät ole yksikäsitteisiä, kaksi alkiota voivat saada saman lokeron taulukosta. Tällöin tapahtuu törmäys (collision), joka on hoidettava sopivasti. 6. luku 353

Jos taulukon A jokainen lokero sisältää ainoastaan yhden alkion, ongelma esiintyy törmäyksen muodossa eli saman avaimen sattuessa kahdelle eri alkiolle. Tämä hoidetaan tehokkaasti tallettamalla alkioiden sekvenssi lokeroon A[k], kun jokaisen sekvenssin alkion avain on kyseinen k. Tämä törmäysratkaisu tunnetaan ketjutuksena (chaining). Nyt sanakirjaoperaatiot ovat suoritettavissa seuraavasti: Operaatio findelement(k) palauttaa mielivaltaisesti valitun alkion sekvenssistä A[k], kunhan tämä lokero ei ole tyhjä. Jos lokero on tyhjä, se palauttaa erikoisalkion EI_AVAINTA. Operaatio insertitem(k,e) käsittää alkion e lisäyksen sekvenssiin A[k]. Operaatio remove(k) poistaa ja palauttaa tuloksenaan sekvenssin A[k] mielivaltaisesti valitun alkion, kunhan tämä lokero ei ole tyhjä. Mikäli lokero on tyhjä, operaatio palauttaa erikoisalkion EI_AVAINTA. 6. luku 354

Mikäli avaimet ovat yksikäsitteisiä, haut, lisäykset ja poistot vaativat pahimmassa tapauksessa ajan O(1). Vaikka tämä on optimaalisen tehokasta, hajautustauluihin liittyy kaksi heikkoutta. Ne vaativat tilaa Θ(N), joka ei välttämättä ole suhteessa tietoyksiköiden todelliseen määrään n sanakirjassa. Jos niiden ero on suuri, tietorakenne tuhlaa tilaa. Toisena heikkoutena on se, että hajautustaulukko vaatii kokonaislukuavaimia väliltä [0,N 1], mikä ei toisinaan tule kysymykseen. Hajautusfunktiot Haluttaessa tallettaa mielivaltaisesti (arbitrary) valittuja avaimia hajautustaulukkoon on löydettävä menetelmä kuvata avain k kokonaislukuvuksi väliltä [0,N 1]. Tätä varten on oltava yhtenäinen tapa määrätä kokonaisluku kullekin avaimelle. Tätä kokonaislukua kutsutaan avaimen k hajautuskoodiksi (hash code). 6. luku 355

Avaimella k tulee olla ominaisuus f(k 1 ) = f(k 2 ), jos avaimet k 1 ja k 2 ovat samoja (geneerisessä mielessä). Tietysti voi olla f(k 1 ) = f(k 2 ), vaikka on k 1 k 2. Kun avainten k 1 ja k 2 yhtäsuuruus riippuu yhtäsuuruuden vertaamisesta, hajautuskoodien määrääminen avaimille riippuu myös tästä. Esim. 6.2. Seuraavien avaintyyppien yhtenäisiä hajautuskoodeja ovat: Liukuluku (floating point number): Lasketaan mantissa ja eksponentti yhteen käsitellen niitä kuin kokonaislukuja. Merkkijono (string): Lasketaan merkkijonojen merkkien ASCII (tai muu) koodiarvot yhteen. Avainsana arvo pari (keyword value pair): Käytetään avainsanan (merkkijono) hajautuskoodia. 6. luku 356

Kun hajautuskoodi on käytettävissä, hajautustaulukkomenetelmää voidaan soveltaa mielivaltaisesti valituille avaimille käyttäen hajautuskoodia f(k) avaimen k sijasta. Talletetaan tietoyksikkö (k,e) sekvenssiin, johon viitataan alkiolla A[f(k)] indeksoituna hajautuskoodilla f(k). Tällöin törmäysten esiintyminen tekee sanakirjaoperaatiot findelement ja remove pelkkien avaimien soveltamisen tapausta monimutkaisemmaksi. Kaksi eri avainta, k 1 ja k 2, saattavat kuvautua samalle hajautuskoodille, i = f(k 1 ) = f(k 2 ), ja mennä näin samaan lokeroon A[i]. Suoritettaessa operaatiota findelement(k 1 ) ei voi yksinkertaisesti palauttaa mielivaltaisesti valitun tietoyksikön alkiota lokerosta A[i], koska tällä voisi olla avaimena k 2. Lokeroa A[i] pitää käsitellä kuin sanakirjaa ja suorittaa operaatio findelement(k 1 ) sille. Samaa pitää paikkansa poisto operaatiolle remove. Kokoa N oleva hajautustaulukko voidaan nähdä täten kokoelmana sekvenssiperusteisia sanakirjoja, joista kukin tallettaa saman hajautusfunktioarvon tuottavien avainten tietoyksiköt. 6. luku 357

Miten käsitellään tilanne, että hajautustaulukon kapasiteetti N ei riitä, kun käytettäisiin taulukon ulkopuolelle menevää hajautusfunktion f(k) arvoa? Ei riitä määrätä kokonaislukuarvo hajautuskoodiksi, vaan pitää kuvata se vielä välille [0,N 1]. Tätä tiivistystä kutsutaan hajautukseksi (hashing). Sen tulisi kuvata hajautuskoodit tasaisen (uniform) jakauman mukaisesti. Tiivistyksen tuottaa hajautusfunktio (hash function). Kun sanakirjaoperaatiot sekvenssitoteutuksessa vaativat aikaa suhteessa sekvenssin kokoon (luku 6.2.), pitää tähdätä siihen, että kuhunkin lokeroon talletettaisiin mahdollisimman vähän avaimia. Pahimmassa tapauksessa avainten määrä yhdessä lokerossa voi olla kuitenkin n. Näin käy silloin, kun sanakirjan kaikki avaimet törmäävät eli niillä on sama hajautuskoodi. 6. luku 358

Jos hajautuskoodit ovat tasaisesti jakautuneet välille [0,N 1], lokeron avainten lukumäärän odotusarvo (keskiarvo) on n/n, mikä on O(1), jos n on O(N). Luku n/n, joka on tietoyksiköiden ja lokerojen määrien suhde, on hajautustaulun täyttöaste (load factor). Törmäysten todennäköisyyksien pienentämiseksi täyttöasteen tulee olla tyypillisesti alle 1 ja tavallinen valinta on 0.75. Jos täyttöaste nousee selvästi määrätyn rajan yli, taulukko pitää tavallisesti muodostaa uudelleen lisätilan saamista varten. Yleensä taulukko suurennetaan kaksinkertaiseksi. Tällöin pitää kaikkien lokerojen alkiot uudelleenhajauttaa (rehash). Tasaisen jakauman saamiseksi hajautuskoodeilla välille [0,N 1] hajautusfunktio on valittava sopivasti. Yksinkertaisessa tapauksessa, jossa käytetään suoraan kokonaislukuavaimia, voidaan soveltaa hajautusfunktiota H(k) = k mod N. 6. luku 359

Kun N on alkuluku (prime number), tämä hajautusfunktio levittää hajautettujen arvojen jakaumaa. Jos se ei ole alkuluku, todennäköistä on, että jakaumassa toistuu samoja hajautuskoodeja aiheuttaen törmäyksiä. Jos esimerkiksi hajautetaan avaimet {200, 205, 210, 215, 220,, 595} hajautustaulukkoon kooltaan 100, niin jokainen hajautuskoodi törmää kolmen muun kanssa. Jos sama avainjoukko hajautetaan taulukkoon kooltaan 101, ei esiinny yhtään törmäystä. Kun hajautusfunktio valitaan hyvin, sen tulisi taata, että kahden eri avaimen hajautuksen todennäköisyys samaan lokeroon olisi enintään 1/N. Yksinkertainen hajautustaulukko on esitetty kuvassa 6.11. Koon N valinta alkuluvuksi ei aina riitä, sillä jos avaimia esiintyy toistuvasti muodossa in + j useille eri arvoille i, törmäyksiä esiintyy yhä. Niinpä parempi hajautusfunktio on h(k) = (ak + b) mod N, missä N on alkuluku, a 0 ja a mod N 0. Lisäksi a ja b ovat einegatiivisia kokonaislukuja, jotka valitaan satunnaisesti. 6. luku 360

A 0 1 2 3 4 5 6 7 8 9 10 11 12 41 28 54 18 36 10 90 12 38 25 Kuva 6.11. Esimerkki hajautustaulusta kooltaan 13, missä on 10 kokonaislukuavainta ketjutettuna. Hajautusfunktio on h(k) = k mod 13. 6. luku 361

Tällainen hajautusfunktio levittää n alkiota melko tasaisesti välille [0,N 1]. Operaatioiden findelement, insertitem ja remove keskimääräinen suoritusaika on tällöin O( n/n ). Hyvällä hajautusfunktiolla ja taulukolla voidaan tavalliset sanakirjaoperaatiot toteuttaa toimiviksi keskimäärin ajassa O(1) edellyttäen, että n on O(N). Vaihtoehtoisia törmäystenkäsittelytapoja Edellä oletettiin törmäykset käsiteltävän ketjutussäännön mukaan. Vaikka ketjutussäännöllä on useita hyviä puolia, sillä on muuan heikkous. Se tarvitsee aputietorakenteen, sekvenssin, törmäysten vuoksi. Yhtenä vaihtoehtona on tallettaa jokaiseen lokeroon ainoastaan yksi tietoyksikkö. Tämä säästää tilaa, kun aputietorakennetta ei tarvita, mutta törmäystenkäsittely on aiempaa mutkikkaampaa. 6. luku 362

Lineaarisessa hajautuksessa (linear probing) törmäyksessä yritetään lisätä tietoyksikkö (k,e), mutta lokero A[i] on jo täynnä, missä i = f(k), jolloin yritetään lokeroa A[(i+1) mod N]. Jos tämäkin on varattu, yritetään lokeroa A[(i+2) mod N] jne., kunnes löydetään tyhjä lokero, johon uusi tietoyksikkö talletetaan. Täyttöasteen on aina oltava enintään 1, siis n N. Huonoina puolina ovat poistojen hankaluus ja tietoyksiköiden ryvästyminen. Toinen vaihtoehto on neliöllinen (quadratic) hajautusfunktio, joka iteratiivisesti yrittää lokeroihin A[(i+f(j)) mod N] arvoilla j = 1,2,3,, missä f(j) = j 2. Se vähentää ryvästyksen muodostumista (tosin niitä tulee erilaisina), mutta poistot ovat monimutkaisia. Kolmas vaihtoehto on kaksoishajautus (double hashing). Normaalisti käytetyn hajautusfunktion lisäksi käytetään törmäysten sattuessa toista hajautinta, joka ei saa saada arvoja 0. Nämä esitetyt hajauttimet edustavat suljettua (closed) hajautusta, kun taas edellä olevat ketjutusta käyttävät ovat avoimia (open). 6. luku 363