2. Perustietorakenteet Tässä osassa käsitellään erilaisia perustietorakenteita, 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 pino, jono sekä linkitetty lista. Käsiteltävät tietorakenteet liittyvät olennaisesti tiedon tallentamiseen sekä hakemiseen. Teoksessa [Cor] käsitellään tässä esitettäviä luvussa 10. 2.1. Tietorakenteen 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. 2.2. Pino, jono ja lista Seuraavaksi esitellään kolme dynaamista joukkoa: pino, jono ja linkitetty lista, jotka toimivat johdantona tietorakenteisiin. Jokainen näistä tietorakenteista 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 ennalta määrätty. Tämä johtuu pinon sisäisestä rakenteesta, joka muistuttaa fyysisten esineiden pinoamista. Pino toteuttaa niin kutsutun 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 2.1 on havainnollistettu PUSH ja POP operaatiot, sekä pinon toteutus käyttäen indeksoitua taulukkoa. Taulukon indeksi S.head (top) osoittaa aina viimeisimpänä lisättyyn alkioon. Kun S.head = 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.head = n, eli kapasiteetiltaan n alkiota olevaan pinoon yritetään lisätä uutta alkiota. Huomaa että lisätilan dynaamista allokointia ei tässä esimerkkitilanteessa ole. Tässä toteutuksessa taulukon indeksit lähtevät arvosta 1; esimerkiksi C- kielisessä toteutuksessa olisi tarkoituksenmukaista käyttää taulukon ensimmäisenä indeksinä arvoa 0. Koodin muuntaminen tähän tapaukseen on suoraviivaista. Kuva 2.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ä. Riippumatta pinossa olevien alkioiden lukumäärästä kaikki operaatiot kuluttavat saman vakioajan. EMPTY(S) 1. if S.head == 0 2. return TRUE 3. else 4. return FALSE PUSH(S,x) 1. S.head = S.head + 1 2. S[S.head] = x POP(S) 1. if EMPTY(S) 2. error Alivuoto pinossa 3. else 4. S.head = S.head 1 5. return S[S.head + 1] #Lisäys #Poisto
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 2.2 havainnollistaa jonon toteutusta indeksoidulla taulukolla. Taulukolla on nyt kaksi erillisesti määriteltyä indeksiä. Indeksi Q.head osoittaa aina jonon päähän ja indeksi Q.tail jonon hännässä olevaan seuraavaan tyhjään paikkaan. Jonon oma attribuutti pituus määrittelee jonon kapasiteetin. Itse jono koostuu alkioista Q[head], Q[head + 1], Q[head + 2],..., Q[tail 1]. Jos Q[head] = Q[tail], jono on tyhjä, lisäksi tyhjän jonon alkutilanteelle pätee head = tail = 1. Paikkaa indeksillä Q[tail] ei voi täyttää, koska tällöin jonon päitä edustavat indeksit limittyvät ja jono tulkitaan tyhjäksi. Huomaa että taulukolla toteutettu jono voi kiertää taulukon lopusta sen alkupuolelle, eli indeksi 1 seuraa indeksiä n. Tässäkin toteutuksessa taulukon indeksit lähtevät arvosta 1. Kuten pinon tapauksessakin, koodin muuntaminen käyttämään taulukkoa, jonka indeksit alkavat arvosta 0, on suoraviivaista. Kuva 2.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ä. Käytännön ohjelmaa laadittaessa tällaiset tapaukset on luonnollisesti aina otettava huomioon. Myön jonon operaatiot kuluttavat saman vakioajan riippumatta jonossa olevien alkioiden lukumäärästä.
ENQUEUE(Q,x) #Lisäys 1. Q[Q.tail] = x 2. if Q.tail == Q.pituus 3. Q.tail = 1 4. else 5. Q.tail = Q.tail + 1 DEQUEUE(Q) #Poisto 1. x = Q[Q.head] 2. if Q.head == Q.pituus 3. Q.head = 1 4. else 5. Q.head = Q.head + 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 2.3. Kuva 2.3. Linkitetyn listan kolme toteutusvaihtoehtoa: Yhteen suuntaan (a), kahteen suuntaan (b) sekä renkaan muotoon (c) linkitetty lista. Linkitetyn listan alkio (eli solmu) sisältää kolme kenttää avain (key), osoitin edelliseen alkioon (previous) sekä osoitin seuraavaan alkioon (next) sekä mahdollisesti satelliittidataa. Jos alkiolle x pätee x.previous = NIL, alkio x on listan pää (head). Jos taas alkiolle x pätee x.next = NIL, alkio x on listan häntä (tail). Tietorakenteen sisäinen attribuutti L.head osoittaa listan päähän, ja jos L.head = 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 voi huonoimmassa tapauksessa joutua käymään kaikki listan alkiot yksi kerrallaan läpi. Näin ollen vaadittavien operaatioiden lukumäärä on suoraan verrannollinen listassa olevien alkioiden lukumäärään n.
Operaatio LISTA_LISÄÄ(L,x) lomittaa uuden alustetun alkion x listan päähän muuttamalla listan head[l] attribuuttia sekä edellisen päänä toimineen alkion sisäistä previous osoitinta. Koska listan päähän lisääminen tekee korkeintaan neljä sijoitusta ja yhden vertailun, se on vakiokestoinen operaatio. Myös tietyn avaimen lisääminen järjestämättömään listaan on vakiokestoinen operaatio, koska edellisen lisäksi on ainoastaan tehtävä uusi solmu, jonka avainkenttänä on lisättävä avain. 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, kun tiedetään poistettava solmu x. Sen sijaan poistettaessa listasta tietty avain, on ensin etsittävä solmu kutsumalla ensin LISTA_ETSI operaatiota jonka ansiosta tällöin operaatioiden lukumäärä on suoraan verrannollinen listassa olevien alkioiden lukumäärää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.head 2. while x NIL and x.avain k 3. x = x.next 4. return x LISTA_LISÄÄ(L,x) 1. x.next = L.head 2. if L.head NIL 3. L.head.previous = x 4. L.head = x 5. x.previous = NIL LISTA_POISTA(L,x) 1. if x.previous NIL 2. (x.previous).next = x.next 3. else 4. L.head = x.next 5. if x.next NIL 6. (x.next).previous = x.previous Lähteet: [Cor] Cormen, T.H., Leiserson, C.E., Rivest, R.L., Stein, C. Introduction to Algorithms, 2 nd edition, The MIT Press 2001.