58131 Tietorakenteet ja algoritmit (kevät 2013) Kurssikoe 1, 25.2.2013, vastauksia 1. (a) O-merkintä Ω-merkintä: Kyseessä on (aika- ja tila-) vaativuuksien kertalukumerkinnästä. O-merkintää käytetään ylärajan esittämiseen. Se siis kertoo, että algoritmi ei toimi koskaan huonommin kuin mitä merkintä on. Ω-merkintää käytetään taas kuvaamaan vaativuuden alarajaa. (b) binääripuu binäärihakupuu: Molemmat ovat puita. Binääripuu on puu, jonka jokaisella solmulla on korkeintaan kaksi lasta. Binääripuussa solmujen järjestyksellä ei ole väliä. Binäärihakupuu, on binääripuu, jonka solmut toteuttavat binäärihakupuuehdon: jos solmu v on solmun x vasemmassa alipuussa, niin v.key < x.key jos solmu o on solmun x oikeassa alipuussa, niin x.key < o.key (c) ketjutus avoin hajautus: Kyseessä on tapa hoitaa hajautuksen yhteentörmäyksiä. Ketjutuksessa yhteentörmäävät avaimet tallennetaan linkitettyihin listoihin. Avoimessa hajautuksessa kukin avain talletetaan suoraan hajautustauluun. Mikäli paikka on jo varattu, etsitään avaimelle toinen indeksi hajautustaulusta. Jos uusikin paikka on varattu, jatketaan etsimistä, kunnes vapaa paikka löytyy. Avoimessa hajautuksessa hajautustauluun mahtuu maksimissaan hajautustaulun koon verran avaimia, kun taas ketjutuksessa linkitettyjen listojen vuoksi ei vastaavaa kokorajaa ole. (d) primäärinen kasautuminen sekundäärinen kasautuminen: Kyseessä on hajautuksen useiden yhteentörmäysten ongelmatyyppejä. Primäärinen kasaantuminen voi esiintyä, kun avoimessa hajautuksessa on käytössä lineaarinen kokeilujono, jossa kokeillaan aina seuraavaa taulukon paikkaa, kunnes löydetään vapaa. Tällöin voi helposti muodostua pitkiä varattuja alueita. Sekundäärinen kasautuminen taas viittaa tilanteeseen, missä avoimessa hajautuksessa eri avaimilla on samat kokeilujonot, jolloin samaan hajautusarvoon kuvautuvat avaimet tarvitsevat helposti monta kokeilua ennen kuin löytyy vapaa paikka taulukosta. Tämä esiintyy esim. neliöisessä kokeilussa. 2. Tehtävään oli oleellisesti kaksi erilaista ratkaisutapaa: 1. Käydään puu läpi taso kerrallaan jonoa apuna käyttäen, aivan kuten viidensien laskuharjoitusten tehtävässä 4b. Tulostuksen sijasta laitammekin alkiot pinoon ja tyhjennämme pinon vasta kun koko puu on pinossa. Pseudokoodina:
tasotalhaaltaylos1(root) if root == Nil return Otetaan käyttöön apujono Q ja pino P enqueue(q,root) while not isempty(q) x = dequeue(q) push(p,x) if x.left!= Nil enqueue(q,x.left) if x.right!= Nil enqueue(q,x.right) while not isempty(p) print(pop(p)) Merkitään puun solmujen lukumäärää kirjaimella n. Algoritmin aikavaativuus on O(n), sillä siinä on kaksi peräkkäistä silmukkaa, jotka molemmat käyvät puun alkiot kertaalleen läpi. Algoritmin tilavaativuus on myös O(n), sillä pinossa P on ensimmäisen silmukan jälkeen talletettuna kaikki puun alkiot. 2. Toisesta tavasta on useita erilaisia versioita ja ne perustuvat puun korkeuteen. Ensin lasketaan puun korkeus käyttäen hyväksi esimerkiksi luentomateriaalin sivun 172 algoritmia laske-korkeus (tämäkin algoritmi kuuluu kirjoittaa auki). Merkitään puun korkeutta kirjaimella h. Tämän jälkeen puu käydään läpi h kertaa, tulostaen aina vain yhden tason solmut. Pseudokoodina: tasotalhaaltaylos2(root) if root == Nil return h = laskekorkeus(root) while h > 0 tulostasolmuttasolta(h,root) h--
tulostasolmuttasolta(i,x) if x == Nil return if i == 0 print(x) else tulostasolmuttasolta(i-1,x.left) tulostasolmuttasolta(i-1,x.right) Merkitään edelleen puun solmujen lukumäärää kirjaimella n ja korkeutta kirjaimella h. Nyt algoritmin aikavaativuus on O(n h), sillä metodin laskekorkeus aikavaativuus on O(n), ja metodin tulostasolmuttasolta on O(n). Lisäksi metodia tulostasolmuttasolta kutsutaan silmukassa, joka suoritetaan h kertaa. Tilavaativuus on luokkaa O(n), sillä rekursiopinossa on pahimmillaan n instanssia. Tämä tapahtuu silloin, kun puu on esimerkiksi oikealle suuntautuva ketju. Yleisiä virheitä: Jälkijärjestyksen, sisäjärjestyksen tai esijärjestyksen käyttö. Kahden pinon käyttö. Tilavaativuus väärin ratkaisutavassa 1. Vääränlaiset läpikäynnit eivät toimi esimerkiksi seuraavanlaiseen puuhun. 1 2 3 \ 4 5 6 \ 7 3. Annettu AVL-puu oli seuraava: 8 18 \ 6 13 20 12 (a) Puun saattavat epätasapainoon alkioiden 9,19 tai 21 lisääminen.
(b) Lisätään ensin alkio 11 8 18 \ 6 13 20 12 11 Nyt solmu 13 on alin solmu, joka on epätasapainossa. Koska epätasapaino on solmun 13 vasemman lapsen vasemmassa alipuussa, niin teemme kierron oikealle: 8 18 \ 6 12 20 11 13 Nyt puu on taas tasapainossa, joten voimme lisätä alkion 10: 8 18 \ 6 12 20 11 13 10 Nyt 8 on alin solmu, joka on epätasapainossa. Koska epätasapainon aiheuttaja löytyy solmun 8 oikean lapsen vasemmasta elipuusta, niin teemme ensin solmulle 12 kierron oikealle ja sen jälkeen kierrämme solmua 8 vasemmalle:
8 18 11 18 \ \ 6 11 20 8 12 20 \ 10 12 6 10 13 \ 13 Nyt puu on jälleen tasapainossa (c) Huom: Nyt aloitamme alkuperäisestä puusta. 8 18 \ 6 13 20 12 Alkion 18 poistaminen saattaa solmun epätasapainoon. 8 20 6 13 12 Koska epätasapainon aiheuttaja on solmun vasemman lapsen oikeassa alipuussa, niin kierrämme ensin solmua 8 vasemmalle, ja tämän jälkeen solmua oikealle. 13 13 20 8 \ 8 6 12 20 6 12 Nyt puu on jälleen tasapainossa ja voimme poistaa alkion 8. Koska alkiolla 8 on kaksi lasta, niin sen paikalla tulee luvun 8 seuraaja, eli luku 12.
13 12 6 20 Yleisiä virheitä: Kiertoja tehtiin enemmän kuin pitäisi. Korjattiin jotain muuta alkiota kuin alinta, joka on epätasapainossa. 4. (a) Vastauksista piti tulla selväksi mitä pakan operaatioilta vaaditaan, ja jokin tai joitakin ratkaisuita niiden vakioaikaisuuteen saattamiseksi. Esimerkiksi: Listan alkua koskevissa insert ja remove operaatioissa tarvitsee käsitellä ainostaan listan ensimmäistä tietuetta, sekä sen naapureita. Tämä pätee myös listan loppua käsitteleville insert ja remove operaatioille. Tähän käsittelyyn liittyy ainostaan solmujen viitteiden päivitystä, joka tapahtuu vaikioaikaisilla sijoituslauseilla. Ratkaisuksi riittää siis sellainen kahteen suuntaan linkitetty lista, jolla päästään käsiksi sekä listan loppuun, että listan alkuun vakioajassa. Ratkaisuksi kelpaa siis esimerkiksi rengaslista, jossa listan ensimmäisen alkion prev viitteellä päästään listan viimeiseen alkioon. Tätä, ja toista mahdollista ratkaisua jossa listan lopusta pidetään kirjaa erillisellä P.tail kentällä, esitellään (b)-kohdan vastauksessa. (b) Pakka voidaan esittää muunmuassa kahteen suuntaan linkitettynä rengaslistana. Tällöin operaatiot ovat kurssin konventioita noudattaen seuraavanlaiset: insertfirst(p, x) 2 P.head = x 3 x.next = x 4 x.prev = x 5 return 6 x.prev = P.head.prev 7 P.head.prev.next = x 8 x.next = P.head 9 P.head.prev = x 10 P.head = x Algoritmi insertfirst koostuu kahdesta suoritushaarasta. Jos algoritmille annettu pakka on tyhjä, suoritetaan ehdon tarkistuksen jälkeen (rivi 1) suorittamaan rivit 2-5. Muussa tapauksessa suoritetaan rivit 6-10. Rivit 2-5 koostuvat vakioaikaisista sijoitusoperaatioista. Sama pätee riveihin 6-10. Rivin 1 ehtolause koostuu tasan yhdesta aliohjelmakutsusta: kutsutaan empty funktiota pakalle. Oletetaan että empty funktio on vakioaikainen aliohjelma, ja näytetään tämä vasta empty operaation yhteydessä. Nyt rivin 1 vaatima suoritusaika on jokin vakio c 1. Suoritusaika tulee
tällöin olemaan c 1 + max{ 5 i=2 c i, 10 i=6 c i} = O(1), missä c i on rivin i vaatima vakiosuoritusaika. Algoritmin insertfirst tilavaativuus on myös vakio. Algoritmissa käytetään ainoastaan annettuja muuttujia P ja x, eikä uusia muuttujia alusteta missään vaiheessa. Aliohjelmaa empty kutsutaan jokaisessa insertfirst suorituksessa tasan kerran, joten se muodostaa vain yhden aktivaatiotietueen, eikä siten aktivaatiotietuepinonkaan koko kasva. insertlast(p, x) 2 P.head = x 3 x.next = x 4 x.prev = x 5 return 6 x.prev = P.head.prev 7 P.head.prev.next = x.prev 8 x.next = P.head 9 P.head.prev = x Operaatio insertlast on hyvin samankaltainen insertfirst:n kanssa. Siinä suoritetaan ehtolausekkeen jälkeisessä haarassa yksi sijoitus vähemmän. Olettaen että empty voidaan suorittaa ajassa O(1), voidaan tämäkin algoritmi suorittaa ajassa O(1), sillä tämänkin algoritmin kumpikin haara koostuu vakioaikaisista sijoitusoperaatioista. Tilavaativuuskin on sama kuin algoritmissa insertfist, täsmälleen samoin perustein. removefirst(p, x) 2 return NIL 3 if P.head == P.head.next Jos ainoa alkio 4 retval = P.head 5 P.head = NIL 6 return retval 7 retval = P.head 8 P.head.prev.next = P.head.next 9 P.head = P.head.next 10 P.head.prev = retval.prev 11 return retval Algoritmin removefirst suoritus voi jakautua kolmeen eri haaraan. Olettaen, että funktio empty on vakioaikainen, suoritetaan rivi 1 vakioajassa. Mikäli empty palauttaa arvon true, suoritetaan ensimmäinen haara, joka koostuu pelkästään vakioaikaisesta return lauseesta. Muussa tapauksessa suoritetaan rivin 3 ehtolause, joka koostuu vain yhdestä vakioaikaisesta
loogisesta operaattorista käytettyjen tietueiden kentille. Rivin kolme ehtolause on siis myös vakioaikainen. Rivit 4-6 ovat myös vakioaikaiset, sillä ne koostuvat sijoitusoperaatioista, ja return lauseesta. Muussa tapauksessa suoritettavat rivit 7-11 koostuvat myös pelkästään sijoituslauseista ja return lauseesta. Operaatio removefirst on siten vakioaikainen. Operaatiossa removefirst käytetään yhtä apumuuttujaa, nimeltään retval, jota käytetään muissa haaroissa kuin ensimmäisessä. Tämä, sekä empty aliohjelman aktivaatiotietue vievät vakiomäärän muistia, joten remove- First suoriutuu vakiotilassa. removelast(p, x) 2 return NIL 3 if P.head == P.head.next Jos ainot alkio 4 retval = P.head 5 P.head = NIL 6 return retval 7 retval = P.head.prev 8 retval.prev.next = retval.next 9 P.head.prev = retval.prev 10 return retval Algoritmin removelast tila- ja aikavaativuus ovat samat kuin remove- First:llä, sillä vain viimeinen suoritushaara on erilainen, ja sekin on yhtä apumuuttujaa käyttävä, sekä sijoitus- ja return-lauseesta koostuva. empty(p) 1 return P.head == NIL Funktion empty suoritus on selvästi vakioaikainen, sillä se koostuu return lauseesta, joka palauttaa yhden loogisen operaattorin evaluoinnin tuloksen, jonka toinen operandi on annetun P-tietueen kenttä, ja toinen on globaalisti tunnettu NIL arvo. Se suoriutuu myös vakiotilassa, sillä se ei käytä yhtäkään aliohjelmakutsua, tai alusta yhtäkään muuttujaa. Koska empty voidaan suorittaa ajassa O(1), voidaan myös ylläkuvatut operaatiot suorittaa vakioajassa. Pakkaa voidaan esittää myös esim. niin että sillä on kaksi kenttää: P.head ja P.tail jotka viittaavat pakan alimpaan ja ylimpään alkioon. Ne ovat siis aluksi NIL, kuten ollaan totuttu näkemään pelkästään listan L.head tapauksessa. Nyt operaatiot ovat seuraavanlaisia:
insertfirst(p, x) 1 x.next = P.head 2 x.prev = NIL 3 if P.head!= NIL 4 P.head.prev = x 5 else 6 P.tail = x 7 P.head = x insertlast(p, x) 1 x.prev = P.tail 2 x.prev = NIL 3 if P.tail!= NIL 4 P.tail.prev = x 5 else 6 P.head = x 7 P.tail = x removefirst(p) 2 return NIL 3 retval = P.head 4 if P.head.next!= NIL 5 P.head = P.head.next 6 P.head.prev = NIL 7 else 8 P.head = NIL 9 P.tail = NIL 10 return retval removelast(p) 2 return NIL 3 retval = P.tail 4 if P.tail.prev!= NIL 5 P.tail = P.tail.prev 6 P.tail.next = NIL 7 else 8 P.head = NIL 9 P.tail = NIL 10 return retval empty(p) 1 return P.head == NIL Tässä esitystavassa aika- ja tilavaativuudet ovat samat kuin rengaslistatoteutuksella. Analyysi voidaan tehdä analogisesti, mutta on jätetty tässä suuren samankaltaisuuden vuoksi laatimatta.
Myös kaikki muut tavat esittää pakka kahteen suuntaan linkitettynä listana ovat hyväksyttäviä. On esimerkiksi hyväksyttävää käyttää kahteen suuntaan linkitettyä tunnussolmullista rengaslistaa, tai tavallista kahteen suuntaan linkitettyä listaa, jolloin operaatiot eivät enää olekaan vakioaikaisia. Arvostelusta: Aika- ja tilavaativuudesta sai yhden pisteen.