58131 Tietorakenteet ja algoritmit (kevät 2016) Ensimmäinen välikoe, malliratkaisut 1. Palautetaan vielä mieleen O-notaation määritelmä. Olkoon f ja g funktioita luonnollisilta luvuilta positiivisille reaaliluvuille. Merkitsemme f = O(g) jos on olemassa positiivinen vakio d ja jokin luonnollinen luku n 0, joilla f(n) d g(n) kaikilla n n 0. (a) Pitää paikkansa. Kun n 1, niin 5n 3 + 7n + 4 5n 3 + 7n 3 + 4n 3 = 16n 3, joten etsityiksi vakioiksi voidaan valita d = 16 ja n 0 = 1. (b) Pitää paikkansa. Kun n 1, niin log 2 (n) = log 2 ((n 1/3 ) 3 ) = 3 log 2 (n 1/3 ) < 3n 1/3, missä viimeisessä arviossa käytettiin erikseen tehtävänaannossa mainittua epäyhtälöä log 2 (x) < x. Voidaan siis valita d = 3 ja n 0 = 1. Kummastakin kohdasta oli mahdollista saada kaksi pistettä. Ensimmäisen pisteen sai oikeasta vastauksesta ja toisen vastauksen matemaattisesta perustelusta. 2. Vastauksesta sai pisteitä muun muassa seuraavilla faktoilla: Yhden pisteen fakta: Lisäysjärjestämisen tilavaativuus on O(1), joka on parempi kuin muilla. Lisäysjärjestäminen on muita nopeampi pienillä taulukoilla. Lisäysjärjestämisen paras tapaus toimii ajassa O(n). Lisäysjärjestäminen toimii keskimäärin ajassa O(n 2 ), joten se on suurilla aineistoilla muita huomattavasti hitaampi. Lomitusjärjestäminen toimii pahimmassakin tapauksessa ajassa O(n log(n)), toisin kuin muut tässä olleet algoritmit. Lomitusjärjestämisen tilavaativuus O(n) on suurempi kuin muilla. Pikajärjestäminen on keskimäärin näistä algoritmeista käytännössä nopein. Pikajärjestäminen ei ole vakaa, kuten kaksi muuta algoritmia. Yhdeen pisteen faktasta sai vain puoli pistettä, jos siitä puuttui jotain oleellista (kuten vaikka lihavoitu keskimäärin). Puolikas piste saattoi tippua myös muunlaisista virheistä. Puolen pisteen fakta: Lisäysjärjestäminen on yksinkertaisin toteuttaa. Lisäysjärjestäminen on vakaa. Lomitusjärjestäminen on vakaa. Pikajärjestämisen tilavaativuus on O(n), joka on parempi kuin lomitusjärjestämisen. Pisteitä ei kuitenkaan laskettu yhteen ihan suoraan. Jokaiselle algoritmille oli valittu yksi piste hyviä ja yksi huonoja puolia kohtaan, eikä näitä pisteitä voinut korvata toisillaan. Lopulliset pisteet saatiin laskemalla kaikkien osatehtävien pisteiteet yhteen ja pyöristämällä ylöspäin. 3. Tehtävän pisteet jakautuivat seuraavalla tavalla: 1
2p toimivasta algoritmista. 1p oikeasta aikavaativuudesta. 1p aikavaativuuden perustelusta (ei vaadittu matemaattista perustelua O-notaation määritelmästä lähtien). Toimivia ratkaisuja oli oleellisesti kahta erilaista: ensimmäinen ratkaisu oli ensin järjestää annetut luvut jollain pahimmassa tapauksessa O(n log(n)), ajassa toimivalla järjestämisalgoritmilla, kuten esimerkiksi lomitusjärjestämisellä, ja sen jälkeen selvittää vastaus taulukosta yhdellä läpikäynnillä. Tämä näyttäisi pseudokoodina seuraavalta: //muutetaan ensin jono taulukoksi luvut[] merge-sort(luvut) nyt=1 eniten=1 enitenesiintyvä=luvut[1] for i=2..luvut.length if luvut[i]==luvut[i-1] nyt++ else nyt=1 if nyt>eniten eniten=nyt enitenesiintyvä=luvut[i] return enitenesiintyvä Algoritmi toimii ajassa O(n log(n)), sillä järjestämisen lisäksi muut operaatiot vievät vain lineaarisesti aikaa syötteen pituuden suhteen. Toinen mahdollinen ratkaisu käytti hyväkseen tasapainotettua binääripuuta. Siinä pidettiin puussa tietoa siitä, kuinka monta kertaa kukin luku esiintyy annetussa jonossa. Lopulta puun kaikki avaimet käydään läpi, ja palautetaan se, jolla on suurin arvo. Pseudokoodina ratkaisu näyttäisi seuraavalta: //Olkoon Q syötteenä annettu jono //esiintymisiä on Javan TreeMap-rakenteen kaltainen binääripuu, //jossa sekä avaimina että arvoina on kokonaislukuja. //Oletetaan vielä, että puuttuvan avaimen arvo alustuu kysyttäessä //automaattisesti arvoon 0. while!q.isempty() esiintymisiä[q.pop()]++ eniten=0 enitenesiintyvä 2
for k in esiintymisia.keyset if esiintymisiä[k]>eniten eniten=esiintymisiä[k] enitenesiintyvä=k return enitenesiintyvä Algoritmi toimii ajassa O(n log(n)), sillä tasapainoisesta puusta voidaan hakea viittaus arvoon ajassa O(log(n)), ja tämä tehdään n kertaa. Lopussa tapahtuva puun läpikäynti saadaan tehtyä O(n)-ajassa esimerkiksi käymällä puu läpi sisäjärjestyksellä. Täten lopullinen aikavaativuus on O(n log(n)). Koska algoritmin piti toimia pahimmassa tapauksessa ajassa O(n log(n)), ei täysiä pisteitä voinut saada käyttämällä taulukon järjestykseen pikajärjestämistä, tai korvaamalla tasapaunotettun binäärihakupuun esimerkiksi hajautustaululla. Syötteenä olevien lukujen suuruudesta ei saanut olettaa mitään ylimääräistä, joten binäärihakupuuta ei voinut myöskään korvata taulukolla laskemisjärjestämisen tapaan. 4. Tarkastellaan binäärihakupuuta, jonka jokaisessa solmussa x on solmun korkeutta varten kenttä x.height. (a) (2p) Puun solmun korkeus on matka siitä sen kaukaisimpaan lehteen. Algoritmin ideana on edetä rekursiivisesti lehteen, aseettaa tämän korkeudeksi 0 ja palauttaa arvon lehden rekursiivisesti kutsuneelle solmulle, eli sen vanhemmalle. Vanhempi vertaa kahdelta lapseltaan saatua arvoa keskenään ja asettaa korkeudekseen näistä suuremman +1. Tämä solmu palauttaa taas omalle vanhemmalleen oman korkeutensa, jne. int asetakorkeus(r) 1 if r == NIL return -1 2 int vasenk = asetakorkeus(r.left()) 3 int oikeak= asetakorkeus(r.right()) 4 r.height = Math.max(vasenK, oikeak) +1 (b) (2p) Algoritmi tarkistaa jos tältä solmulta puuttu yksikään lapsi. Jos näin on ja sen korkeus on yli 1, tiedämme että puu rikkoo AVL-puu ehdon. Muuten etenemme tarkistamaan ovatko solmun lasten korkeudet vähintään yhden päässä toisistaan. Jos ei, palautamme false. Jos on, tarkistamme täyttävätkö solmun alipuut AVL-puu ehdon. Näin algoritmi käy jokaisessa solmussa rekursiivisesti tarkastamassa että ehto on voimassa tässäkin solmussa. boolean onkoavl(r) 1 if r == NIL return true 2 if r.right == NIL r.left == NIL 3 if r.height > 1 return false 4 else return true 5 if Math.abs(r.left.height - r.right.height) < 1 6 return (onkoavl(r.left) AND onkoavl(r.right)) 3
7 return false 5. (a) (1p) Avaimen 16 lisäyksen jälkeen ei puun solmu ole enään tasapainossa. 16 Solmua pitää siis kiertää vasemmalle jotta tasapaino saavutettaisiin. 15 7 11 16 (b) (1p) Avaimen 12 lisäykseen jälkeen ei solmu täytä enään AVL-puun ehtoja. 7 5 9 11 15 12 Koska epätasapaino on solmun vasemman lapsen oikeassa alipuussa, tar- 4
vitaan kaksoiskierto. Ensiksi epätasapainossa olevan solmun vasemman lapsen () kierto vasemmalle, ja sitten itse solmun kierto oikealle. 15 7 11 5 9 12 7 11 15 5 9 12 (c) (2p) Kun poistamme avaimen 37, muodostuu solmuun 41 epätasapaino. 32 41 48 Ja koska epätasapaino tulee solmun oikean lapsen vasemmasta alipuusta, pitää tehdää kaksoikierto 5
32 41 48 32 41 Koska tämän korjauksen alkulähtökohtana oli poisto, tulee meidän tarkastaa puu uudelleen ensimmäisen korjauksen jälkeen. Ja kuten näemme yllä olevasta kuvasta on solmuun 32 muodostunut epätasapaino korjauksen johdosta. Meidän tulee siis tehdä vielä yksi kierto, kohdistuen tällä kertaa solmuun 32 oikeanpuoleisena. 32 41 48 6