4. Perustietorakenteet

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

2. Perustietorakenteet

5. Hash-taulut ja binääriset etsintäpuut

811312A Tietorakenteet ja algoritmit IV Perustietorakenteet

811312A Tietorakenteet ja algoritmit, , Harjoitus 5, Ratkaisu

8. Puna-mustat puut ja tietorakenteiden täydentäminen

811312A Tietorakenteet ja algoritmit II Perustietorakenteet

811312A Tietorakenteet ja algoritmit, , Harjoitus 5, Ratkaisu

Algoritmit 2. Luento 2 To Timo Männikkö

A TIETORAKENTEET JA ALGORITMIT

Algoritmit 2. Luento 2 Ke Timo Männikkö

Hakupuut. tässä luvussa tarkastelemme puita tiedon tallennusrakenteina

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

811312A Tietorakenteet ja algoritmit Kertausta jälkiosasta

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.

A TIETORAKENTEET JA ALGORITMIT

Algoritmit 2. Luento 7 Ti Timo Männikkö

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

Algoritmit 2. Luento 4 To Timo Männikkö

Algoritmit 1. Luento 7 Ti Timo Männikkö

811312A Tietorakenteet ja algoritmit Kertausta jälkiosasta

Algoritmit 2. Luento 3 Ti Timo Männikkö

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

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

Algoritmit 2. Luento 3 Ti Timo Männikkö

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

Algoritmit 2. Luento 6 Ke Timo Männikkö

Algoritmit 2. Luento 4 Ke Timo Männikkö

811312A Tietorakenteet ja algoritmit, , Harjoitus 7, ratkaisu

Algoritmit 2. Luento 5 Ti Timo Männikkö

Algoritmit 1. Luento 4 Ke Timo Männikkö

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

Kierros 4: Binäärihakupuut

CS-A1140 Tietorakenteet ja algoritmit

14 Tasapainotetut puurakenteet

18. Abstraktit tietotyypit 18.1

Ohjelmoinnin perusteet Y Python

Algoritmit 2. Luento 6 To Timo Männikkö

1.1 Pino (stack) Koodiluonnos. Graafinen esitys ...

Sisällys. 18. Abstraktit tietotyypit. Johdanto. Johdanto

Ohjelmoinnin peruskurssi Y1

Luku 3. Listankäsittelyä. 3.1 Listat

TKT20001 Tietorakenteet ja algoritmit Erilliskoe , malliratkaisut (Jyrki Kivinen)

Luku 8. Aluekyselyt. 8.1 Summataulukko

Algoritmit 2. Luento 5 Ti Timo Männikkö

Tieto- ja tallennusrakenteet

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

4 Tehokkuus ja algoritmien suunnittelu

Algoritmit 1. Luento 8 Ke Timo Männikkö

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

7. Tasapainoitetut hakupuut

811312A Tietorakenteet ja algoritmit Kertausta kurssin alkuosasta

6. Sanakirjat. 6. luku 298

Tietorakenteet ja algoritmit Johdanto Lauri Malmi / Ari Korhonen

Algoritmit 1. Luento 3 Ti Timo Männikkö

1.1 Tavallinen binäärihakupuu

Datatähti 2019 loppu

10. Painotetut graafit

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

Algoritmit 2. Demot Timo Männikkö

Tietorakenteet ja algoritmit - syksy

B + -puut. Kerttu Pollari-Malmi

Tarkennamme geneeristä painamiskorotusalgoritmia

58131 Tietorakenteet ja algoritmit (kevät 2014) Uusinta- ja erilliskoe, , vastauksia

Algoritmit 1. Luento 12 Ti Timo Männikkö

Algoritmit 1. Luento 12 Ke Timo Männikkö

Binäärihaun vertailujärjestys

Miten käydä läpi puun alkiot (traversal)?

Algoritmit 2. Luento 9 Ti Timo Männikkö

ALGORITMIT 1 DEMOVASTAUKSET KEVÄT 2012

Algoritmit 1. Luento 13 Ti Timo Männikkö

Algoritmit 1. Luento 9 Ti Timo Männikkö

Algoritmit 1. Luento 5 Ti Timo Männikkö

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

Luku 6. Dynaaminen ohjelmointi. 6.1 Funktion muisti

4. Joukkojen käsittely

Algoritmit 1. Luento 10 Ke Timo Männikkö

Tietorakenteet, laskuharjoitus 7, ratkaisuja

Tietorakenteet, laskuharjoitus 3, ratkaisuja

A TIETORAKENTEET JA ALGORITMIT

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.

Algoritmit 1. Luento 1 Ti Timo Männikkö

etunimi, sukunimi ja opiskelijanumero ja näillä

811312A Tietorakenteet ja algoritmit III Lajittelualgoritmeista

Pino S on abstrakti tietotyyppi, jolla on ainakin perusmetodit:

1 Erilaisia tapoja järjestää


Algoritmit 1. Luento 6 Ke Timo Männikkö

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

811312A Tietorakenteet ja algoritmit Kertausta kurssin alkuosasta

T Syksy 2004 Logiikka tietotekniikassa: perusteet Laskuharjoitus 7 (opetusmoniste, kappaleet )

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

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

Luku 7. Verkkoalgoritmit. 7.1 Määritelmiä

811312A Tietorakenteet ja algoritmit , Harjoitus 2 ratkaisu

Luento 2: Tiedostot ja tiedon varastointi

Harjoitustyö: virtuaalikone

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

811312A Tietorakenteet ja algoritmit, , Harjoitus 3, Ratkaisu

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

Transkriptio:

4. Perustietorakenteet Tässä osassa käsitellään erilaisia tietorakenteita, joita algoritmit käyttävät toimintansa perustana. Aluksi käydään läpi tietorakenteen abstrakti määritelmä. Tämän jälkeen käsitellään kolme alkeistietorakennetta: pino, jono sekä linkitetty lista. Sitten esitellään hash-taulukot ja binääriset etsintäpuut sekä puna-mustat puut ja niiden perusalgoritmit. Käsiteltävät tietorakenteet liittyvät olennaisesti tiedon hakemiseen. Teoksessa [Cor] käsitellään tässä esitettäviä asioita seuraavasti: tietorakenteiden määritelmä sekä alkeistietorakenteet luku 10; hash-taulukot luku 11; binääriset etsintäpuut luku 12; puna-mustat puut luku 13. Osan lopuksi käsitellään vielä tietorakenteiden täydentämistä, johon perehdytään myös kirjan [Cor] luvussa 14. 4.1. Abstrakti määritelmä Erilaiset joukot ovat keskeisessä roolissa sekä tietotekniikassa että yleisemmin matematiikassa. Siinä missä matemaattiset joukot ovat staattisia, algoritmien käyttämät joukot voivat kasvaa, pienentyä tai muuttua sisällöltään ajan funktiona. Kutsutaan näitä yhteisnimityksellä dynaamiset joukot. Jotta algoritmit pääsevät muokkaamaan dynaamista joukkoa, on joukon ensin toteutettava tiettyjä operaatioita kuten esimerkiksi lisää alkio tai poista alkio. Dynaamiseen joukkoon liittyvät operaatiot kokonaisuudessaan määrittävät rajapinnan jota algoritmi voi käyttää joukkoa muokatessaan. Tämän alustuksen kautta saadaan tietorakenteelle seuraava abstrakti määritelmä: Tietorakenne on äärellinen dynaaminen joukko, joka toteuttaa rajapintanaan sarjan operaatioita joiden kautta algoritmi voi muokata joukkoa. Ylläolevassa määritelmässä on kaksi ydinasiaa. Ensimmäinen on tietorakenteen määrittely joukkona, seuraten siten tiivisti yleisempää matemaattista käytäntöä. Toinen on sarja operaatioita, joka muodostaa tietorakenteen rajapinnan. Usein tietorakenteen suunnittelu alkaa tämän rajapinnan määrittelyllä (operaatioiden rakenne ja tehokkuusvaatimukset), jonka jälkeen yksittäiset operaatiot toteutetaan tietorakenteeseen vaatimuksia noudattaen. Huomaa, että ylläoleva määritelmä on siis abstrakti eikä vielä ota millään tavalla kantaa siihen, miten tietorakenne käytännössä toteutetaan. Tämän takia määritelmä eroaa teknologialäheisemmistä määritelmistä kuten esim. tietty tapa säilöä ja järjestellä dataa tietokoneen muistissa. Tietorakenteen sisäinen rakenne koostuu joukosta alkioita, joilla on sisäinen rakenne ja joihin voidaan toteutetussa rakenteessa yksikäsitteisesti osoittaa osoittimella. Oliosuuntautuneen ajattelun mukaisesti voidaan ajatella että kukin alkio on olio ja että sisäinen rakenne muodostuu joukosta kenttiä. Tyypillisesti yhtä alkiota edustava olio sisältää seuraavat kentät: Avain, joka identifioi yksittäisen alkion tietorakenteen sisällä. Usein avainten tulee olla järjestettyjä (totally ordered set), jolloin kahdelle avaimelle pätee joukon sisällä täsmälleen yksi seuraavista ehdoista: a < b, a = b tai a > b. Tämä mahdollistaa avainten keskinäisen vertailun. Järjestysdata, joka määrittelee miten alkiot suhtautuvat toisiinsa joukon sisällä. Operaatiot voivat muokata tätä dataa muokatessaan tietorakenteen sisäistä rakennetta. Satelliittidata, joka on talletettuna alkioon mutta jolla ei ole merkitystä tietorakenteen sisäisen toiminnan kannalta.

Tietorakenteeseen liittyvä operaatiot voidaan korkealla tasolla jaotella kahteen kategoriaan: kyselyt jotka palauttavat tietoa rakenteesta muokkaamatta sitä, sekä muokkausoperaatiot jotka muuttavat joukon rakennetta. Operaation aikatehokkuus ilmaistaan joukon koon mukaan. Joukossa jossa on n alkiota, tietyn operaation tehokkuus voi olla esim. O(n). Seuraavassa listataan yleisimpiä operaatioita. Huomaa että usein tietty algoritmi vaatii ainoastaan osan näistä operaatioista toteutetuiksi. ETSI(S,k) Kysely joka palauttaa olion osoittimen x joukosta S siten, että x.key = k, tai NIL jos haettua oliota ei ole tallennettuna joukkoon. Huomaa että osoitin x eroaa avaimesta k: Osoitin on alustaspesifinen keino osoittaa tiettyyn olioon joukossa ja avain on tietorakenteen sisäisesti uniikki tunniste. LISÄÄ(S,x) Muokkausoperaatio joka kasvattaa joukkoa S uudella elementillä jonka osoitin on x. Huomaa että uuteen olioon liittyvät arvot oletetaan alustetuiksi valideilla kentillä. POISTA(S,x) Muokkausoperaatio joka poistaa joukosta S elementin osoittimella x. Huomaa että operaatio käyttää osoitinta avaimen sijasta. MINIMI(S) Kysely monotonisesti järjestettyyn joukkoon S joka palauttaa osoittimen alkioon jonka avainarvo on pienin. MAKSIMI(S) Kysely monotonisesti järjestettyyn joukkoon S joka palauttaa osoittimen alkioon jonka avainarvo on suurin. SEURAAJA(S,x) Kysely, joka järjestettyjen avainten muodostamassa joukossa S palauttaa osoittimen alkioon joka seuraa alkiota osoittimella x. EDELTÄJÄ(S,x) Kysely, joka järjestettyjen avainten muodostamassa joukossa S palauttaa osoittimen alkioon joka edeltää alkiota osoittimella x. 4.2. Alkeistietorakenteet Aluksi esitellään kolme äärellistä dynaamista joukkoa, pino, jono ja linkitetty lista, jotka toimivat johdantona tietorakenteille. Jokainen alkeistietorakenne toteuttaa rakenteensa puolesta yllämainitut operaatiot hieman eri tavalla, joten operaatioiden lähempi tarkastelu auttaa tietorakenteiden erojen havainnollistamisessa. Pino (Stack) on dynaaminen joukko jolle POISTA operaation kohdealkio on ennaltamäärätty. Tämä johtuu pinon sisäisestä rakenteesta, joka muistuttaa fyysisten esineiden pinoamista. Pino toteuttaa niinkutsutun viimeiseksi sisään, ensimmäiseksi ulos (last in, first out, LIFO) menettelytavan alkioiden määrän muokkaamisessa. Pinolle määritelty LISÄÄ operaatio on nimeltään PUSH, ja se lisää uuden alkion pinon huipulle. POISTA operaatiota vastaa POP, joka poistaa yhden alkion pinon huipulta.

Operaatioiden nimet viittaavat reaalimaailman malleihin pinoista, kuten esimerkiksi lautaspino johon lisätään lautasia huipulle sekä myös otetaan lautasia huipulta. Kuvassa 4.1 on havainnollistettu PUSH ja POP operaatiot, sekä pinon toteutus käyttäen indeksoitua taulukkoa. Taulukon indeksi S.pää (top) osoittaa aina viimeisimpänä lisättyyn alkioon. Kun S.pää = 0, pino on tyhjä, ja POISTA operaation kutsuminen tässä tilanteessa johtaa pinon alivuotoon, joka on algoritmin sisällä käsiteltävä virhetilanne. Yhtäläisesti ylivuotoon johtava virhetilanne syntyy jos LISÄÄ operaatiota kutsutaan tilanteessa jossa S.pää = n, eli kapasiteetiltaan n alkiota olevaan pinoon yritetään lisätä uutta alkiota. Huomaa että lisätilan dynaamista allokointia ei tässä esimerkkitilanteessa ole. Kuva 4.1. Pinon perusominaisuudet. Määritellään taulukolla toteutettuun pinoon liittyvät perusoperaatiot ylläolevien ominaisuuksien perusteella seuraavasti. Huomaa ettei pinon ylivuotoa käsitellä tässä esimerkissä. Kaikkien operaatioiden aikakompleksisuus on vakiomuotoa T(n) = O(1). PINO_TYHJÄ(S) 1. if S.pää == 0 2. return TRUE 3. else 4. return FALSE PINO_LISÄÄ(S,x) 1. S.pää = S.pää + 1 2. S[S.pää] = x # PUSH PINO_POISTA(S) # POP 1. if PINO_TYHJÄ(S) 2. error Alivuoto pinossa %d, %S 3. else 4. S.pää = S.pää 1 5. return S[S.pää + 1]

Jono (Queue) on tietorakenne joka toteuttaa niinkutsutun ensimmäiseksi sisään, ensimmäiseksi ulos (first in, first out,fifo) menettelytavan alkioiden määrän muokkaamisessa. Jonon reaalimalli on mikä tahansa jono esim. ihmisiä, ja jonolla on pää (head) ja häntä (tail). POISTA operaation (DEQUEUE) osoitin on rakenteen puolesta määritelty aina jonon päähän, ja LISÄÄ operaatio (ENQUEUE) lisää uuden alkion jonon häntään. Kuva 4.2 havainnollistaa jonon toteutusta indeksoidulla taulukolla. Taulukolla on nyt kaksi erillisesti määriteltyä indeksiä. Indeksi Q.pää osoittaa aina jonon päähän ja indeksi Q.häntä jonon hännässä olevaan seuraavaan tyhjään paikkaan. Jonon oma attribuutti pituus määrittelee jonon kapasiteetin. Itse jono koostuu alkioista Q[pää], Q[pää + 1], Q[pää + 2],..., Q[häntä 1]. Jos Q[pää] = Q[häntä], jono on tyhjä, lisäksi tyhjän jonon alkutilanteelle pätee pää = häntä = 1. Paikkaa indeksillä Q[häntä] ei voi täyttää, koska tällöin jonon päitä edustavat indeksit limittyvät ja jono tulkitaa tyhjäksi. Huomaa että taulukolla toteutettu jono voi kiertää taulukon lopusta sen alkupuolelle, eli indeksi 1 seuraa indeksiä n. Kuva 4.2. Jonon perusominaisuudet. Määritellään taulukolla toteutettuun jonoon liittyvät perusoperaatiot ylläolevien ominaisuuksien perusteella seuraavasti. Huomaa että jonoon liittyvissä LISÄÄ ja POISTA operaatioissa on mahdollisuus yli- ja alivuotoon, joita ei kuitenkaan seuraavissa listauksissa käsitellä. Operaatioiden aikakompleksisuus on vakiomuotoa T(n) = O(1).

JONO_LISÄÄ(Q,x) 1. Q[Q.häntä] = x 2. if Q.häntä == Q.pituus 3. Q.häntä = 1 4. else 5. Q.häntä = Q.häntä + 1 JONO_POISTA(Q) 1. x = Q[Q.pää] 2. if Q.pää == Q.pituus 3. Q.pää = 1 4. else 5. Q.pää = Q.pää + 1 5. return x Linkitetty lista (Linked list) on tietorakenne jossa alkiot ovat lineaarisessa järjestyksessä. Toisin kuin aiemmissa taulukkopohjaisissa rakenteissa, linkitetyssä listassa lineaarisuus määräytyy jokaisessa alkiossa olevien osoittimien perusteella. Jokainen alkio sisältää osoittimen sekä listassa edellisenä että seuraavana olevaan alkioon, ja operaatiot mahdollistavat listan muokkaamisen osoittimien arvoja muuttamalla. Seuraavissa esimerkeissä kuvataan kahteen suuntaan linkitetyn listan toiminnallisuutta, tämä sekä kaksi muuta listatyyppiä ovat havainnollistettuna kuvassa 4.3. Kuva 4.3. Linkitetyn listan kolme toteutusvaihtoehtoa: Yhteen suuntaan (a), kahteen suuntaan (b) sekä renkaan muotoon (c) linkitetty lista. Linkitetyn listan alkio sisältää kolme kenttää avain, osoitin edelliseen alkioon sekä osoitin seuraavaan alkioon sekä mahdollisesti satelliittidataa. Jos alkiolle x pätee x.edellinen = NIL, alkio x on listan pää (head). Jos taas alkiolle x pätee x.seuraava = NIL, alkio x on listan häntä (tail). Tietorakenteen sisäinen attribuutti L.pää osoittaa listan päähän, ja jos L.pää = NIL, lista on tyhjä. Operaatio LISTA_ETSI(L,k) etsii ei-järjestetystä listasta L ensimmäisen alkion jonka avaimen arvo on k yksinkertaisella lineaarisella hakumenetelmällä, ja palauttaa osoittimen tähän alkioon (NIL jos avainarvoa ei löydy). Käsitellessään n alkiota sisältävää listaa, ETSI_LISTA operaation aikatehokkuus on muotoa T(n) = Θ(n), sillä huonoimmassa tapauksessa kaikki listan alkiot on käytävä yksi kerrallaan läpi.

Operaatio LISTA_LISÄÄ(L,x) lomittaa uuden alustetun alkion x listan päähän muuttamalla listan pää[l] attribuuttia sekä edellisen päänä toimineen alkion sisäistä edellinen osoitinta. Koska listan päähän lisääminen on vakiokestoinen operaatio, sille pätee T(n) = O(1). Operaatio LISTA_POISTA(L,x) lomittaa alkion x ulos listasta L muokkaamalla sitä ympäröivien alkioiden osoittimia. Uudet osoittimet asetetaan osoittamaan ohi alkiosta x, jolloin se ei ole enää mukana listassa. Operaatio on vakiokestoinen ja siten muotoa T(n) = O(1), mutta tapauksessa jossa osoitinta x ei ole valmiiksi saatavilla, on kutsuttava ensin LISTA_ETSI operaatiota jonka ansiosta lopullinen tehokkuus on muotoa T(n) = Θ(n). Linkitetyn listan lisäys- ja poisto-operaatioiden suorituskykyä voidaan jonkin verran parantaa käyttämällä nk. vartioalkioita (sentinel) [Cor], joiden käsittely sivuutetaan tässä yhteydessä. LISTA_ETSI(L,k) 1. x = L.pää 2. while x NIL and x.avain k 3. x = x.seuraava 4. return x LISTA_LISÄÄ(L,x) 1. x.seuraava = L.pää 2. if L.pää NIL 3. L.pää.edellinen = x 4. L.pää = x 5. x.edellinen = NIL LISTA_POISTA(L,x) 1. if x.edellinen NIL 2. (x.edellinen).seuraava = x.seuraava 3. else 4. L.pää = x.seuraava 5. if x.seuraava NIL 6. (x.seuraava).edellinen = edellinen[x] 4.3. Hash-taulukot Monissa sovelluksissa tarvitaan jonkin tyyppistä joukkoa, johon voidaan kohdistaa lisäys- poistoja hakuoperaatioita. Esimerkiksi ohjelmointikielen kääntäjissä käytetään symbolitaulua, jonka avulla ohjelmassa esiintyvät muuttujat liitetään käytettävän kielen tunnisteisiin. Hash-taulukoiden avulla voidaan toteuttaa tehokkaasti tällainen ratkaisu; hash-taulukoinnissa käytetään ns. tiivisteeli hashfunktioita. Hashfunktioilla on muitakin sovelluksia kuten tarkistussumman laskeminen. Oletetaan, että ollaan tallentamassa tietoalkioita, joissa on jokin yksilöllinen avainkenttä, esimerkiksi kokonaisluku väliltä 1,...,N. Tällöin yksi tapa tallentaa alkoita on varata N- paikkainen taulukko T[1,..,N] ja tallentaa tietoalkio (tai osoitin ko. alkioon) paikkaan T[k], mikäli tietoalkion avainkenttä on k. Tällaista menetelmää sanotaan suoraksi osoittamiseksi (directaddressing). Aluksi taulukko alustetaan tyhjillä arvoilla (NIL). Tällöin hakeminen avainkentän perusteella samoin kuin uusien alkioiden poistaminen sekä entisten lisääminen on tehokasta (operaatiot ovat vakioaikaisia). Haettaessa alkiota tarkistetaan, onko taulukkoon avainkenttään tallennettu NIL vai jokin arvo. Poistettaessa ainoastaan kirjoitetaan arvo NIL taulukkoon

sopivaan kohtaan. Mutta mikäli mahdollisia avaimia on paljon, tuhlataan tällä tavalla suuri määrä muistia Jos avaimet voivat olla mitä tahansa kokonaislukuja, ei tarpeeksi suurta taulukkoa voida edes varata. Edellämanittu ongelma voidaan kiertää tiivistefunktioita (hashfunktioita, hash functions) ja hash-taulukoita (hash tables) käyttämällä. Tällöin käytetään taulukkoa T[1,..,m], missä m on paljon pienempi kuin mahdollisten tietoalkioiden lukumäärä, lisäksi määritellään funktio h, joka kuvaa avaimet joukkoon {1,...,m}. Nyt alkio, jonka avainkenttä on k tallennetaan taulukon paikkaan T[h(k)]. Koska avaimia on enemmän kuin funktion arvoja, jotkin avaimet kuvautuvat väistämättä samalle arvolle; tätä sanotaan törmäykseksi (collision). Tiivistefunktiot pyritään suunnittelemaan niin, että törmäykset ovat mahdollisimman epätodennäköisiä, mutta törmäysongelmaa ei voi kuitenkaan kokonaan välttää. Törmäyksistä voidaan selvitä ketjutuksella, eli tallentamalla taulukkoon osoitin lineaariseen (linkitettyyn) listaan ja lisätä uudet tietoalkiot aina listan häntään. T Kaikki avaimet Käytetyt avaimet k 8 / / k 4 k 8 / k 1 k 2 k 7 / k 1 k 2 k 3 k 4 k 5 k 7 k 6 / / / / k 3 k 5 / k 6 / Kuva 4.4. Törmäysongelman välttäminen ketjutuksella. Yllä h(k1) = h(k2) = h(k7), h(k4) = h(k8) ja h(k3) = h(k5). Tällöin alkioiden käsittelyssä käytetään listojen käsittelyoperaatioita, jotka ovat tuttuja tietorakenteiden kurssista. Nyt operaatiot eivät enää ole vakioaikaisia, koska vakioajassa löydetään ainoastaan sen listan pää, jossa tietoalkio on tai johon se pitää lisätä tai josta se pitää poistaa. Listan käsittelyoperaatiot ovat tunnetusti lineaarisia listan pituuden suhteen. Oletetaan, että tiivistefunktion arvot jakaantuvat tasaisesti, ts. jos avaimia on N kappaletta ja funktion arvoja m, niin jokaiselle arvolle kuvautuu yhtä monta avainta, eli N/m kappaletta. Jos taulukkoon tallennetaan n alkiota, yhdelle arvolle kuvautuu keskimäärin n/m alkiota, eli yhden listan keskimääräinen pituus on n/m. Suhdetta n/m sanotaan taulukon täyttöasteeksi (load factor).

Näin ollen hash-taulukon käsittelyoperaatioiden (haku, lisäys, poisto) keskimääräinen aikakompleksisuus on luokkaa O( 1 n / m), koska tiivistefunktion arvon laskeminen on vakioaikainen operaatio, samoin kuin listan pään hakeminen. Listassa on keskimäärin n/m alkiota, joten listaoperaatio tehdään keskimäärin ajassa joka on luokkaa O ( n / m). Näin saadaan keskimääräinen aikakompleksisuus. Huomaa, että jos tiivistefunktio on huono ja kuvaa kaikki avaimet samalle arvolle, tilanne on sama kuin jos käytettäisiin yhtä linkitettyä listaa tallentamaan kaikki tietoalkiot. Tällöin saataisiin operaatioiden suoritusajan luokaksi O (n). Pohditaan seuraavaksi minkälainen on hyvä tiivistefunktio. Tämä riippuu voimakkaasti käyttötarkoituksesta. Mikäli tarkoituksena on tehostaa tietoalkioiden tallennusta ja hakua, eikä avainkentän arvojen jakaumaa tunneta, on järkevää valita funktio, joka kuvaa avaimet keskimäärin tasaisesti arvojoukolle. Tietoturvasovelluksissa tällainen järjestely ei yleensä ole tarkoituksenmukainen, vaan on ensisijaisen tärkeää minimoida törmäykset ja estää avainkentän arvaaminen, kun tiiviste on tiedossa. Tällainen tapaus voisi olla esimerkiksi pääsynvalvonta: jotta käyttäjien salasanoja ei tarvitsisi tallentaa, tallennetaan ainoastaan salasanaa vastaava tiiviste ja verrataan tätä salasanasta laskettuun tiivisteeseen. Tällöin tietenkin pitää tiivistettä vastaavan salasanan olla erittäin vaikeasti keksittävissä, mikäli tiiviste joutuu hyökkääjän ulottuville. Tarkastellaan nyt kuitenkin tietoalkioiden tehokasta käsittelyä. Voimme olettaa, että avaimet ovat luonnollisia lukuja, ts. lukuja 0,1,2,3,.... Näin ei aseteta rajoitusta, sillä avaimet voidaan aina yksikäsitteisesti koodata luonnollisiksi luvuiksi. Yksinkertainen tiivistefunktio saadaan jakomenetelmällä: Valitaan jokin positiivinen kokonaisluku m ja määritellään tiivistefunktio h( k) k(mod m), ts. funktion arvo on kentän arvon jakojäännös jaettaessa se luvulla m. On vain valittava luku m sopivasti. Ohjelmoinnissa voisi olla houkuttelevaa valita jokin luvun 2 potenssi 2 r, mutta se ei ole yleensä hyvä valinta, koska tällöin tiivistefunktion arvo riippuu vain luvun k alimmista r:stä bitistä. Useimmiten halutaan kuitenkin tiivistefunktion riippuvan luvun kaikista biteistä, ellei olla varmoja, että alimmat r bitin kaikki kuviot ovat yhtä todennäköisiä. Tämän vuoksi usein valitaan luvuksi m jokin alkuluku, joka ei ole kovin lähellä mitään luvun 2 potenssia. Oletetaan, että tallennettavia avaimia on 3000 kappaletta ja voidaan sallia keskimäärin viiden avaimen tallentua samaan listaan: tällöin valitaan jokin alkuluku läheltä lukua 3000/5 = 600. Nyt 601 olisi sopiva luvun m arvo ja tiivistefunktioksi saataisiin h( k) k(mod 601). Tiivistefunktio voidaan konstruoida myös tulomenetelmällä. Käytetään seuraavaa merkintää: kun x on jokin reaaliluku, x(mod 1) tarkoittaa luvun x desimaaliosaa, esimerkiksi 2.234(mod 1) 0.234 ja 5.9993(mod 1) 0. 9993. Tällöin valitaan taas positiivinen kokonaisluku m ja jokin reaalilukuvakio 0 < A < 1 sekä määritellään tiivistefunktio h( k) m ( ka(mod1)).

Tämän menetelmän etu on, ettei luvun m arvo vaikuta kriittisesti tiivistystulokseen. Usein p valitaan luku m joksikin luvun 2 potenssiksi; olkoon m 2, missä p on jokin positiivinen kokonaisluku. Tällöin tiivistefunktio voidaan implementoida helposti seuraavalla tavalla, kun tietokoneen sananpituus on w: Valitaan luku s väliltä (0,2 w w ) ja A s / 2. Silloin w k s ka 2 r r, w 1 2 0 missä sekä r1 että r0 ovat väliltä (0,2 w ) ja mahtuvat siten yhteen sanaan. Silloin ka 2 w r1 r0 /, joten ka(mod1) r0 / 2 w. Siten saadaan p w m ( ka(mod1)) 2 r / h k) 2. ( 0 Tiivistefunktion arvo muodostuu täten luvun r0 p:stä eniten merkitsevästä bitistä, kun arvo laajennetaan w-bittiseksi lisäämällä sen eteen riittävä määrä nollia, ks. kuva 4.2. w bittiä k s=a 2 w k s: r 1 r 0 p bittiä = h(k) Kuva 4.5. Tiivistefunktion laskeminen tulomenetelmällä, kun tietokoneen sananpituus on w bittiä. Ellei haluta käyttää ketjutusta, voidaan tallentaa avaimet suoraan ja hallita törmäykset niin, että törmäystilanteessa valitaan uusi, vapaa paikka taulukosta. Tällaisia ratkaisuja kutsutaan avoimen osoittamisen menetelmiksi (open addressing). Yksi tällainen menetelmä on antaa tiivistefunktion riippua avaimen lisäksi toisestakin parametrista, jonka arvot voivat olla 0,...,m-1, kun taulukon koko on m. Tällöin avain k yritetään tallentaa ensiksi paikkaan h(k,0) ja jos tämä on

varattu, paikkaan h(k,1), h(k,2) jne. kunnes löytyy vapaa kohta. Menetelmää kutsutaan luotaamiseksi (probing). Luotaamisen hyöty verrattuna tavalliseen taulukkoon tallentamiseen on siinä, että tallentamista ei aloiteta aina taulukon alusta, vaan avaimesta riippuvasta arvosta; lisäksi luotausjono riippuu myös avaimesta k. Näin törmäysten mahdollisuus vähenee. Usein tiivistefunktiolta vaaditaan lisäksi, että luotausjonossa h(k,0),..., h(k,m-1) esiintyvät kaikki taulukon paikat jossain järjestyksessä kaikilla avaimilla k, joten avain saadaan aina lopulta lisättyä taulukkoon ellei taulukko ole täynnä. Kuvatussa järjestelmässä avain haetaan samalla lailla kuin lisätäänkin: luodataan avaimen määräämää jonoa, kunnes avain löytyy tai kohdataan tyhjä arvo (NIL), josta tiedetään, että avain ei esiinny taulukossa. Seuraavat algoritmit toteuttavat nämä operaatiot, oletuksena tiivistefunktio h, jonka luotausjonossa h(k,0),..., h(k,m-1) esiintyvät kaikki taulukon paikat jossain järjestyksessä kaikilla avaimilla k. Syöte: Taulukko T[1,..,m], m >= 1, lisättävä avain k Tulostus: Taulukon indeksi, johon avain lisätään tai arvo OVERFLOW jos taulukko oli täynnä. HASH_TALLENNA(T,k) 1. i = 0 2. while i<m 3. j = h(k,i) 4. if T[j]==NIL 5. T[j]=k 6. return j 7. i = i+1 8. return OVERFLOW Syöte: Taulukko T[1,..,m], m >= 1, etsittävä avain k Tulostus: Taulukon indeksi, josta avain löydetään tai arvo NIL jos avain ei ole taulukossa. HASH_ETSI(T,k) 1. i = 0 2. j = h(k,i) 3. while (i<m && T[j]!=NIL) 4. if T[j]==k 5. return j 6. i = i+1 7. if i<m 8. j = h(k,i) 9. return NIL Avaimen poisto on hieman monimutkaisempi operaatio, koska poistaminen (ts. arvon NIL tallentaminen taulukkoon) keskeltä luotausjonoa poistaisi myös jonon loppuosan. Ongelma voidaan poistaa esimerkiksi käyttämällä toista erikoisarvoa DEL, joka ei voi esiintyä avaimena. Poistettaessa olemassaoleva alkio, merkitään se arvolla DEL. Näin ollen hakualgoritmi muuttuu niin, että DEL-arvot ohitetaan ja haetaan, kunnes avain löytyy tai kohdataan arvo NIL. Lisäys tehdään puolestaan ensimmäiseen DEL- tai NIL-kohtaan, joka tulee luotausjonossa vastaan. Haittapuolena hakuajat kasvavat, koska myös poistetut kohdat on tarkistettava; siten hakuaika ei enää ole suorassa suhteessa taulukon täyttöasteeseen. Mikäli poistoja tehdään yleisesti, on

järkevämpää toteuttaa hash-taulu ketjuttamalla. Luotauksen ongelma on myös rajoitettu tallennettavien avainten määrä. Toisaalta luotauksen etuja ovat helpompi osoittaminen ja vähäisempi muistin käyttö. Esitetään seuraavaksi lyhyesti kolme luotausmenetelmää. Lineaarisessa luotauksessa (linear probing) oletetaan tunnetuksi jokin tiivistefunktio h, joka kuvaa avaimet joukolle {0,..., m-1}. Funnktio h voi olla esimerkiksi jako- tai tulomenetelmällä konstruoitu funktio. Tällöin voidaan määritellä uusi tiivistefunktio h, joka riippuu kahdesta parametrista: h( k, i) ( h'( k) i)(mod m). Parametri i voi saada arvot 0,..., m-1. On helppo havaita, että jokaisella avaimella luotausjono h(k,0),..., h(k,m-1) sisältää kaikki arvot 0,...,m-1. Luotausjono alkaa nimittäin aina arvosta h (k) ja etenee askel kerrallaan kohti listan päätä, pyörähtää listan päässä alkuun ja jatkuu aina arvoon h (k)-1 saakka (ellei h (k) ole nolla, jolloin taulukko käydään läpi alusta loppuun). Lineaarinen luotaus on helppo implementoida, mutta se kärsii kasautumisongelmasta: Tiivistefunktion arvo sattuu todennäköisemmin pitkälle varatulle osuudelle kuin lyhyelle ja pitentää pitkää osuutta entisestään. Näin haut hidastuvat. Neliöllinen luotaus (quadratic probing) käyttää myös tunnettua tiivistefunktiota h avuksi. Nyt tiivistefunktio määritellään kuitenkin kaavalla: 2 h( k, i) ( h'( k) c i c i )(mod ), 1 2 m missä c1 ja c2 ovat nollasta poikkeavia vakioita. Nyt paikkoja ei testata peräkkäin, vaan siirryttävien askeleiden määrä riippuu siitä, kuinka monta paikkaa on jo testattu. Näin ollen kasautumisongelma poistuu. Jotta menetelmä toimisi mahdollisimman hyvin, on kuitenkin vakiot c1 ja c2 valittava aina jokaiselle taulukon koolle m sopivasti. Kaksoishashausta (double hashing) pidetään yhtenä parhaista avoimen osoittamisen muodoista. Nyt tarvitaan kaksi aputiivistefunktiota h1 ja h2. Tällöin tiivistefunktio h määritellään seuraavasti: h( k, i) ( h1 ( k) i h2 ( k))(mod m) Luotaus aloitetaan nyt avaimesta h1(k), ja askeleen pituus on h2(k), joten askeleen pituus riippuu avaimesta k. Erilaisia luotausjonoja saadaan näin yksi kutakin paria (h1(k), h2(k)) kohti, eli niitä on aputiivistefunktioista riippuen noin m 2 kappaletta. Ei ole kuitenkaan varmaa, että luotausjono kävisi kullakin avaimella koko taulukon läpi. Mikäli m on alkuluku, näin käy aina olettaen, että h2 saa arvot 0,..., m-1, mutta muuten funktio h2 on valittava sopivasti. Asia voidaan varmistaa myös esimerkiksi valitsemalla luku m luvun 2 potenssiksi ja h2 niin, että sen arvo on aina pariton. Tässä ei analysoida kaksoishashausta tarkemmin, todettakoon kuitenkin, että voidaan osoittaa kaksoishashauksen olevan suorituskyvyltään hyvin lähellä ideaalitapausta. Mainittakoon vielä lopuksi tiivistefunktioiden muita sovelluksia. Tiivistefunktioita käytetään yleisesti tarkistussummissa: välitettävään viestiin k liitetään tiivistefunktion arvo h(k) ja lähetetään pari (k,h(k)). Vastaanottaja voi tarkistaa tiedon eheyden seuraavasti: hän saa parin

(k,h ) ja laskee arvon h(k ). Mikäli h on erisuuri kuin h(k ), tiedetään, että viesti on muuttunut matkalla. Mikäli h =h(k ), on todennäköistä, että saatiin alkuperäinen viesti muuttumattomana. Tällaisessa käytössä tiivistefunktion arvojen törmäämisen on oltava hyvin epätodennäköistä. Tiivistefunktion avulla voidaan varmistaa esimerkiksi Internetissä jaeltavien tiedostojen eheys: julkaistaan tiedoston tarkistussumma, jolloin peilisivustolta ladattavan tiedoston sisältöön voidaan luottaa vertaamalla sille laskettua tarkistussummaa alkuperäiseen. Jos halutaan estää viestin tarkoituksellinen muuttaminen, tiivisteen täytyy lisäksi riippua avaimesta, joka on ainoastaan lähettäjän ja vastaanottajan tiedossa. Tällöin puhutaan myös MACfunktiosta (Message Authentication Code). Kummassakin sovelluksessa edellytetään, että tiivistefunktio on erittäin vaikea väärentää, ts. löytää toinen viesti, jolla on sama tiiviste kuin alkuperäisellä viestillä. Itse asiassa salaussovelluksissa käytettävän hyvän kryptografisen tiivistefunktion suunnitteleminen on hyvin vaikea tehtävä ja ainoastaan verrattain harvoja käyttökelpoisia funktioita tunnetaan. Asiasta kiinnostunut lukija voi perehtyä aiheeseen kirjan [Sta] luvusta 12. 4.4. Binääriset etsintäpuut Binääripuut esiteltiin aiemmin. Muistettakoon, että binääripuu on tyhjä tai se on puu, jonka jokaisella solmulla on korkeintaan kaksi lapsisolmua. Puun korkeudeksi sanotaan sen tasojen lukumäärää. Puun solmuun voidaan liittää jokin avainkenttä. Kun x on puun solmu, käytetään seuraavia merkintöjä: x.key solmun avainkentän arvo x.left osoitin solmun vasempaan alipuuhun x.right osoitin solmun oikeaan alipuuhun. x.p osoitin solmun vanhempaan Kun T on puu, sen juuren osoitin on T.root. Binäärisessä etsintäpuussa (Binary Search Tree, BST) eli binäärisessä hakupuussa avainkenttien arvoilla on järjestys. Puun kunkin solmun avainkenttä on suurempi kuin mikään sen vasemman alipuun avainkenttä ja pienempi kuin mikään sen oikean alipuun avainkenttä. Näin puun läpikäyminen sisäjärjestyksessä tuottaa avainkenttien järjestyksen pienimmästä suurimpaan. Seuraavassa on esitetty mainittu puun läpikäymistapa Syöte: Binäärisen etsintäpuun solmu x Tulostus: Tulostaa solmusta x lähtevän alipuun avaimet suuruusjärjestyksessä BST_SISÄJÄRJESTYS(x) 1. if x!= NIL 2. BST_SISÄJÄRJESTYS(x.left) 3. tulosta x.key 4. BST_SISÄJÄRJESTYS(x.right) Kun halutaan tulostaa kaikki puun T avaimet, rutiinia kutsutaan antamalla puun juuri syötteeksi, ts. BST_SISÄJÄRJESTYS(T.root). Seuraavassa oletetaan, että puussa ei sama avain esiinny useaan kertaan; avaimien useampikertainen tallentaminen on mahdollista toteuttaa pienillä muutoksilla.

20 10 28 8 16 27 14 18 Kuva 4.6. Binäärinen etsintäpuu Kun binäärinen etsintäpuu on muodostettu, siitä on varsin helppo etsiä tietoa. Seuraava algoritmi etsii avaimen puusta tai havaitsee, että avain ei esiinny puussa. Algoritmia kutsutaan seuraavasti: BST_ETSI(T.root,k), kun k on puusta T etsittävä avain. Syöte: Binäärisen etsintäpuun juuri x, etsittävä avain k Tulostus: Palauttaa osoittimen solmuun joka sisältää avaimen tai arvon NIL jos avain ei ole puussa. BST_ETSI(x,k) 1. y = x 2. while y!=nil && y.key!=k 3. if y.key < k // k oikeassa alipuussa 4. y = y.right 5. else // k vasemmassa alipuussa 6. y = y.left 7. return y Koska jokaisella silmukan kierroksella mennään puussa syvemmälle, kierroksia kertyy korkeintaan puun korkeuden verran. Näin ollen algoritmin kompleksisuus on luokkaa (h), missä h on puun korkeus. Myös puun avainkenttien minimi- ja maksimiarvot on helppo löytää, koska minimiarvo löytyy edettäessä jatkuvasti vasenta haaraa pitkin ja maksimiarvo oikeaa haaraa pitkin.

Syöte: Binäärisen etsintäpuun juuri x Tulostus: Palauttaa osoittimen solmuun joka sisältää puun pienimmän avaimen tai arvon NIL, jos puu on tyhjä. BST_MINIMI(x) 1. if x==nil 2. return NIL 3. y = x 4. while y.lefty!=nil 5. y = y.left 6. return y Syöte: Binäärisen etsintäpuun juuri x Tulostus: Palauttaa osoittimen solmuun joka sisältää puun suurimman avaimen tai arvon NIL, jos puu on tyhjä. BST_MAKSIMI(x) 1. if x==nil 2. return NIL 3. y = x 4. while y.right!=nil 5. y = y.right 6. return y Näidenkin algoritmien kompleksisuus on luokkaa (h), kun h on puun korkeus. Vielä tarvitaan algoritmit puun muodostamiseen, ts. avaimen lisäämiseen puuhun ja poistamiseen puusta. Avain voidaan lisätä varsin suoraviivaisesti: Etsitään puusta sopiva lehti, jonka lapseksi lisätään avaimen sisältävä solmu. Seuraava algoritmi lisää vain uusia avaimia; mikäli avain esiintyy puussa, ei tehdä mitään. Syöte: Binäärinen etsintäpuu T ja lisättävä avain k Tulostus: Lisää avaimen sisältävän solmun puun lehtisolmuksi. BST_LISÄÄ(T,k) 1. Muodosta uusi solmu t 2. t.key = k 3. t.left = NIL 4. t.right = NIL 5. x = T.root 6. while x!= NIL // Etsitään sopiva lehti 7. y=x 8. if k < x.key 9. x=x.left 10. else if k > x.key 11. x=x.right 12. else // Avain oli jo puussa, ei tehdä mitään 13. return 14. t.p = y // Avain menee y:n lapseksi 15. if y==nil // Puu tyhjä, uusi solmu juureksi 16. T.root = t 17. else if k < y.key 18. y.left = t 19. else 20. y.right = t 21. return

Tässäkin algoritmissa esiintyvän silmukan jokaisella kierroksella mennään puussa syvemmälle, joten kierroksia kertyy korkeintaan puun korkeuden verran. Silmukan lisäksi tehdään vakiomäärä vakioaikaisia operaatioita. Näin ollen algoritmin kompleksisuus on jälleen luokkaa (h), missä h on puun korkeus. Avaimen poistaminen puusta on mutkikkain toimenpide, koska poistettava avain voi olla puun keskellä. Poistossa voidaan kohdata kolme olennaisesti erilaista tapausta. Ensimmäiseksi, jos avain on puun lehdessä, se voidaan yksinkertaisesti poistaa seuraavan esimerkin mukaisesti. 20 20 10 25 10 25 8 16 28 8 16 28 14 18 27 30 14 27 30 12 12 13 13 Kuva 4.7. Binäärisestä etsintäpuusta poistetaan avain 18, joka on puun lehdessä. Toinen tapaus syntyy, kun puun keskeltä poistetaan solmu, jolla on ainoastaan yksi alipuu. Tällöin solmu voidaan yksinkertaisesti ohittaa siirtämällä mainittu alipuu poistettavan solmun vanhemman alipuuksi, kuten seuraava kuva osoittaa.

20 20 10 25 10 28 8 16 28 8 16 27 30 14 18 27 30 14 18 12 12 13 13 Kuva 4.8. Binäärisestä etsintäpuusta poistetaan avain 25, jolla on vain yksi alipuu. Kolmas tapaus on mutkikkain: tällöin on poistettava solmu, jolla on kaksi alipuuta. Tällaista solmua ei voida poistaa suoraan, joten ensin solmun oikeasta alipuusta haetaan pienin avain k. Tämän on oltava solmussa, jolla ei ole vasenta alipuuta (koska vasen alipuu sisältää ainoastaan juuriavainta pienempiä avaimia). Tämä solmu voidaan poistaa yllä esitetyllä menetelmällä. Solmun avain voidaan kopioida poistettavan solmun uudeksi avainkentäksi, koska oikeassa alipuussa esiintyy ainoastaan suurempia avaimia. Näin on avain saatu poistettua puusta: seuraava kuva havainnollistaa tapausta.

20 10 25 12 20 10 25 8 16 28 8 16 28 14 18 27 30 14 18 27 30 12 13 13 20 12 25 8 16 28 14 18 27 30 13 Kuva 4.9. Binäärisestä etsintäpuusta poistettava avain (10) on solmussa, jolla on kaksi alipuuta. Solmun oikean alipuun pienin avain on 12, joka poistetaan alipuusta ja kopioidaan avaimen 10 tilalle. Poistettaessa puusta solmuja, voidaan joutua siirtelemään alipuita, joten konstruoidaan ensin apualgoritmi tätä varten. Syöte: Binäärinen etsintäpuu T ja sen solmut u ja v (v voi olla NIL) Tulostus: Binäärinen etsintäpuu, jossa solmusta u lähtevän alipuun tilalle on siirretty solmusta v lähtevä alipuu BST_TRANSPLANT(T,u,v) 1. if u.p == NIL // u oli juuri 2. T.root = v 3. else if u == u.p.left // u vanhempansa vas. alipuussa 4. u.p.left = v 5. else 6. u.p.right = v // u vanhempansa oik. alipuussa 7. if v!= NIL 8. v.p = u.p Algoritmi on luonnollisesti vakioaikainen. Nyt voidaan luoda algoritmi käsittelemään solmujen poistotapaukset:

Syöte: Binäärinen etsintäpuu T ja poistettava avain k Tulostus: Poistaa avaimen puusta, jos se esiintyy. Palauttaa arvon TRUE, jos avain oli puussa ja arvon FALSE muuten. BST_POISTA(T,k) 1. x = BST_ETSI(T,k) 2. if x == NIL 3. return FALSE // Avain ei ollut puussa 4. if x.left == NIL // Ei vasenta alipuuta 5. BST_TRANSPLANT(T,x,x.right) 6. else if x.right == NIL // Ei oikeata alipuuta 7. BST_TRANSPLANT(T,x,x.left) 8. else // Kaksi alipuuta 9. y = BST_MINIMI(x.right) 10. if y.p!= x 11. BST_TRANSPLANT(T,y,y.right) 12. y.right = x.right 13. y.right.p = y 14. BST_TRANSPLANT(T,x,y) 15. y.left = x.left 16. y.left.p = y 17. return TRUE Yllä oleva algoritmi tekee vakioaikaisia operaatioita lukuun ottamatta algoritmien BST_ETSI ja BST_MINIMI kutsuja. Mainitut algoritmit ovat kumpikin huonoimmassa tapauksessa luokkaa (h), kuten aiemmin on todettu. Näin ollen avaimen poistamisen kompleksisuus on (h). 4.5. Puna-mustat puut Kaikkien edellä esitettyjen binääristen etsintäpuiden algoritmien aikakompleksisuus on luokkaa (h), kun h on käsiteltävän puun korkeus. Tällaisessa puussa voi olla 2 h 1solmua, joten puun ollessa tasapainossa (mikään alipuu ei ole muita alipuita selvästi korkeampi), algoritmien kompleksisuus on luokkaa (lg(n )), kun syötteen koon mittana käytetään avaimien lukumäärää n. Valitettavasti lisäys- ja poistoalgoritmit eivät välttämättä pidä puuta tasapainossa, vaan tuloksena syntynyt puu riippuu avaimien järjestyksestä. Esimerkiksi lisättäessä avaimet suuruusjärjestyksessä puuhun muodostuu vain yksi haara. Operaatioiden lukumäärä voi siis huonoimmassa tapauksessa riippua lineaarisesti avaimien lukumäärästä. Tämän vuoksi on kehitetty useita etsintäpuumalleja, joissa taataan puun tasapainon säilyminen ja siten logaritminen käsittelyaika kaikissa tapauksissa. Ensimmäinen tällainen etsintäpuun malli on vuonna 1962 esitelty AVL-puu, joka on saanut nimensä keksijöistään (Adelson-Velskii ja Landis). Näihin tietorakenteisiin voi tutustua mm. teoksen [Wei] luvusta 4.4. Tässä käsitellään tarkemmin puna-mustia puita (red-black trees), jotka ovat yksi tapa tasapainottaa binäärisiä etsintäpuita. Kirjan [Cor] luvusta 13 voi myös perehtyä aiheeseen.

Puna-mustat puut ovat binäärisiä hakupuita, joiden solmuihin on lisätty väri: solmut ovat joko punaisia tai mustia. Binäärinen hakupuu on puna-musta puu, jos se toteuttaa seuraavat viisi ehtoa: 1. Jokainen solmu on joko punainen tai musta. 2. Juuri on musta. 3. Jokainen lehti (NIL) on musta. 4. Jos solmu on punainen, sen molemmat lapset ovat mustia. 5. Kaikille solmuille on voimassa: Jokainen polku solmusta sen jälkeläislehtiin sisältää saman lukumäärän mustia solmuja. HUOM! Puun lehdet ovat siis aina tyhjiä (NIL) ja ne merkitään aina mustiksi. Niitä ei välttämättä merkitä kuvioissa näkyviin. Kaikilla sisäsolmuilla on siis aina kaksi lasta, joista toinen tai molemmat voivat olla lehtiä. Juuren vanhemmaksi merkitään NIL, joka on siis musta. 20 20 10 25 10 25 8 16 28 8 16 28 14 18 27 30 14 27 30 Kuva 4.10. Kaksi binääristä etsintäpuuta, jotka eivät ole puna-mustia puita. Kuvasta on jätetty merkitsemättä mustat tyhjät lehtisolmut. Samoin juuren vanhempi. Edellä olevat kaksi puuta eivät kumpikaan ole hyvin muodostettuja puna-mustia puita. Vasemmanpuoleisessa puussa punaisella solmulla 10 on punainen lapsi 16, joten ominaisuus 4 ei ole voimassa. Oikeanpuoleisessa puussa taas solmun 28 vasemmanpuoleisessa polussa lehteen on yksi musta solmu enemmän kuin oikeanpuoleisessa solmussa, joten ominaisuus 5 ei toteudu. Seuraava puu on sen sijaan oikea puna-musta puu, kuten on helppo todeta. 2 0 1 2 8 1 6 2 4 2 8 1 1 2 3 4 8 7 0 Kuva 4.11. Esimerkki puna-mustasta puusta. Osoitetaan seuraavaksi, että puna-mustat puut ovat riittävän tasapainoisia toimiakseen hyvinä hakupuina. Käytetään seuraavaa merkintää: Kun x on puna-mustan puun solmu, sen

mustakorkeus bh(x) on sen mustien jälkeläisten lukumäärä polussa solmusta x puun lehteen saakka. Huomaa, että ominaisuuden 5 perusteella on merkityksetöntä, mihin solmun x jälkeläislehteen menevää polkua tarkastellaan. Perustellaan seuraava ominaisuus: Olkoon x punamustan puun solmu. Tällöin alipuu, jonka juuri x on, sisältää vähintään 2 1solmua. Tämä bh( x) voidaan näyttää induktiivisesti solmun x korkeuden suhteen. Jos solmun x korkeus on 0, niin x on 0 bh( x) lehti, bh(x)=0 ja sen alipuussa on vähintään 0 2 1 2 1solmua, joten väite on voimassa. Olkoon sitten solmun x korkeus h > 0, jolloin solmu on sisäsolmu ja solmulla kaksi lasta. Oletetaan, että väite on voimassa solmuille, joiden korkeus on pienempi. Nyt solmun x lasten bh( x) 1 mustakorkeus on vähintään bh(x)-1 ja niiden alipuissa yhteensä vähintään 2*(2 1) solmua. bh( x) 1 bh( x) Näin ollen solmun x alipuussa on vähintään 2*(2 1) 1 2 1solmua. Tarkasteltava ominaisuus on siis aina voimassa. Todetaan että edellä havaitusta ominaisuudesta seuraa puun korkeuden logaritminen suhde solmujen lukumäärään: Jos puna-mustassa puussa on n solmua, niin puun korkeus on korkeintaan 2*lg( n 1). Olkoon nimittäin puun korkeus h. Puna-mustan puun ominaisuuksista 3 ja 4 seuraa, että juuren mustakorkeus on vähintään h/2, koska polulla juuresta lehteen vähintään joka toinen solmu on musta ja lehdet ovat aina mustia. Näin ollen puun solmujen lukumäärä n toteuttaa ehdon n 2 h / 2 1, mistä saadaan h / 2 lg( n 1) ja h 2*lg( n 1). Puna-mustiin puihin voidaan luonnollisesti soveltaa binäärisen hakupuun algoritmeja avaimen etsimiseen sekä minimi- ja maksimiarvojen hakemiseen (edellisen osan algoritmit BST_ETSI, BST_MINIMI ja BST_MAKSIMI). Koska kaikki nämä algoritmit ovat kompleksisuudeltaan luokkaa (h), missä h on puun korkeus, niin edellä olevasta tarkastelusta seuraa heti, että punamustalle puulle algoritmien kompleksisuus on (lg n), missä n on puussa olevien avainten lukumäärä. Sen sijaan edellisessä osassa esitettyjä algoritmeja avainten lisäämiseen poistamiseen ei voi sellaisenaan soveltaa, sillä ne eivät välttämättä säilytä puuta puna-mustana. Puna-mustien puiden kunnossapidon perusoperaatio on rotaatio eli pyöritys. Pyöritys voidaan tehdä solmun suhteen joko vasemmalle tai oikealle. Vasen rotaatio voidaan tehdä mille tahansa solmulle, jonka oikea lapsi ei ole lehti. Samoin oikea rotaatio voidaan tehdä mille tahansa solmulle, jonka vasen lapsi ei ole lehti. Seuraava kuva havainnollistaa tilannetta

x RBT_VASEN_ROTAATIO(T,x) y a y RBT_OIKEA_ROTAATIO(T,y) x c b c a b Kuva 4.12. Vasen ja oikea rotaatio. Edellä olevassa kuvassa solmut x ja y voivat olla punaisia tai mustia; solmun väri ei muutu rotaatiossa. Symbolit a, b ja c tarkoittavat solmujen alipuita. Huomaa, miten alipuu b vaihtaa vanhempaansa rotaatiossa. Vasemman rotaation pseudokoodi on esitetty seuraavassa: Syöte: Binäärinen etsintäpuu T ja solmu x, jonka suhteen kierretään. Oletus: x.right!= NIL Tulostus: Suorittaa puussa vasemman rotaation solmun x suhteen RBT_VASEN_ROTAATIO(T,x) 1. y = x.right 2. x.right = y.left // y:n vasen alipuu x:n oikeaksi alipuuksi 3. if y.left!= NIL 4. y.left.p = x 5. y.p = x.p // x:n vanhempi y:n vanhemmaksi 6. if x.p == NIL 7. T.root = y 8. else if x == x.p.left 9. x.p.left = y 10. else 11. x.p.right = y 12. y.left = x // x y:n vasemmaksi lapseksi 13. x.p = y 14. return

Oikea rotaatio on symmetrinen Syöte: Binäärinen etsintäpuu T ja solmu x, jonka suhteen kierretään. Oletus: x.left!= NIL Tulostus: Suorittaa puussa oikean rotaation solmun x suhteen RBT_OIKEA_ROTAATIO(T,x) 1. y = x.left 2. x.left = y.right // y:n oikea alipuu x:n vasemmaksi alipuuksi 3. if y.right!= NIL 4. y.right.p = x 5. y.p = x.p // x:n vanhempi y:n vanhemmaksi 6. if x.p == NIL 7. T.root = y 8. else if x == x.p.right 9. x.p.right = y 10. else 11. x.p.left = y 12. y.right = x // x y:n oikeaksi lapseksi 13. x.p = y 14. return Rotaatiot ovat vakioaikaisia, koska niissä tehdään vakiomäärä osoittimien sijoituksia. Perehdytään seuraavaksi siihen, miten puna-mustaan puuhun voidaan lisätä avain niin, että puu säilyy puna-mustana ja siten tasapainossa. Algoritmi lisää ensin avaimen kuten binääriseen etsintäpuuhun, mutta lisäämisen jälkeen kutsutaan vielä rutiinia, joka korjaa puun: Syöte: Puna-musta puu T ja solmu z, jonka avain on lisättävä avain. Tulostus: Lisää avaimen z.key puuhun niin, että T säilyy puna-mustana. RBT_LISÄÄ(T,z) 1. y = NIL 2. x = T.root 3. while x!= NIL 4. y = x 5. if z.key < x.key 6. x = x.left 7. else 8. x = x.right 9. z.p = y 10. if y == NIL 11. T.root = z 12. else if z.key < y.key 13. y.left = z 14. else 15. y.right = z 16. z.left = NIL 17. z.right = NIL 18. z.color = RED 19. RBT_LISÄYS_KORJAUS(T,z) Huomaa, että lisättävä solmu väritetään punaiseksi. Solmun lisääminen voi luonnollisesti rikkoa puun puna-mustaominaisuuden, joten tarvitaan korjausrutiini:

Syöte: Puu T ja solmu z, joka on lisätty T:hen. T on ollut puna-musta puu ennen z:n lisäystä. Tulostus: Korjaa T:n puna-mustaksi. RBT_LISÄYS_KORJAUS(T,z) 1. while z.p.color == RED 2. if z.p == z.p.p.left 3. L_KORJAA_VASEN(T,z) 4. else 5. L_KORJAA_OIKEA(T,z) 6. T.root.color = BLACK L_KORJAA_VASEN(T,z) 1. y = z.p.p.right 2. if y.color == RED 3. z.p.color = BLACK // tapaus 1 4. y.color = BLACK // tapaus 1 5. z.p.p.color = RED // tapaus 1 6. z = z.p.p // tapaus 1 7. else 8. if z == z.p.right 9. z = z.p // tapaus 2 10. RBT_VASEN_ROTAATIO(T,z) // tapaus 2 11. z.p.color = BLACK // tapaus 3 12. z.p.p.color = RED // tapaus 3 13. RBT_OIKEA_ROTAATIO(T, z.p.p) // tapaus 3 L_KORJAA_OIKEA(T,z) 1. y = z.p.p.left 2. if y.color == RED 3. z.p.color = BLACK // tapaus 1* 4. y.color = BLACK // tapaus 1* 5. z.p.p.color = RED // tapaus 1* 6. z = z.p.p // tapaus 1* 7. else 8. if z == z.p.left 9. z = z.p // tapaus 2* 10. RBT_OIKEA_ROTAATIO(T,z) // tapaus 2* 11. z.p.color = BLACK // tapaus 3* 12. z.p.p.color = RED // tapaus 3* 13. RBT_VASEN_ROTAATIO(T, z.p.p) // tapaus 3* Perustellaan seuraavaksi, miksi T korjaantuu edellä määritellyillä operaatioilla puna-mustaksi puuksi. Perustelu ei ole erityisen tarkka: yksityiskohtaisempaa käsittelyä kaipaava voi perehtyä kirjan [Cor] esitykseen. Tutkitaan ensin, mitkä puna-mustan puun ominaisuudet voivat rikkoutua solmua lisättäessä. Palautetaan mieleen, että lisättävä solmu on aina punainen. Kun solmu lisätään puuhun, ominaisuudet 1 ja 3 säilyvät, koska puuhun lisätään punainen solmu, jolla on kaksi mustaa (tyhjää) lehtisolmua. Lisättävä punainen solmu sijoitetaan mustan lehtisolmun tilalle, joten ominaisuus 5 pysyy myös voimassa. Jos punainen solmu lisätään juureksi, voi ominaisuus 2 rikkoutua. Mikäli lisättävä punainen solmu sattuu punaisen solmun lapsisolmuksi, voi myös ominaisuus 4 lakata pätemästä.

Perehdytään nyt algoritmiin. Todetaan aluksi, että jos solmu lisätään tyhjään puuhun (jolloin se menee juureen) ei korjauksessa tehdä muuta kuin juuren muuttaminen mustaksi. Tällöin puusta tulee hyvin muodostettu puna-musta puu. Oletetaan jatkossa, että solmua ei lisätä juureen. Tällöin juuri on musta, joten jos solmu lisätään juuren lapseksi, saadaan puna-musta puu ilman korjauksia. Näin ollen voidaan olettaa, että solmulla on alussa isovanhempi. Edelleen havaitaan, että while-silmukan jokaisen kierroksen alussa solmu z on punainen ja silmukkaa jatketaan vain jos solmun z vanhempi on punainen. Siten vanhempi ei ole juuri ja solmulla z on puussa isovanhempi. Lisäksi solmu z nousee puussa kohti juurta jokaisella kierroksella, joten algoritmi päättyy. Mikäli solmu nousee juureen asti, puun juuri on silmukan päättyessä punainen. Tämä korjataan algoritmin RBT_LISÄYS_KORJAUS rivillä 6, joten algoritmin päättyessä joka tapauksessa puun juuri on musta. Näin ollen ainoa puna-mustan puun ominaisuus, joka ei ehkä ole algoritmin päättyessä voimassa on ominaisuus 4. Kun solmu lisätään, ainoa vika puussa voi olla, että lisätyn solmun z vanhempi on punainen. Tutkitaan nyt mitä algoritmissa tapahtuu tällöin. Koska algoritmit L_KORJAA_VASEN ja L_KORJAA_OIKEA ovat toistensa peilikuvat, riittää tarkastella toista. Oletetaan siis, että solmun z vanhempi on oman vanhempansa vasemmassa alipuussa ja mennään suorittamaan algoritmia L_KORJAA_VASEN. Tarkastelu jakaantuu nyt kolmeen tapaukseen: 1. Solmu y on punainen. Tällöin solmun z vanhempi ja solmu y väritetään mustiksi ja solmun z isovanhempi punaiseksi. Solmu z nousee kaksi askelta puussa. Seuraava kuva havainnollistaa tilannetta. Operaation jälkeen ainoa mahdollinen vika muodostuneessa puussa on uuden z:n vanhemman punaisuus. Mikäli näin on, jatketaan silmukkaa. 20 10 25 20 10 25 y 8 16 28 8 16 z 28 14 18 y 14 18 12 z 12

2. Solmu y on musta ja z on oikea lapsi. Tällöin tehdään seuraavan kuvan mukainen vasen rotaatio, 20 10 25 y 16 20 25 y 8 16 z 28 z 10 18 28 14 18 8 14 12 12 joka vie tilanteen tapaukseen 3. Solmu y on musta ja z on vasen lapsi. Tällöin väritetään solmun z vanhempi mustaksi ja isovanhempi punaiseksi. Lisäksi tehdään oikea rotaatio solmun z isovanhemman suhteen seuraavan kuvan mukaisesti. Operaation jälkeen solmun z vanhempi on musta ja silmukka päättyy. 16 20 25 y z 10 16 20 z 10 18 28 8 12 14 18 25 28 8 14 12 Edellisestä tarkastelusta huomataan, että while-silmukkaa suoritettaessa ominaisuus 4 voi rikkoutua vain yhdessä kohdassa, kun z ja sen vanhempi on punainen. Silmukan päättyessä solmun z vanhempi on musta, joten silmukan päättyessä ominaisuus 4 on puussa voimassa. Koska juuri väritetään lopulta mustaksi, saadaan algoritmin päättyessä puna-musta puu. Selvitetään vielä lisäysalgoritmin aikakompleksisuus. Lisäyksen operaatioiden lukumäärä ennen korjausta on, kuten binäärisillä etsintäpuillakin, luokkaa O(h), missä h on puun korkeus. Puussahan edetään syvemmälle, kunnes kohdataan lehti, johon solmu voidaan lisätä. Rotaatiot

ovat vakioaikaisia, koska niissä tehdään vakiomäärä osoittimien sijoituksia. Näin ollen korjauksen aikakompleksisuus riippuu siitä, kuinka monta kertaa while-silmukkaa suoritetaan. Korjattava solmu nousee puussa jokaisella silmukan kierroksella ylöspäin, joten myös korjauksen aikakompleksisuus luokkaa O(h). Näin ollen koko lisäysoperaation aikakompleksisuus on luokkaa O(h). Puun korkeuden osoitettiin aiemmin olevan suuruusluokkaa lg(n), kun n on puun solmujen lukumäärä. Puna-mustaan puuhun lisäyksen aikakompleksisuus on siten kaikkiaan luokkaa O(lg(n)), missä n on puun solmujen lukumäärä. Perehdytään vielä solmun poistamiseen puna-mustasta puusta. Aluksi tarvitaan apualgoritmi, joka siirtää puussa alipuun toisen alipuun paikalle: Syöte: Puna-musta puu T ja sen solmu u sekä solmu v. Tulostus: Poistaa puusta u:n ja siitä lähtevän alipuun ja korvaa tämän solmulla v ja siitä lähtevällä alipuulla. RBT_TRANSPLANT(T,u,v) 1. if u.p == NIL 2. T.root = v 3. else 4. if u == u.p.left 5. u.p.left = v 6. else 7. u.p.right = v 8. v.p = u.p Puna-mustasta puusta poistaminen muistuttaa binäärisestä etsintäpuusta poistamista, mutta nyt pitää luonnollisesti huolehtia myös puna-mustaominaisuuksien säilymisestä.

Syöte: Puna-musta puu T ja sen solmu z. Tulostus: Poistaa puusta solmun z niin, että jäljelle jää puna-musta puu. RBT_POISTA(T,z) 1. y = z 2. y_orig_color = y.color 3. if z.left == NIL 4. x = z.right 5. RBT_TRANSPLANT(T,z,z.right) 6. else 7. if z.right == NIL 8. x = z.left 9. RBT_TRANSPLANT(T,z,z.left) 10. else 11. y = BST_MINIMI(z.right) 12. y_orig_color = y.color 13. x = y.right 14. if y.p == z 15. x.p = y 16. else 17. RBT_TRANSPLANT(T,y, y.right) 18. y.right = z.right 19. y.right.p = y 20. RBT_TRANSPLANT(T,z,y) 21. y.left = z.left 22. y.left.p = y 23. y.color = z.color 24. if y_orig_color == BLACK 25. RBT_POISTO_KORJAUS(T,x) Algoritmi tuhoaa solmun kuten binäärisestä etsintäpuusta, mutta puun kunnossa pitämiseksi joudutaan lisäämään hieman ominaisuuksia. Tarkastellaan ensin muuttujan y arvoa algoritmin päättyessä. Mikäli poistettavalla solmulla z ei ole kumpaakin alipuuta, y on poistettu solmu z. Jos taas solmulla z on kumpikin alipuu, y on algoritmin suorituksen loputtua solmu, joka tulee poistettavan solmun z paikalle. Tällöin solmu x puolestaan siirretään solmun y alkuperäiselle paikalle. Oletetaan, että solmun y alkuperäinen väri on punainen. Tarkastellaan aluksi ensimmäistä tapausta: y on poistettu solmu z. Poistettu solmu ei punaisena voinut olla juuri; siten puun juuri on algoritmin päättyessä edelleen musta. Punaisen solmun poistaminen ei myöskään voi aiheuttaa punaisen solmun lapsen muuttumista mustaksi eikä se vaikuta mustien solmujen lukumäärään poluissa lehtiin. Näin ollen puu säilyy puna-mustana eikä korjauksia tarvita. Siirrytään tapaukseen, jossa y tulee solmun z tilalle. Koska y saa värikseen poistettavan solmun värin, poisto ei vaikuta mustien solmujen lukumääriin poluissa. Jos y oli solmun z oikea lapsi, y korvaa solmun z ja mikään puna-mustan puun ominaisuus ei rikkoudu. Jos y ei ollut solmun z oikea lapsi, solmun y oikea lapsi x korvaa solmun y. Koska y oli punainen, solmun x on oltava musta. Näin ollen operaatio ei aiheuta punaisten solmujen joutumista punaisten solmujen lapsiksi ja jälleen puu säilyy puna-mustana.

Solmun poistaminen voi vahingoittaa puna-mustaa puuta siis vain, jos solmun y alkuperäinen väri on musta. Tällöin saatetaan tarvita korjausoperaatio. Puu voi vahingoittua kolmella eri tavalla: 1. Solmu y oli alun perin juuri, joka poistettiin ja sen punaisesta lapsesta tuli uusi juuri. 2. Solmu x on punainen, samoin kuin solmun y vanhempi. Tällöin punaiselle solmulle tulee punainen lapsi. 3. Kun solmua y siirretään puussa, voi jokin polku, jolla y aiemmin oli, menettää yhden mustan solmun. Tällöin ei siis puna-mustan puun ominaisuus 5 säily voimassa. Kaikki nämä tapaukset voidaan käsitellä seuraavalla algoritmilla: Syöte: Puu T ja solmu x. Tulostus: Korjaa T:n puna-mustaksi. RBT_POISTO_KORJAUS(T,x) 1. while x!= T.root AND x.color == BLACK 2. if x == x.p.left 3. P_KORJAA _VASEN(T,x) 4. else 5. P_KORJAA_OIKEA(T,x) 6. x.color = BLACK P_KORJAA_VASEN(T,x) 1. w = x.p.right 2. if w.color == RED 3. w.color = BLACK // tapaus 1 4. x.p.color = RED // tapaus 1 5. RBT_VASEN_ROTAATIO(T, x.p) // tapaus 1 6. w = x.p.right // tapaus 1 7. if w.left.color == BLACK AND w.right.color == BLACK 8. w.color = RED // tapaus 2 9. x = x.p // tapaus 2 10. else 11. if w.right.color == BLACK 12. w.left.color = BLACK // tapaus 3 13. w:color = RED // tapaus 3 14. RBT_OIKEA_ROTAATIO(T,w) // tapaus 3 15. w = x.p.right // tapaus 3 16. w.color = x.p.color // tapaus 4 17. x.p.color = BLACK // tapaus 4 18. w.right.color = BLACK // tapaus 4 19. RBT_VASEN_ROTAATIO(T,x.p) // tapaus 4 20. x = T.root // tapaus 4