1 Muita linkattuja rakenteita Johdanto Aikaisemmin on käsitelty listan, jonon ja pinon toteutus dynaamisesti linkattuna rakenteena. Dynaamisella linkkauksella voidaan toteuttaa mitä moninaisimpia rakenteita. Seuraavassa annetaan muutamia esimerkkejä. Kaikissa esimerkeissä kiinnitetään huomiota myös abstraktioon. Tämä merkitsee, että dynaamisesti linkattuja rakenteita pidetään yhtenä toteutusvaihtoehtona erilaisille abstrakteille tietotyypeille. Niille voitaisiin esittää myös taulukkoon perustuva toteutus. Tässä käsiteltävät esimerkit ovat renkaaksi linkitetty lista, kaksoislinkattu lista, ns. harva matriisi, ja binäärinen etsintäpuu. Renkaaksi linkitetty lista Renkaaksi linkitetty lista tarkoittaa tavallista listaa, jossa viimeiseen alkioon ei laiteta NULLosoitetta, vaan sinne laitetaan ensimmäisen alkion osoite. Renkaaksi linkitetty lista näyttää silloin seuraavalta: cll 11.0 12.0 13.0 Eräänä sovelluksena voidaan mainita vaikkapa mittaustietojen suodatus. Yllämainitun kuvan renkaaksi linkitetty lista voisi palvella sovellusta, jossa mittausarvoja suodatetaan laskemalla aina kolmen viimeisen mittauksen keskiarvo. Uuden mittausarvon tallennus ja keskiarvon laskenta käyvät kätevästi, kun osoitin osoittaa aina vanhinta mittausta. Tällöin uusi mittaus tallennetaan aina osoittimen osoittamaan paikkaan ja osoittimelle päivitetään uusi arvo, joka saadaan vanhan solmun next-kentästä. Yleisyyttä voidaan lisätä paketoimalla tietotyypin Tcll (tyyppi circularly linked list) sisälle renkaaseen kulloinkin kuuluvien mittausten määrä. Lukija voi miettiä, millä tavalla tällaiseen renkaaksi linkitettyyn listaan lisätään uusi alkio ja uusi mittausarvo ja miten lasketaan siellä olevien mittausten keskiarvo. Oletetaan, että kuvan mukaisessa tilanteessa solmun tietotyyppi on määritelty seuraavasti: typedef struct node { float mittaus; struct node *next; Tnode; Silloin uusi mittausarvo (ei siis uusi solmu) lisätään renkaaseen seuraavasti cll->mittaus = uusi_arvo; cll = cll->next; Kaksoislinkattu lista
2 Aikaisemmin on käsitelty dynaamisesti linkattu lista. Siinä linkkien avulla voidaan kulkea ensimmäisestä alkiosta viimeiseen. Kaksoislinkatun listan ajatus on siinä, että solmuihin laitetaan linkki seuraavaan solmuun ja edelliseen solmuun. Tällöin toista linkkiketjua pitkin päästään kulkemaan lista alusta loppuun ja toista linkkiketjua pitkin lopusta alkuun. Kaksoislinkattu lista näyttää käytännössä seuraavalta. dbll first last a e g Kun abstraktiovaatimus huomioidaan, määritellään esimerkiksi tietotyyppi Tdbll (tyyppi double linked list), jolle sitten määritellään asiaan kuuluvat operaatiofunktiot, kuten void alusta(tdbll *dbll); void lisaa_alkio(titem alkio, Tdbll *dbll); void poista_alkio(titem *alkio, Tdbll *dbll); Kaksoislinkattua listaa kannattaa käyttää sovelluksissa, joissa pitää päästä liikkumaan sekä eteenpäin, että taaksepäin. Eräs sovellus on vaikkapa määrittelemättömän pituisten lukujen esittäminen ja laskutoimitukset niille. Luvun numerot tallennetaan kaksoislinkattuun listaan. Lukua syötettäessä lista rakennetaan alusta loppuun. Lukua tulostettaessa lista käydään myös alusta loppuun. Lukuja laskettaessa yhteen listaa käydään läpi lopusta alkuun. Harva matriisi Taulukkolaskentaohjelmissa käyttäjälle tarjotaan laskentaa varten taulukkopohja, jossa on paljon soluja (jopa miljoonia soluja). Jokaiseen soluun liittyy paljon tietoja, esimerkiksi solun varsinainen tietosisältö ja sen muotoilutiedot. Kuitenkin vain pieni osa taulukon soluista on yleensä kerrallaan käytössä. Tämän takia ei muistista kannata varata tilaa kaikille alkioille heti alussa, vaan sitä mukaa, kuin taulukosta otetaan alkioita käyttöön. Samanlaista periaatetta voidaan käyttää lukumatriisien esittämiseen, joilla suurin osa alkioista on arvoltaan 0. Tällaista matriisia kutsutaan harvaksi matriisiksi. Alla on esimerkki harvasta matriisista. 5 0 0 1 0 3 0 0 0 0 6 7 0 0 0 9 Harvan matriisin esittämisessä tietokoneen muistissa voidaan säästää tilaa, kun nolla-alkioita ei esitetä ollenkaan muistissa. Seuraava kuva esittää periaatteen, jolla tällainen matriisi voidaan esittää. Muistista on varattu tilaa vain nollasta poikkeaville alkioille. Alla oleva kuva esittää edellä olevaa 4x4-matriisia.
3 matrix 1 1 5 4 1 2 2 3 3 3 6 4 7 4 4 9 Kun matriisia tarkastellaan abstraktina tietotyyppinä, sille on määriteltävä tietotyyppi Tmatrix ja operaatiofunktiot, joista seuraavat esimerkit: void alusta(tmatrix *matrix); void lue_matriisi(tmatrix *matrix); void tulosta_matriisi(tmatrix matrix); int onko_nelio(tmatrix matrix); float determinantti(tmatrix matrix); int palauta_rivi_maara(tmatrix matrix); int palauta_sarake_maara(tmatrix matrix); void summaa(tmatrix a, Tmatrix b, Tmatrix *c); void kerro(tmatrix a, Tmatrix b, Tmatrix *c); Binäärinen etsintäpuu Puu on yksi perustavaa laatua oleva tietorakenne tietojenkäsittelytekniikassa. Puu tarjoaa esimerkiksi tavan etsiä tietoa nopeasti. Sitä käytetään mm. hakemistoissa ja kielen prosessointisovelluksissa (ilmaisupuut, expression trees). Puu on monimutkaisempi tietorakenne kuin useimmat muut ADT:t. On olemassa useita puiden toteutustapoja, jotka voivat perustua sekä taulukkoon että linkattuihin rakenteisiin. Ennen kuin määrittelemme binäärisen etsintäpuun, määritellään binääripuu (binary tree): Binääripuu on äärellinen joukko solmuja, joka on joko tyhjä tai muodostuu juurisolmusta ja kahdesta erillisestä binääripuusta, joita kutsutaan vasemmaksi alipuuksi ja oikeaksi alipuuksi. Puun solmu voi sisältää tietoa, jonka luonne riippuu sovelluksesta. Kuten muutkin säiliöt myös puun toteutus voidaan tehdä riippumattomaksi solmuihin tallennettavan tiedon tyypistä. Huomaa, että binääripuun määritelmä on rekursiivinen. Oppitunnilla testataan määritelmän toimivuus käytännössä. Määritelmä. Binäärinen etsintäpuu on binääripuu, jossa kaikki arvot vasemman alipuun solmuissa ovat pienempiä kuin arvo juurisolmussa ja kaikki arvot oikean alipuun solmuissa ovat suurempia kuin arvo juurisolmussa. Lisäksi binäärisen etsintäpuun molemmat alipuut ovat binäärisiä etsintäpuita.
4 Puun läpikäynnillä (traverse a binary tree) tarkoitetaan prosessia, jolla käydään puun jokaisessa solmussa jonkin systemaattisen järjestyksen mukaan. Seuraavassa määritellään kolme tunnettua tapaa puun läpikäymiseen (huomaa, että määritelmät ovat rekursiivisia): Etujärjestys (Preorder traversal): 1. Käydään juuressa 2. Käydään läpi vasen alipuu 3. Käydään läpi oikea alipuu Välijärjestys (Inorder traversal): 1. Käydään läpi vasen alipuu 2. Käydään juuressa 3. Käydään läpi oikea alipuu Jälkijärjestys (Postorder traversal): 1. Käydään läpi vasen alipuu 2. Käydään läpi oikea alipuu 3. Käydään juuressa Binäärisen etsintäpuun läpikäynti välijärjestyksessä merkitsee puussa olevien tietojen läpikäyntiä suuruusjärjestyksessä (treesort). Binääripuuta sanotaan täydelliseksi, jos sen oikeassa ja vasemmassa alipuussa on yhtä monta solmua ja lisäksi sen molemmat alipuut ovat täydellisiä. Onnistunut etsintä täydellisestä binääripuusta vaatii noin lg N vertailua. Menetelmä on paras mahdollinen silloin, kun aika riippuu vertailujen lukumäärästä (eikä levysiirtojen lukumäärästä). Ongelma sinänsä on, kuinka voidaan taata, että binäärinen etsintäpuu pysyy lisäyksissä ja poistoissa riittävän pensasmaisena? Samasta tietoaineistosta voidaan rakentaa useita erilaisia etsintäpuita (mutta ei täydellisiä). Jos lisäys puuhun tehdään yksinkertaisella tavalla ilman mitään tasapainon säilyttämiseen pyrkivää algoritmia, puun muoto riippuu siitä, missä järjestyksessä tiedot on lisätty puuhun. Kuten yllä on sanottu binäärinen etsintäpuu voidaan toteuttaa eri tavoilla. Se voidaan toteuttaa esimerkiksi taulukolla tai dynaamisesti linkatulla rakenteella. Dynaamisesti linkattu rakenne on helppo toteuttaa. Ainoastaan operaatio DeleteNode vaatii enemmän vaivaa ja pohdintaa ja samoin lisäys siinä tapauksessa, että halutaan varmistaa puun tasapainoisuus. Liitteenä on esimerkki. Tarvittavat tietomäärittelyt ovat typedef??? Titem; //määritellään puuhun tallennettava tiedon tyyppi typedef struct node *Tpointer; typedef struct node { Tpointer left; Titem item; Titem right; Tnode; typedef Tpointer Tree; muutamia esimerkkejä puun operaatioista: Ttree Create(); void PutItemToTree(Tree *tree, Titem item); int IsItemInTree(Tree tree, Titem item);
5 void in_order_traverse( Ttree tree ); jne. Seuraavassa on puun alkioiden läpikäynnin välijärjestyksessä toteuttava funktio. Tämä ensimmäinen versio lähtee siitä, että rekursiota ei käytetä. Sen sijaan käytetään omaa osoittimien pinoa niiden solmujen tallentamiseen, jotka ovat tulleet vastaan ja joissa ei vielä saa käydä. void in_order_traverse( Ttree tree ) { TStack pointerstack; Ttreenode *p; initialize_stack(&pointerstack); p = tree; while ( (p!= NULL)!is_stack_empty(&pointerstack)) { while (p!= NULL) { push(&pointerstack, p); p = p->left; pop(&pointerstack, &p); printf("%d ", p->item); p = p->right; Seuraavassa on puun alkioiden läpikäynnin välijärjestyksessä toteuttava funktio rekursiivisena. Huomataan, että tämä funktio on erittäin selkeä. Koska puun määritelmä jo oli rekursiivinen, rekursiiviset funktiot sopivat erittäin hyvin puun operaatioiden toteuttamiseen. void in_order_traverse( Ttree tree ) { if (!is_tree_empty(tree)) { in_order_traverse( tree -> left); printf(" %c", tree->item); in_order_traverse ( tree -> right); Dynaamisesti linkatun rakenteen ja taulukkorakenteen vertailu Dynaamisesti linkatussa rakenteessa alkioiden järjestys esitetään erillisten linkkien avulla. Taulukkorakenteessa järjestys esitetään alkion sijaintipaikalla muistissa. Taulukkorakenteessa kaikki alkiot sijaitsevat vierekkäisissä muistipaikoissa ja taulukolle varattu tila muodostaa siksi yhtenäisen alueen tietokoneen muistissa. Taulukkoa kutsutaan tämän takia englanniksi myös nimellä contiguous list, kun taas dynaamisesti linkattua listaa kutsutaan dynamically linked list. Huomaa siis, että taulukko ja dynaamisesti varattu linkattu lista ovat vaihtoehtoisia toteutustapoja listan määrittelyssä esiintyvälle joukolle tietoalkioita, joiden kesken on määritelty järjestys. Seuraavassa verrataan näiden toteutusten ominaisuuksia. Ominaisuus Muistin varaus Dynaamisesti linkattu lista Ei ylivuotoa. Kaikki koneen vapaa muisti on Taulukko Muisti varattava kokonaan ennen kuin käyttö alkaa,
6 Tiedon haku Järjestyksen ilmaisutapa käytettävissä. Vain peräkkäishaku mahdollista. Järjestys osoitetaan linkeillä. jolloin muisti voi loppua kesken tai sitä on varattuna liikaa. Myös suorahaku on mahdollista, kun järjestysnumero tiedetään. Järjestys ilmaistaan sijainnilla muistissa. Operaatioiden vaatima prosessoriteho Lisäykset väliin ja poistot välistä ovat helppoja.. Lisäyksissä ja poistoissa väliin tarvitaan tietojen siirtoa. Ohjelmointi Vaatii vähän harjoittelua. On helppoa. Yhteenveto abstraktien tietotyyppien käytöstä On käsitelty standardeja abstrakteja tietotyyppejä, jotka ovat yleiskäyttöisiä, sovellusalueesta riippumattomia komponentteja. Tällaisia ovat esimerkiksi listat jono pino joukko merkkijono puu jne Ohjelmiston kehitysprojekteissa voidaan tehdä sovellusaluekohtaisia abstrakteja tietotyyppejä. Niiden rakentamisessa voidaan käyttää usein hyväksi edellä mainittuja yleisiä abstrakteja tietotyyppejä. Esimerkkeinä sovellusaluekohtaisista abstrakteista tietotyypeistä ovat mm. mittaukset (katso labraharjoitus) kolmio kompleksiluku matriisi urheilulaji_kilpailussa dynaaminen muisti jne Abstraktien tietotyyppien käytöllä saavutetaan mm seuraavia etuja: Ohjelmat ja niiden laatiminen yksinkertaistuu. Ohjelmointityö voidaan jakaa kätevästi useammalle henkilölle. Ohjelmiin ei tule niin helposti virheitä. Ohjelmien testaus helpottuu. On helpompi tuottaa uudelleenkäytettävää koodia.