170 likes | 296 Views
Tavanomaisimpien tietorakenteiden ja algoritmien testauksesta. Lasse Bergroth. 1. Haku järjestämättömästä listasta tai vektorista. Etsitty alkio voi sijaita missä kohdin listaa / vektoria tahansa! Pahimmassa tapauksessa koko lista / vektori tutkittava läpi.
E N D
Tavanomaisimpien tietorakenteiden ja algoritmien testauksesta Lasse Bergroth
1. Haku järjestämättömästä listasta tai vektorista • Etsitty alkio voi sijaita missä kohdin listaa / vektoria tahansa! • Pahimmassa tapauksessa koko lista / vektori tutkittava läpi. • Toteutus yhteen suuntaan linkitetyn listan avulla • Varmistuttava, että on käytettävissä osoitin listan alkuun. • Muutoin listan alkupään alkiot jäävät tarkastelematta. • Jos haku epäonnistuu, koko lista on käyty läpi. Pitää varmistua, ettei yritetä testata enää avainarvoa, jos listaosoitin saa arvon NIL. • Muutoin tuloksena yleensä ajonaikainen virhe. • Tieto listaan tallennettujen alkioiden määrästä ei ole oleellinen. • Toteutus kahteen suuntaan linkitetyn listan avulla • Listan alkuosoitin ei ole tarpeen, mutta on varmistuttava, ettei etsintöjä lopeteta liian aikaisin. • Kahteen suuntaan linkitetyssä listassa haku epäonnistuu, kun lähtösolmusta on edetty molempiin suuntiin niin pitkälle kuin päästään (toisessa suunnassa edeltäjä- ja toisessa seuraajaosoitin NIL). • NIL-osoittimiin liittyvät rajatestit nytkin tarpeen.
1. Haku järjestämättömästä listasta tai vektorista • Toteutus rengaslistan avulla • Listan alkuosoitin ei ole tarpeen, mutta on varmistuttava, ettei etsintöjä lopeteta liian aikaisin. • NIL-testejä ei tarvita, sillä ei ole pelkoa joutumisesta ulos tietorakenteesta 4) Toteutus staattisen vektorin avulla • Vektoriin tallennettujen alkioiden lukumäärä tarpeen tietää. Muutoin • voidaan ajautua ajonaikaiseen virheeseen, jos vektori loppuu kesken. • voidaan saada myös virheellinen myönteinen vastaus (tieto haun onnistumisesta), mikäli etsitty arvo löytyy vektorin osasta, joka ei ole paraikaa käytössä (mutta johon on aikaisemmin ehditty tallettaa tietoa)!
2. Haku järjestetystä vektorista • Etsinnän hakualue voimakkaasti rajoitettu: puolitus- eli binäärihaulla tieto alkion löytymisestä (tai vaihtoehtoisesti haune epäonnistumisesta) saadaan työmäärällä O(log2n). • Hakualue kutistuu puoleen aikaisemmasta jokaisella yrityksellä. • Edellytykset puolitushaun toimimiselle: • Etsinnän kohteena oleva vektori on oltava lajiteltu. Muutoin menetelmä ei ole käyttökelpoinen (joudutaan turvautumaan lineaarihakuun)! • On varmistuttava, ettei puolitushakua (eikä mitään muutakaan alkioiden järjestämistä edellyttävää hakumenetelmää) kutsuta, ellei vektori ole lajiteltu! • Tarvitaan tieto vektoriin tallennettujen alkioiden lukumäärästä. • Tiedettävä, mitkä indeksirajat vektorista ovat käytössä.
2. Haku järjestetystä vektorista Esimerkki 1: Puolitushaku (kts. https://trakla.cs.hut.fi/ebook_fi/ebook-Puolitushaku.html) public class BinarySearch { /** * Binäärihaku, haetaan arvoa k taulukosta table */ public static int binarySearch( int table[], int k) { /* table: säilö; k: kiintoarvo */ int low = 0; /* low: kulkija, alaraja */ int high = a.length - 1; /* high: kulkija, yläraja */ int mid; /* mid, kulkija, keskikohta */ while( low <= high ) { mid = (low + high) / 2; if( table[mid] < k) low = mid + 1; else if(table[mid] > k) high = mid - 1; else return mid; } return -1; // Epäonnistunut haku } }
2. Haku järjestetystä vektorista • Puolitushakualgoritmi ohjelmoidaan usein virheellisesti. Pitää kiinnittää huomiota erityisesti seuraaviin seikkoihin: • Hakualueen rajojen oikea asettaminen? • Tutkitaanko vektorista varmasti kaikki paikat, joista haettavaa avainta kannattaa etsiä? • Poistetaanko kertaalleen jo tutkittu vektorin indeksi jatkotutkimusten ulkopuolelle, vai voiko haku jäädä junnaamaan paikoilleen? • Varmistutaanko, ettei vektorista yritetä tutkia positiota, jota ei ole olemassa tai joka on alustamaton? • Poistutaanko ohjelmasta heti, kun vastaus tiedetään? • Joko etsitty alkio on jo löydetty tai • Ei ole enää järkevää tapaa haun jatkamiseksi (epäonnistunut haku)
3. Pinojen ja jonojen käsittelystä Pinojen ja jonojen käsittelyn kannalta kriittiset kohdat olettaen, että ne on toteutettu vektorin avulla:Pinot: • Pinon täyttyminen – miten hallitaan? • Alkion lukemis- / poistoyrityksen estäminen pinon tultua tyhjäksi. Jonot: • Jonon alku- ja loppuindeksien ylläpito, kun jonoon lisätään tai sieltä poistetaan alkio. • On mahdollista, että jonon alkuosa sijaitsee fyysisesti vektorin loppupäässä ja jono loppuosa vastaavasti vektorin alussa! • Jonon täyttymisen tunnistaminen. • Alkion lukemis- / poistoyrityksen estäminen jonon tultua tyhjäksi.
4. Binäärinen hakupuu • Binäärisen hakupuun määritelmä: • Puun kaikilla solmuilla juurta lukuun ottamatta on yksikäsitteinen isäsolmu • Puun jokaiseen solmuun on tallennettuna avainarvo sekä mahdollisesti jotain ylimääräistä dataa • Lisäksi solmulla voi olla vasen ja/tai oikea seuraajasolmu • Mielivaltaisen solmun Z kohdalta alkavan vasemman alipuun kaikkien solmujen avainarvot ovat aidosti pienempiä kuin Z:n avainarvo • Samaisen solmun kohdalta alkavan oikean alipuun kaikkien solmujen avainarvot ovat vähintään yhtä suuria kuin Z:n avainarvo • Avainarvon etsiminen tai uuden avaimen lisääminen puuhun on verrattain yksinkertaista, sillä • Hakupolku on aina yksikäsitteinen • Samoin uuden solmun lisäämispolku: lisääminen tapahtuu aina lehtitasolle ja vaatii vain kolmen linkin asettamisen (isälinkki + vasen ja oikea seuraajalinkki [nämä ovat aluksi kumpikin NIL-osoittimia]) • Solmua poistettaessa on kuitenkin 3 eri tapausta, jotka vaativat huolellista linkkien päivittämistä. ---> Helposti jokin tilanne jää vahingossa huomiotta!
4. Binäärinen hakupuu Esimerkki 2: Avaimen poisto binäärisestä hakupuusta (kts. https://trakla.cs.hut.fi/ebook_fi/ebook-Alkion_poistaminen.html) /** Remove element k from subtree t */ public TreeNode remove( int k, TreeNode t ) { /* k: kiintoarvo, poistettava avain */ /* t: kulkija, tutkittava solmu */ if( k < t.getKey()) // Hakuvaihe t.left = remove( k, t.left ); else if( k > t.getKey() ) t.right = remove( k, t.right ); else if( t.left != null && t.right != null ) { // Tapaus: kaksilapsinen solmu TreeNode min = t.right; /* min: kulkija, oikean alipuun pienin */ while ( min.left != null ) min = min.left; t.setKey(min.getKey()); t.right = remove( min.getKey(), t.right ); } else // Tapaus: 0-1 lapsinen solmu t = ( t.left != null ) ? t.left : t.right; return t; }
5. Rekursiivisista algoritmeista yleensä • Jotta rekursiivinen algoritmi voisi olla toimiva, kahden perusedellytyksen pitää välttämättä toteutua • Algoritmissa pitää esiintyä ainakin yksi ns. helppo tai triviaali tapaus, joka ratkeaa sellaisenaan ilman uutta rekursiivista kutsua. • Jokainen kerta, kun algoritmia kutsutaan rekursiivisesti – eli siitä muodostuu uusi aktivaatio – lähestytään jotakin yksinkertaisesti ratkeavaa tapausta. • Mahdollisia virhelähteitä: • Triviaalin tapauksen joko puuttuu kokonaan, tai ainakaan sen toteutumista ei testata ennen kutakin uutta rekursiivista kutsua. • Rekursioaskel toteutettu virheellisesti, jolloin laskennassa ei tapahdu edistymistä. • Kummassakin tapauksessa seurauksina joko laskennan päättymättömyys tai ajonaikainen virhe muistiresurssien loppuminen kesken.
6. Lajittelumenetelmistä yleensä • Syötteeksi annetaan äärellinen joukko lajiteltavia avaimia yleensä joko vektori- tai listamuodossa. • Jos syöte on vektorimuotoinen, myös avainten lukumäärä on tiedossa. • Lajittelun loppuehtovaatimus: kutsujalle palautetaan tarkalleen samat avaimet, jotka saatiin syötteenä, mutta nyt ne on lajiteltuna joko ei-vähenevään (nousevaan, ellei duplikaatteja) tai ei-kasvavaan (laskevaan, ellei duplikaatteja) suuruusjärjestykseen • Yksikkötestausvaiheessa kannattaa asettaa rajoitus, joka takaa, että lopputulos on oikeellinen, käytettiinpä mitä lajittelualgoritmia hyvänsä! • Jos lajittelun lopputulos on virheellinen, se on merkki siitä, ettei kaikkia syötteen alkiopareja ole verrattu toisiinsa onnistuneesti – jotain on jäänyt tekemättä tai on suoritettu väärät toimenpiteet. • Toisinaan päädytään käyttämään lajittelualgoritmia, joka ei ole käyttökelpoinen sovellettavaan tilanteeseen. • Itse ohjelman testaaminen riippuu paljolti sille tyypillisistä ominaisuuksista (onko menetelmä rekursiivinen vai iteratiivinen, käsitteleekö se vektoreita vai listoja).
6.1. Pikalajittelu • Perustuu syötteen osittamiseen ns. pivot-alkioiden avulla niin pitkään, kunnes vektorissa ei enää ole jäljellä yhtään vähintään kahden alkion muodostamaa ositetta. • Ositusten päätyttyä lopputulos on jo valmis (osittaminen työlästä, mutta osatulosten yhdistäminen äärimmäisen yksinkertaista)! • Toimii keskimäärin ajassa O(nlog2n), mutta huonolla tuurilla suoritusajaksi voi tulla neliöllinen, jos esimerkiksi • pivot-alkion (alkion, jonka suhteen ositus tapahtuu) valinta epäonnistuu toistuvasti siten, että kaikki ositettavat alkiot pivot-alkiota lukuun ottamatta päätyvät yhteen ja samaan ositteeseen, kun taas toinen osite jää tyhjäksi • syötteessä runsaasti duplikaatteja • epäedullisin syöte: vektori jo valmiiksi lajiteltuna! • Ellei lajittelu valmistu kohtuullisessa ajassa, kannattaa pyytää ohjelmaa tulostamaan kuvaruudulle kulloinkin partitioinnin kohteena oleva vektorin osuus (tapahtuuko edistymistä vai ei?).
Esimerkki 3: Yksi versio pikalajittelualgoritmista (kts. https://trakla.cs.hut.fi/ebook_fi/ebook-Pikajarjestamismenetelma_el.html) void quicksort(int left; int right) { /* left, right: kulkijat */ int i; /* kiintoarvo */ if (right > left) { i = partition(left, right); quicksort(left, i-1); quicksort(i+1, right); } } int partition(int left; int right) { /* left, right: kiintoarvot; alueen reunat */ int i, j, pivot, apu; pivot = a[right]; /* pivot: kiintoarvo */ i = left - 1; /* i: askeltaja; vasemmalta oikealle */ j = right; /* j: askeltaja; oikealta vasemmalle */ do { do i++; while (a[i] < pivot); do j--; while (a[j] > pivot); apu = a[i]; /* apu: tilapäissäilö; vaihto */ a[i] = a[j]; a[j] = apu; } while (j > i); a[j] = a[i]; a[i] = a[right]; a[right] = apu; return i; }
6.2. Laskentalajittelu • Tehokas ja yksinkertainen lajittelumenetelmä, joka on kuitenkin käytettävissä vain tarkoin rajoitetusta määrittelyjoukosta peräisin oleville avaimille. • On tiedettävä etukäteen, mitä avaimia syötteessä voi esiintyä! • Ei voida käyttää reaalilukujen lajitteluun, eikä sitä myöskään kannata käyttää sellaisten kokonaislukujen lajitteluun, joilla arvoalueen vaihteluvälin pituus muuttuu herkästi. • Joudutaan varautumaan aina leveimpään mahdollisesti esiintyvään vaihteluväliin: ohjelman suoritus hidastuu tarpeettomasti.
7. Ehtolauseiden ja silmukoiden vartijat • Usein esiintyy yksinkertaisia ohjelmointivirheitä ehtolauseissa ja toistorakenteissa. Seuraavassa esitelty muutamia tilanteita: • Silmukan tai ehtolauseen aloitusehto asetetaan liian väljäksi esimerkkejä: 1. if ((x > 3) || (x < 7)) /* merkintä || on TAI-operaattori */ { … } 2. if ((x 1) || (x 2)) { … } • Edellisten ehtolauseiden vartijat ovat aina tosia! • Tapauksessa 1: jos x olisi pienempi tai yhtäsuuri kuin 3, jälkimmäinen vaihtoehto x < 7 toteutuu väkisin. • Tapauksessa 2: jos x olisi yhtäsuuri kuin 1, se olisi varmasti erisuuri kuin 2, eli jälleen ehtolause suoritettaisiin kaikissa tapauksissa!
7. Ehtolauseiden ja silmukoiden vartijat • Silmukan tai ehtolauseen aloitusehto asetetaan liian tiukaksi esimerkkejä: 1. while !((x 1) || (x 2)) /* merkintä ! on EI-operaattori */ { … } 2. if ((x < 0) && (x > 10)) /* merkintä && on JA-operaattori */ { … } • Edellisten ehtolauseiden vartijat ovat aina epätosia! • Tapauksessa 1: erisuuruus kahdesta eri vakiosta on aina tosi, joten sen negaatio ei toteudu koskaan! • Tapauksessa 2: x ei voi olla samanaikaisesti sekä negatiivinen että suurempi kuin 10, joten ehtolauseen vaatimus ei täyty koskaan.
7. Ehtolauseiden ja silmukoiden vartijat • Kirjoitusvirhe operaattoria valittaessa esimerkki: if (x < 0) || (x = 5)) /* merkintä = on C-kielen asetusoperaattori! */ • Mitä ilmeisimmin haluttaisiin testata, onko muuttujan x arvo suoritushekellä joko negatiivinen tai 5, mutta nyt • tutkitaan, onko muuttujan x arvo negatiivinen • asetetaan muuttujalle x arvoksi 5, mikä (tietystikin) onnistuu. Tällöin ehtolauseen ehdoksi tulee tosi, ja ehtolause suoritetaan, oli x:n arvo lauseen suorituksen käynnistyessä ihan mitä vaan! • Todellisuudessa olisi varmastikin haluttu kirjoittaa if (x < 0) || (x == 5)) /* merkintä == on C-kielen vertailuoperaattori! */ • kyseinen ehtolause suoritetaan ainoastaan, jos x on 5 tai negatiivinen.